forked from Mirrors/keyoxide-web
Compare commits
101 commits
redesign-2
...
dev
Author | SHA1 | Date | |
---|---|---|---|
abfa9697b1 | |||
|
353cd3b1e5 | ||
|
7b5aa4703a | ||
|
b8c94ebc0b | ||
|
9caa1f6795 | ||
|
ba532be4f3 | ||
|
567130f634 | ||
|
a57d24ad6a | ||
|
255e99af39 | ||
|
785647bbb8 | ||
|
3fb5fb860f | ||
|
e7c1a878ff | ||
|
ed4c265dad | ||
|
912f3619eb | ||
|
9e19a622d9 | ||
|
5f1b800a42 | ||
|
7f671e74b6 | ||
|
18841c41af | ||
|
827969f9a1 | ||
|
3726642211 | ||
|
0d5f6aaf3e | ||
|
4b764583d9 | ||
|
fe96ab2c35 | ||
|
e52d965d02 | ||
|
6950170cc9 | ||
|
759f7bd79e | ||
|
7416912197 | ||
|
1f3c47af2b | ||
|
de2a938ccc | ||
|
e55a0b2b77 | ||
|
0061149aa5 | ||
|
4873d8bc8c | ||
|
875df78dfe | ||
|
1607ca684b | ||
|
607fee17f9 | ||
|
9be8cab435 | ||
|
a2857d1a3e | ||
|
ee55c50a1a | ||
|
e0d47e5247 | ||
|
ee60af395a | ||
|
ce9ffce84d | ||
|
d661e3d61f | ||
|
520165115a | ||
|
1a789ab90d | ||
|
836e6d2077 | ||
|
28852c74fb | ||
|
4d49df981b | ||
|
94471b4b8e | ||
|
99217f6a3d | ||
|
df48b92732 | ||
|
c6932f8b98 | ||
|
6f688d5caf | ||
|
fafa3ad58d | ||
|
1b36959d17 | ||
|
5200ad3611 | ||
|
96983a67df | ||
|
d8c7ed8e8a | ||
|
f83eb78f80 | ||
|
d91fdfc1bd | ||
|
b0a93dcf91 | ||
|
be97ff4246 | ||
|
b250392326 | ||
|
995e4c73f7 | ||
|
91244b992b | ||
|
f137c6aa4c | ||
|
c0ee28d778 | ||
|
5928e4d28d | ||
|
cf78f90251 | ||
|
86b2b35462 | ||
|
b1bc1328eb | ||
|
88ecf73d11 | ||
|
652bfe227e | ||
|
73dd948a33 | ||
|
8e39b11cec | ||
|
9163b45525 | ||
|
526e69809c | ||
|
72e18ac7dd | ||
|
cd629fabb9 | ||
|
4fb8302cc9 | ||
|
af1bdd872c | ||
|
b333365730 | ||
|
bccd5d298f | ||
|
3d7f1ce11a | ||
|
09d2292557 | ||
|
a812bb0866 | ||
|
5f5e039a2c | ||
|
c272fa7d44 | ||
|
36ff58576b | ||
|
fc10aeba1c | ||
|
7ce2287894 | ||
|
1756a37ab2 | ||
|
57da895ae5 | ||
|
b103be7897 | ||
|
bed7a7ee77 | ||
|
78ec2a6bc6 | ||
|
a28f1bba96 | ||
|
494b93bf5c | ||
|
e2ed828f9d | ||
|
d74aa0854a | ||
|
4cdbe1783b | ||
d34d3027ee |
53 changed files with 9138 additions and 5643 deletions
|
@ -1,7 +0,0 @@
|
||||||
<!-- Please search existing issues to avoid duplicates -->
|
|
||||||
|
|
||||||
<!-- If you'd like to propose a new service provider,
|
|
||||||
please do so over at https://community.keyoxide.org -->
|
|
||||||
|
|
||||||
<!-- Feel free to remove these comments -->
|
|
||||||
|
|
16
.gitea/issue_template/bug.md
Normal file
16
.gitea/issue_template/bug.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
name: 'Bug'
|
||||||
|
about: 'Report a bug'
|
||||||
|
title: '[BUG] '
|
||||||
|
ref: 'dev'
|
||||||
|
labels:
|
||||||
|
- 'Status/Needs Triage'
|
||||||
|
- Type/Bug
|
||||||
|
---
|
||||||
|
|
||||||
|
### What happened
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Proposed solutions
|
||||||
|
|
8
.gitea/issue_template/claim_verification_bug.yml
Normal file
8
.gitea/issue_template/claim_verification_bug.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
name: Claim verification bug
|
||||||
|
about: Report a claim no longer verifying, or not verifying as it should
|
||||||
|
title: ''
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please open this issue in the [doip-js repo](https://codeberg.org/keyoxide/doipjs/issues/new/choose) instead.
|
12
.gitea/issue_template/enhancement.md
Normal file
12
.gitea/issue_template/enhancement.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
name: 'Enhancement'
|
||||||
|
about: 'Suggest a new feature or improve an existing one'
|
||||||
|
title: ''
|
||||||
|
ref: 'dev'
|
||||||
|
labels:
|
||||||
|
- 'Status/Needs Triage'
|
||||||
|
- Type/Enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
### Proposal
|
||||||
|
|
8
.gitea/issue_template/new_claim.yml
Normal file
8
.gitea/issue_template/new_claim.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
name: New claim
|
||||||
|
about: Suggest a new service provider or website for identity verification
|
||||||
|
title: ''
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please open this issue in the [doip-js repo](https://codeberg.org/keyoxide/doipjs/issues/new/choose) instead.
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -34,3 +34,12 @@ ignore
|
||||||
dist
|
dist
|
||||||
static
|
static
|
||||||
logs
|
logs
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
|
@ -18,6 +18,9 @@ steps:
|
||||||
from_secret: codeberg_password
|
from_secret: codeberg_password
|
||||||
repo: codeberg.org/keyoxide/keyoxide-web
|
repo: codeberg.org/keyoxide/keyoxide-web
|
||||||
tags: latest
|
tags: latest
|
||||||
|
build_args_from_env:
|
||||||
|
- CI_COMMIT_SHA
|
||||||
|
- CI_COMMIT_BRANCH
|
||||||
|
|
||||||
build-tag-container:
|
build-tag-container:
|
||||||
when:
|
when:
|
||||||
|
@ -32,6 +35,9 @@ steps:
|
||||||
from_secret: codeberg_password
|
from_secret: codeberg_password
|
||||||
repo: codeberg.org/keyoxide/keyoxide-web
|
repo: codeberg.org/keyoxide/keyoxide-web
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
|
build_args_from_env:
|
||||||
|
- CI_COMMIT_SHA
|
||||||
|
- CI_COMMIT_BRANCH
|
||||||
|
|
||||||
build-dev-container:
|
build-dev-container:
|
||||||
when:
|
when:
|
||||||
|
@ -46,3 +52,6 @@ steps:
|
||||||
from_secret: codeberg_password
|
from_secret: codeberg_password
|
||||||
repo: codeberg.org/keyoxide/keyoxide-web
|
repo: codeberg.org/keyoxide/keyoxide-web
|
||||||
tags: dev
|
tags: dev
|
||||||
|
build_args_from_env:
|
||||||
|
- CI_COMMIT_SHA
|
||||||
|
- CI_COMMIT_BRANCH
|
4
.yarnrc.yml
Normal file
4
.yarnrc.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
npmScopes:
|
||||||
|
myriation:
|
||||||
|
npmRegistryServer: https://git.myriation.xyz/api/packages/myriation/npm/
|
77
CHANGELOG.md
77
CHANGELOG.md
|
@ -6,6 +6,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [4.2.7] - 2024-02-01
|
||||||
|
### Added
|
||||||
|
- Server version HTTP endpoint
|
||||||
|
- Server version in footer
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.9
|
||||||
|
### Notes
|
||||||
|
- The version of keyoxide-web that the server is running can now be requested at the `/.well-known/keyoxide/version` HTTP endpoint. It is also found in the footer of every page.
|
||||||
|
- Version 1.2.9 of doipjs notably adds support for ORCiD claim verification, as well as a couple of performance improvements.
|
||||||
|
|
||||||
|
|
||||||
|
## [4.2.6] - 2024-01-24
|
||||||
|
### Added
|
||||||
|
- Support openpgp4fpr: queries in URL
|
||||||
|
- Proxy routes for openpgp and aspe
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.8
|
||||||
|
### Notes
|
||||||
|
- This version notably adds support for OpenPGP and ASPE claim verification.
|
||||||
|
|
||||||
|
## [4.2.5] - 2023-10-09
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.7
|
||||||
|
|
||||||
|
## [4.2.4] - 2023-10-09
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.6
|
||||||
|
|
||||||
|
## [4.2.3] - 2023-10-05
|
||||||
|
### Added
|
||||||
|
- Themeable profile pages
|
||||||
|
- Apps page
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.5
|
||||||
|
- Make Dicebear API domain configurable
|
||||||
|
### Fixed
|
||||||
|
- Catch errors potentially thrown by function
|
||||||
|
- Update JSON schemas
|
||||||
|
- Icon URL generation in profile view
|
||||||
|
### Notes
|
||||||
|
- ASP profiles use the Dicebear API to generate avatars. By default, Keyoxide
|
||||||
|
uses the official api.dicebear.com instance. To use a custom Dicebear instance,
|
||||||
|
set the DICEBEAR_API_HOSTNAME environment variable to its hostname.
|
||||||
|
|
||||||
|
## [4.2.2] - 2023-10-03
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.2
|
||||||
|
|
||||||
|
## [4.2.1] - 2023-09-23
|
||||||
|
### Fixed
|
||||||
|
- Tweak the rate limiter parameters
|
||||||
|
|
||||||
|
## [4.2.0] - 2023-09-23
|
||||||
|
### Added
|
||||||
|
- Profile request rate limiter (experimental; opt-in)
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.2.1
|
||||||
|
- Add logging to OpenPGP profile creation
|
||||||
|
- Add debug data to logs
|
||||||
|
### Fixed
|
||||||
|
- Make hash utils aware of ASPE
|
||||||
|
|
||||||
|
## [4.1.1] - 2023-09-21
|
||||||
|
### Changed
|
||||||
|
- Update doipjs to 1.1.0
|
||||||
|
### Fixed
|
||||||
|
- Missing rel=me for ambiguous claims
|
||||||
|
- OpenPGP cache logic
|
||||||
|
|
||||||
|
## [4.1.0] - 2023-09-18
|
||||||
|
### Changed
|
||||||
|
- Redesign
|
||||||
|
- Update doipjs to 1.0.1
|
||||||
|
- Update node to 20
|
||||||
|
- Make https scheme for proxy calls optional
|
||||||
|
- Display site version in footer
|
||||||
|
|
||||||
## [4.0.2] - 2023-09-12
|
## [4.0.2] - 2023-09-12
|
||||||
### Fixed
|
### Fixed
|
||||||
- Handle doip promise rejection
|
- Handle doip promise rejection
|
||||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -3,14 +3,20 @@ FROM node:20-alpine as builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn --pure-lockfile
|
RUN corepack enable
|
||||||
RUN yarn run build:server
|
RUN yarn install --immutable
|
||||||
RUN yarn run build:static
|
RUN yarn run build:server && yarn run build:static
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
ARG CI_COMMIT_SHA
|
||||||
|
ARG CI_COMMIT_BRANCH
|
||||||
|
|
||||||
|
ENV COMMIT_SHA=$CI_COMMIT_SHA
|
||||||
|
ENV COMMIT_BRANCH=$CI_COMMIT_BRANCH
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/package.json /app/package.json
|
COPY --from=builder /app/package.json /app/package.json
|
||||||
COPY --from=builder /app/dist /app/dist
|
COPY --from=builder /app/dist /app/dist
|
||||||
|
|
70
README.md
70
README.md
|
@ -1,15 +1,15 @@
|
||||||
# Keyoxide
|
# keyoxide-web
|
||||||
|
|
||||||
[![Drone (self-hosted) with branch](https://img.shields.io/drone/build/keyoxide/keyoxide-web/main?server=https%3A%2F%2Fdrone.keyoxide.org&style=for-the-badge)](https://drone.keyoxide.org/keyoxide/keyoxide-web)
|
[![status-badge](https://ci.codeberg.org/api/badges/5919/status.svg)](https://ci.codeberg.org/repos/5919)
|
||||||
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=for-the-badge)](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE)
|
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=flat)](https://codeberg.org/keyoxide/keyoxide-web/src/branch/main/LICENSE)
|
||||||
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
|
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
|
||||||
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
|
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
|
||||||
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=for-the-badge)](https://fosstodon.org/@keyoxide)
|
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=flat)](https://fosstodon.org/@keyoxide)
|
||||||
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=for-the-badge)](https://opencollective.com/keyoxide)
|
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
|
||||||
|
|
||||||
[Keyoxide](https://keyoxide.org) is a modern, secure and decentralized platform to prove your online identity.
|
`keyoxide-web` is the web client that powers [keyoxide.org](https://keyoxide.org) and which you can freely host on your own infrastructure.
|
||||||
|
|
||||||
## Self-hosting
|
## Hosting keyoxide-web
|
||||||
|
|
||||||
Self-hosting Keyoxide is an important aspect of the project. Users need to trust the Keyoxide instance they're using to reliably verify identities. Making Keyoxide itself decentralized means no one needs to trust a central server. If a friend or family member is hosting a Keyoxide instance, it becomes much easier to trust the instance!
|
Self-hosting Keyoxide is an important aspect of the project. Users need to trust the Keyoxide instance they're using to reliably verify identities. Making Keyoxide itself decentralized means no one needs to trust a central server. If a friend or family member is hosting a Keyoxide instance, it becomes much easier to trust the instance!
|
||||||
|
|
||||||
|
@ -23,34 +23,44 @@ docker run -d -p 3000:3000 codeberg.org/keyoxide/keyoxide-web:latest
|
||||||
|
|
||||||
Keyoxide will now be available by visiting http://localhost:3000.
|
Keyoxide will now be available by visiting http://localhost:3000.
|
||||||
|
|
||||||
More information available in the [documentation](docs.keyoxide.org/self-hosting).
|
More information available in the [documentation](https://docs.keyoxide.org/guides/self-hosting/).
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
Install `node` in one of the following ways:
|
||||||
|
|
||||||
|
- [nix](https://nixos.org/guides/install-nix.html) with [direnv](https://direnv.net/)
|
||||||
|
- using [fnm](https://github.com/Schniz/fnm)
|
||||||
|
- using [nvm](https://github.com/nvm-sh/nvm)
|
||||||
|
- directly from their [website](https://nodejs.org/)
|
||||||
|
|
||||||
|
Install dependencies with `npm install` or `yarn`.
|
||||||
|
|
||||||
|
Run the server with `npm dev` or `yarn dev`. The Keyoxide web client will now be available at [https://localhost:3000](https://localhost:3000).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Anyone can contribute if they'd like! No need to be a programmer or technically-oriented for that matter.
|
Anyone can contribute!
|
||||||
|
|
||||||
Contributing to Keyoxide can happen in many forms:
|
Developers are invited to:
|
||||||
|
|
||||||
- Finding and reporting bugs
|
- fork the repository and play around
|
||||||
- Suggesting new features
|
- submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/keyoxide-web/issues)
|
||||||
- Improving documentation
|
|
||||||
- Writing code to fix bugs and features
|
If you are new to contributing to open source software, we'd love to help you! To get started, here's a [list of "good first issues"](https://codeberg.org/keyoxide/keyoxide-web/issues?q=&type=all&state=open&labels=183598) that you could look into.
|
||||||
- Promoting decentralized identity and web3.0
|
|
||||||
|
Everyone is invited to:
|
||||||
|
|
||||||
|
- find and [report bugs](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
|
||||||
|
- suggesting [new features](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
|
||||||
|
- [help with translations](https://translate.codeberg.org/projects/keyoxide/)
|
||||||
|
- [improve documentation](https://codeberg.org/keyoxide/keyoxide-docs)
|
||||||
|
- start using open source software and promote it
|
||||||
|
|
||||||
Please note that this project has a [Code of Conduct](https://codeberg.org/keyoxide/web/src/branch/main/CODE_OF_CONDUCT.md) that all contributors agree to abide when participating.
|
Please note that this project has a [Code of Conduct](https://codeberg.org/keyoxide/web/src/branch/main/CODE_OF_CONDUCT.md) that all contributors agree to abide when participating.
|
||||||
|
|
||||||
### Local development
|
## About the Keyoxide project
|
||||||
|
|
||||||
To run Keyoxide locally on your machine for development:
|
The Keyoxide project strives for a healthier internet for all and has made its efforts fully [open source](https://codeberg.org/keyoxide). Our [community](https://docs.keyoxide.org/community/) is open and welcoming, feel free to say hi!
|
||||||
|
|
||||||
- install either
|
Funding for the project comes from the [NLnet foundation](https://nlnet.nl/), [NGI0](https://www.ngi.eu/) and the people supporting our [OpenCollective](https://opencollective.com/keyoxide). The project is grateful for all your support.
|
||||||
- NodeJS
|
|
||||||
- directly from their [website](https://nodejs.org/en/), or
|
|
||||||
- using [nvm](https://github.com/nvm-sh/nvm): `nvm install --lts; nvm use --lts`
|
|
||||||
- [yarn](https://yarnpkg.com/)
|
|
||||||
- [nix](https://nixos.org/guides/install-nix.html) with
|
|
||||||
[direnv](https://direnv.net/) will install yarn and other dependencies.
|
|
||||||
- install dependencies with `npm install` or `yarn`
|
|
||||||
- run the server with `npm dev` or `yarn dev`
|
|
||||||
|
|
||||||
Keyoxide will now be available at [https://localhost:3000](https://localhost:3000)
|
|
||||||
|
|
15
biome.json
Normal file
15
biome.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.2.2/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,9 +12,6 @@ services:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
environment:
|
environment:
|
||||||
- DOMAIN=
|
- DOMAIN=
|
||||||
# - KX_HIGHLIGHTS_1_NAME=
|
|
||||||
# - KX_HIGHLIGHTS_1_DESCRIPTION=
|
|
||||||
# - KX_HIGHLIGHTS_1_FINGERPRINT=
|
|
||||||
## The hostname to reach the doip_proxy container below
|
## The hostname to reach the doip_proxy container below
|
||||||
# - PROXY_HOSTNAME=
|
# - PROXY_HOSTNAME=
|
||||||
## The onion URL to advertise in the HTTP response headers
|
## The onion URL to advertise in the HTTP response headers
|
||||||
|
|
16
keyoxide-web.service
Normal file
16
keyoxide-web.service
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Keyoxide (Online identity verification)
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=keyoxide
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/opt/keyoxide-web/
|
||||||
|
ExecStart=/usr/bin/node /opt/keyoxide-web/dist/index.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2s
|
||||||
|
Environment=PORT=5000 DOMAIN=domain.example PROXY_HOSTNAME=domain.example
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"NODE_ENV": "development"
|
"NODE_ENV": "development",
|
||||||
|
"LOG_LEVEL": "debug"
|
||||||
},
|
},
|
||||||
"ext": "js,json,css,pug,md"
|
"ext": "js,json,css,pug,md"
|
||||||
}
|
}
|
35
package.json
35
package.json
|
@ -1,31 +1,35 @@
|
||||||
{
|
{
|
||||||
"name": "keyoxide-web",
|
"name": "keyoxide-web",
|
||||||
"version": "4.0.2",
|
"version": "4.2.7",
|
||||||
"description": "Verifying online identity with cryptography",
|
"description": "Verifying online identity with cryptography",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"packageManager": "yarn@3.6.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"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",
|
||||||
"dialog-polyfill": "^0.5.6",
|
"colorjs.io": "^0.4.5",
|
||||||
"doipjs": "^1.0.0",
|
"doipjs": "npm:@myriation/doipjs@1.2.9+myriation.1",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-http-context2": "^1.0.0",
|
||||||
|
"express-rate-limit": "^7.0.1",
|
||||||
"express-validator": "^6.13.0",
|
"express-validator": "^6.13.0",
|
||||||
"fork-awesome": "^1.2.0",
|
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"hash-wasm": "^4.9.0",
|
"hash-wasm": "^4.9.0",
|
||||||
"jstransformer-markdown-it": "^3.0.0",
|
"jstransformer-markdown-it": "^3.0.0",
|
||||||
"keyv": "^4.5.0",
|
"keyv": "^4.5.0",
|
||||||
"libravatar": "^3.0.0",
|
"libravatar": "^3.0.0",
|
||||||
|
"nanoid": "^5.0.1",
|
||||||
"openpgp": "^5.5.0",
|
"openpgp": "^5.5.0",
|
||||||
"pug": "^3.0.0",
|
"pug": "^3.0.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"string-replace-middleware": "^1.0.2",
|
"string-replace-middleware": "^1.0.2",
|
||||||
"winston": "^3.8.2"
|
"winston": "^3.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.2.2",
|
||||||
"@vercel/ncc": "^0.34.0",
|
"@vercel/ncc": "^0.34.0",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
@ -34,8 +38,7 @@
|
||||||
"license-check-and-add": "^4.0.5",
|
"license-check-and-add": "^4.0.5",
|
||||||
"mini-css-extract-plugin": "^2.5.3",
|
"mini-css-extract-plugin": "^2.5.3",
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^3.0.3",
|
||||||
"rome": "^12.1",
|
|
||||||
"sass": "^1.67.0",
|
"sass": "^1.67.0",
|
||||||
"sass-loader": "^13.3.2",
|
"sass-loader": "^13.3.2",
|
||||||
"standard": "^17.0.0",
|
"standard": "^17.0.0",
|
||||||
|
@ -48,19 +51,19 @@
|
||||||
"start": "node ./",
|
"start": "node ./",
|
||||||
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev",
|
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev",
|
||||||
"test": "yarn run lint && mocha",
|
"test": "yarn run lint && mocha",
|
||||||
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
|
"watch": "nodemon --config nodemon.json ./",
|
||||||
"build": "yarn run build:server && yarn run build:static",
|
"build": "yarn run build:server && yarn run build:static",
|
||||||
"build:server": "ncc build ./src/index.js -o dist",
|
"build:server": "ncc build ./src/index.js -o dist",
|
||||||
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production",
|
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production",
|
||||||
"build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
|
"build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
|
||||||
"lint": "yarn run standard:check && yarn run rome:check",
|
"lint": "yarn run standard:check && yarn run biome:check",
|
||||||
"standard:check": "./node_modules/.bin/standard ./src",
|
"biome:check": "biome check ./src && biome lint ./src",
|
||||||
"standard:fix": "./node_modules/.bin/standard --fix ./src",
|
"biome:fix": "biome check --apply ./src && biome lint --apply ./src",
|
||||||
"rome:check": "./node_modules/.bin/rome check ./src",
|
"standard:check": "standard ./src",
|
||||||
"rome:fix": "./node_modules/.bin/rome check --apply ./src",
|
"standard:fix": "standard --fix ./src",
|
||||||
"license:check": "./node_modules/.bin/license-check-and-add check",
|
"license:check": "license-check-and-add check",
|
||||||
"license:add": "./node_modules/.bin/license-check-and-add add",
|
"license:add": "license-check-and-add add",
|
||||||
"license:remove": "./node_modules/.bin/license-check-and-add remove"
|
"license:remove": "license-check-and-add remove"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -128,7 +128,13 @@ router.get('/fetch',
|
||||||
data = await doVerification(data)
|
data = await doVerification(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
data = data.toJSON()
|
data = data.toJSON()
|
||||||
|
} catch (error) {
|
||||||
|
data = {
|
||||||
|
errors: [error.message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate JSON
|
// Validate JSON
|
||||||
|
@ -162,7 +168,13 @@ router.get('/verify',
|
||||||
// Do verification
|
// Do verification
|
||||||
let data = await doVerification(profile)
|
let data = await doVerification(profile)
|
||||||
|
|
||||||
|
try {
|
||||||
data = data.toJSON()
|
data = data.toJSON()
|
||||||
|
} catch (error) {
|
||||||
|
data = {
|
||||||
|
errors: [error.message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate JSON
|
// Validate JSON
|
||||||
|
|
|
@ -42,7 +42,12 @@ const opts = {
|
||||||
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
|
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
|
||||||
},
|
},
|
||||||
irc: {
|
irc: {
|
||||||
nick: process.env.IRC_NICK || null
|
nick: process.env.IRC_NICK || null,
|
||||||
|
sasl: Object.keys(process.env).filter(k => k.startsWith('IRC_SASL_USERNAME_')).map(k => ({
|
||||||
|
username: process.env[k],
|
||||||
|
password: process.env[`IRC_SASL_PASSWORD_${k.substring('IRC_SASL_USERNAME_'.length)}`],
|
||||||
|
domainRegex: process.env[`IRC_SASL_DOMAIN_REGEX_${k.substring('IRC_SASL_USERNAME_'.length)}`]
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
matrix: {
|
matrix: {
|
||||||
instance: process.env.MATRIX_INSTANCE || null,
|
instance: process.env.MATRIX_INSTANCE || null,
|
||||||
|
@ -92,6 +97,40 @@ router.get(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ASPE route
|
||||||
|
router.get('/aspe', query('aspeUri').isString(), (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher.aspe
|
||||||
|
.fn(req.query, opts)
|
||||||
|
.then((data) => {
|
||||||
|
return res.status(200).send(data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// OpenPGP route
|
||||||
|
router.get('/openpgp', query('url').isURL(), query('protocol').isString(), (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher.openpgp
|
||||||
|
.fn(req.query, opts)
|
||||||
|
.then((data) => {
|
||||||
|
return res.status(200).send(data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// DNS route
|
// DNS route
|
||||||
router.get('/dns', query('domain').isFQDN(), (req, res) => {
|
router.get('/dns', query('domain').isFQDN(), (req, res) => {
|
||||||
const errors = validationResult(req)
|
const errors = validationResult(req)
|
||||||
|
|
28
src/index.js
28
src/index.js
|
@ -28,7 +28,10 @@ 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 express from 'express'
|
import express from 'express'
|
||||||
|
import * as httpContext from 'express-http-context2'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
import { stringReplace } from 'string-replace-middleware'
|
import { stringReplace } from 'string-replace-middleware'
|
||||||
import * as pug from 'pug'
|
import * as pug from 'pug'
|
||||||
import * as dotenv from 'dotenv'
|
import * as dotenv from 'dotenv'
|
||||||
|
@ -41,6 +44,20 @@ import staticRoute from './routes/static.js'
|
||||||
import utilRoute from './routes/util.js'
|
import utilRoute from './routes/util.js'
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
// Get information about the last git commit
|
||||||
|
let gitBranch = process.env.CI_COMMIT_BRANCH ?? process.env.COMMIT_BRANCH
|
||||||
|
if (!gitBranch) {
|
||||||
|
try {
|
||||||
|
gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
let gitHash = process.env.CI_COMMIT_SHA ?? process.env.COMMIT_SHA
|
||||||
|
if (!gitHash) {
|
||||||
|
try {
|
||||||
|
gitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const packageData = JSON.parse(readFileSync('./package.json'))
|
const packageData = JSON.parse(readFileSync('./package.json'))
|
||||||
|
|
||||||
|
@ -49,12 +66,23 @@ app.engine('pug', pug.__express).set('view engine', 'pug')
|
||||||
app.set('port', process.env.PORT || 3000)
|
app.set('port', process.env.PORT || 3000)
|
||||||
app.set('domain', process.env.DOMAIN)
|
app.set('domain', process.env.DOMAIN)
|
||||||
app.set('scheme', process.env.SCHEME || 'https')
|
app.set('scheme', process.env.SCHEME || 'https')
|
||||||
|
app.set('keyoxide_name', 'keyoxide-web')
|
||||||
app.set('keyoxide_version', packageData.version)
|
app.set('keyoxide_version', packageData.version)
|
||||||
|
app.set('git_branch', gitBranch)
|
||||||
|
app.set('git_hash', gitHash)
|
||||||
app.set('onion_url', process.env.ONION_URL)
|
app.set('onion_url', process.env.ONION_URL)
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
|
app.use(httpContext.middleware)
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.setHeader('Permissions-Policy', 'interest-cohort=()')
|
res.setHeader('Permissions-Policy', 'interest-cohort=()')
|
||||||
|
httpContext.set('requestId', nanoid())
|
||||||
|
httpContext.set('requestPath', req.path)
|
||||||
|
httpContext.set('requestIp', req.ip)
|
||||||
|
|
||||||
|
logger.info('Handle a request',
|
||||||
|
{ component: 'http_server', action: 'request' })
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
11
src/log.js
11
src/log.js
|
@ -28,6 +28,7 @@ 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 { createLogger, format, transports } from 'winston'
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
import * as httpContext from 'express-http-context2'
|
||||||
import * as dotenv from 'dotenv'
|
import * as dotenv from 'dotenv'
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
@ -37,13 +38,23 @@ const anonymize = format((info, opts) => {
|
||||||
info.keyserver_domain = undefined
|
info.keyserver_domain = undefined
|
||||||
info.username = undefined
|
info.username = undefined
|
||||||
info.fingerprint = undefined
|
info.fingerprint = undefined
|
||||||
|
info.request_path = undefined
|
||||||
|
info.request_ip = undefined
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const addRequestData = format((info, opts) => {
|
||||||
|
if (httpContext.get('requestId')) info.request_id = httpContext.get('requestId')
|
||||||
|
if (httpContext.get('requestPath')) info.request_path = httpContext.get('requestPath')
|
||||||
|
if (httpContext.get('requestIp')) info.request_ip = httpContext.get('requestIp')
|
||||||
|
return info
|
||||||
|
})
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
format: format.combine(
|
format: format.combine(
|
||||||
|
addRequestData(),
|
||||||
anonymize(),
|
anonymize(),
|
||||||
format.timestamp(),
|
format.timestamp(),
|
||||||
format.json()
|
format.json()
|
||||||
|
|
|
@ -36,19 +36,11 @@ const router = express.Router()
|
||||||
const md = markdownImport({ typographer: true })
|
const md = markdownImport({ typographer: true })
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const highlights = []
|
res.render('index', { meta: getMetaFromReq(req) })
|
||||||
for (let index = 1; index < 4; index++) {
|
|
||||||
if (process.env[`KX_HIGHLIGHTS_${index}_NAME`] &&
|
|
||||||
process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) {
|
|
||||||
highlights.push({
|
|
||||||
name: process.env[`KX_HIGHLIGHTS_${index}_NAME`],
|
|
||||||
description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`],
|
|
||||||
fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('index', { highlights, meta: getMetaFromReq(req) })
|
router.get('/apps', (req, res) => {
|
||||||
|
res.render('apps', { title: 'Apps', meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/privacy', (req, res) => {
|
router.get('/privacy', (req, res) => {
|
||||||
|
@ -75,6 +67,12 @@ router.get('/.well-known/webfinger', (req, res) => {
|
||||||
res.json(body)
|
res.json(body)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.get('/.well-known/keyoxide/version', async (req, res) => {
|
||||||
|
// TODO Support responding with JSON object when requested
|
||||||
|
const meta = getMetaFromReq(req)
|
||||||
|
return res.status(200).contentType('text/plain').send(meta.keyoxide.semver)
|
||||||
|
})
|
||||||
|
|
||||||
router.get('/users/keyoxide', (req, res) => {
|
router.get('/users/keyoxide', (req, res) => {
|
||||||
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
||||||
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')
|
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')
|
||||||
|
|
|
@ -29,18 +29,47 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
*/
|
*/
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import bodyParserImport from 'body-parser'
|
import bodyParserImport from 'body-parser'
|
||||||
|
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, escapedParam } from '../server/utils.js'
|
||||||
|
import logger from '../log.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
||||||
|
|
||||||
router.get('/sig', (req, res) => {
|
let profileRateLimiter = (req, res, next) => {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.ENABLE_EXPERIMENTAL_RATE_LIMITER) {
|
||||||
|
profileRateLimiter = rateLimit({
|
||||||
|
windowMs: 5000,
|
||||||
|
limit: 20,
|
||||||
|
standardHeaders: 'draft-7',
|
||||||
|
legacyHeaders: false,
|
||||||
|
handler: (req, res, next, options) => {
|
||||||
|
logger.debug('Rate-limiting a profile request',
|
||||||
|
{ component: 'profile_rate_limiter', action: 'block' })
|
||||||
|
|
||||||
|
res.status(options.statusCode).render('429', { meta: getMetaFromReq(req) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug('Starting the profile request rate limiter',
|
||||||
|
{ component: 'profile_rate_limiter', action: 'start' })
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/sig',
|
||||||
|
profileRateLimiter,
|
||||||
|
(req, res) => {
|
||||||
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
|
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/sig', bodyParser, async (req, res) => {
|
router.post('/sig',
|
||||||
|
profileRateLimiter,
|
||||||
|
bodyParser,
|
||||||
|
async (req, res) => {
|
||||||
const data = await generateSignatureProfile(req.body.signature)
|
const data = await generateSignatureProfile(req.body.signature)
|
||||||
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)
|
||||||
|
@ -55,7 +84,10 @@ router.post('/sig', bodyParser, async (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/wkd/:id', async (req, res) => {
|
router.get('/wkd/:id',
|
||||||
|
profileRateLimiter,
|
||||||
|
escapedParam('id'),
|
||||||
|
async (req, res) => {
|
||||||
const data = await generateWKDProfile(req.params.id)
|
const data = await generateWKDProfile(req.params.id)
|
||||||
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)
|
||||||
|
@ -68,7 +100,10 @@ router.get('/wkd/:id', async (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/hkp/:id', async (req, res) => {
|
router.get('/hkp/:id',
|
||||||
|
profileRateLimiter,
|
||||||
|
escapedParam('id'),
|
||||||
|
async (req, res) => {
|
||||||
const data = await generateHKPProfile(req.params.id)
|
const data = await generateHKPProfile(req.params.id)
|
||||||
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)
|
||||||
|
@ -81,7 +116,11 @@ router.get('/hkp/:id', async (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/hkp/:server/:id', async (req, res) => {
|
router.get('/hkp/:server/:id',
|
||||||
|
profileRateLimiter,
|
||||||
|
escapedParam('server'),
|
||||||
|
escapedParam('id'),
|
||||||
|
async (req, res) => {
|
||||||
const data = await generateHKPProfile(req.params.id, req.params.server)
|
const data = await generateHKPProfile(req.params.id, req.params.server)
|
||||||
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)
|
||||||
|
@ -94,7 +133,11 @@ router.get('/hkp/:server/:id', async (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/keybase/:username/:fingerprint', async (req, res) => {
|
router.get('/keybase/:username/:fingerprint',
|
||||||
|
profileRateLimiter,
|
||||||
|
escapedParam('username'),
|
||||||
|
escapedParam('fingerprint'),
|
||||||
|
async (req, res) => {
|
||||||
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
|
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
|
||||||
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)
|
||||||
|
@ -107,8 +150,12 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/:id', async (req, res) => {
|
router.get('/:id',
|
||||||
|
profileRateLimiter,
|
||||||
|
escapedParam('id'),
|
||||||
|
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', {
|
||||||
|
@ -116,6 +163,7 @@ router.get('/:id', 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,7 +28,7 @@ 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 express from 'express'
|
import express from 'express'
|
||||||
import { getMetaFromReq } from '../server/utils.js'
|
import { escapedParam, getMetaFromReq } from '../server/utils.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
@ -38,42 +38,54 @@ router.get('/', function (req, res) {
|
||||||
router.get('/profile-url', function (req, res) {
|
router.get('/profile-url', function (req, res) {
|
||||||
res.render('util/profile-url', { meta: getMetaFromReq(req) })
|
res.render('util/profile-url', { meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
router.get('/profile-url/:input', function (req, res) {
|
router.get('/profile-url/:input',
|
||||||
|
escapedParam('input'),
|
||||||
|
function (req, res) {
|
||||||
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
|
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/qr', function (req, res) {
|
router.get('/qr', function (req, res) {
|
||||||
res.render('util/qr', { meta: getMetaFromReq(req) })
|
res.render('util/qr', { meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
router.get('/qr/:input', function (req, res) {
|
router.get('/qr/:input',
|
||||||
|
escapedParam('input'),
|
||||||
|
function (req, res) {
|
||||||
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
|
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/qrfp', function (req, res) {
|
router.get('/qrfp', function (req, res) {
|
||||||
res.render('util/qrfp', { meta: getMetaFromReq(req) })
|
res.render('util/qrfp', { meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
router.get('/qrfp/:input', function (req, res) {
|
router.get('/qrfp/:input',
|
||||||
|
escapedParam('input'),
|
||||||
|
function (req, res) {
|
||||||
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
|
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/wkd', function (req, res) {
|
router.get('/wkd', function (req, res) {
|
||||||
res.render('util/wkd', { meta: getMetaFromReq(req) })
|
res.render('util/wkd', { meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
router.get('/wkd/:input', function (req, res) {
|
router.get('/wkd/:input',
|
||||||
|
escapedParam('input'),
|
||||||
|
function (req, res) {
|
||||||
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
|
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/argon2', function (req, res) {
|
router.get('/argon2', function (req, res) {
|
||||||
res.render('util/argon2', { meta: getMetaFromReq(req) })
|
res.render('util/argon2', { meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
router.get('/argon2/:input', function (req, res) {
|
router.get('/argon2/:input',
|
||||||
|
escapedParam('input'),
|
||||||
|
function (req, res) {
|
||||||
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
|
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/bcrypt', function (req, res) {
|
router.get('/bcrypt', function (req, res) {
|
||||||
res.render('util/bcrypt', { meta: getMetaFromReq(req) })
|
res.render('util/bcrypt', { meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
router.get('/bcrypt/:input', function (req, res) {
|
router.get('/bcrypt/:input',
|
||||||
|
escapedParam('input'),
|
||||||
|
function (req, res) {
|
||||||
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
|
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,10 @@ export const personaSchema = {
|
||||||
description: 'URL to an avatar image',
|
description: 'URL to an avatar image',
|
||||||
type: ['string', 'null']
|
type: ['string', 'null']
|
||||||
},
|
},
|
||||||
|
themeColor: {
|
||||||
|
description: 'Profile page theme color',
|
||||||
|
type: ['string', 'null']
|
||||||
|
},
|
||||||
isRevoked: {
|
isRevoked: {
|
||||||
type: 'boolean'
|
type: 'boolean'
|
||||||
},
|
},
|
||||||
|
@ -215,17 +219,25 @@ export const claimSchema = {
|
||||||
display: {
|
display: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
profileName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Account name to display in the user interface'
|
description: 'Account name to display in the user interface'
|
||||||
},
|
},
|
||||||
url: {
|
profileUrl: {
|
||||||
type: ['string', 'null'],
|
type: ['string', 'null'],
|
||||||
description: 'URL to link to in the user interface'
|
description: 'Profile URL to link to in the user interface'
|
||||||
|
},
|
||||||
|
proofUrl: {
|
||||||
|
type: ['string', 'null'],
|
||||||
|
description: 'Proof URL to link to in the user interface'
|
||||||
},
|
},
|
||||||
serviceProviderName: {
|
serviceProviderName: {
|
||||||
type: ['string', 'null'],
|
type: ['string', 'null'],
|
||||||
description: 'Name of the service provider to display in the user interface'
|
description: 'Name of the service provider to display in the user interface'
|
||||||
|
},
|
||||||
|
serviceProviderId: {
|
||||||
|
type: ['string', 'null'],
|
||||||
|
description: 'Id of the service provider'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,9 @@ const generateAspeProfile = async (id) => {
|
||||||
|
|
||||||
return doipjs.asp.fetchASPE(id)
|
return doipjs.asp.fetchASPE(id)
|
||||||
.then(profile => {
|
.then(profile => {
|
||||||
|
if (process.env.DOMAIN) {
|
||||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
|
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
|
||||||
|
}
|
||||||
profile = processAspProfile(profile)
|
profile = processAspProfile(profile)
|
||||||
return profile
|
return profile
|
||||||
})
|
})
|
||||||
|
@ -58,7 +60,9 @@ const generateWKDProfile = async (id) => {
|
||||||
|
|
||||||
return fetchWKD(id)
|
return fetchWKD(id)
|
||||||
.then(async profile => {
|
.then(async profile => {
|
||||||
|
if (process.env.DOMAIN) {
|
||||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
|
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
|
||||||
|
}
|
||||||
profile = processOpenPgpProfile(profile)
|
profile = processOpenPgpProfile(profile)
|
||||||
|
|
||||||
logger.debug('Generating a WKD profile',
|
logger.debug('Generating a WKD profile',
|
||||||
|
@ -89,7 +93,9 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
||||||
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.DOMAIN) {
|
||||||
profile.addVerifier('keyoxide', keyoxideUrl)
|
profile.addVerifier('keyoxide', keyoxideUrl)
|
||||||
|
}
|
||||||
profile = processOpenPgpProfile(profile)
|
profile = processOpenPgpProfile(profile)
|
||||||
|
|
||||||
logger.debug('Generating a HKP profile',
|
logger.debug('Generating a HKP profile',
|
||||||
|
@ -111,6 +117,7 @@ const generateAutoProfile = async (id) => {
|
||||||
let result
|
let result
|
||||||
|
|
||||||
const aspeRe = /aspe:(.*):(.*)/
|
const aspeRe = /aspe:(.*):(.*)/
|
||||||
|
const openpgpRe = /openpgp4fpr:(.*)/
|
||||||
|
|
||||||
if (aspeRe.test(id)) {
|
if (aspeRe.test(id)) {
|
||||||
result = await generateAspeProfile(id)
|
result = await generateAspeProfile(id)
|
||||||
|
@ -120,6 +127,15 @@ const generateAutoProfile = async (id) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (openpgpRe.test(id)) {
|
||||||
|
const match = id.match(openpgpRe)
|
||||||
|
result = await generateHKPProfile(match[1])
|
||||||
|
|
||||||
|
if (result && !('errors' in result)) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (id.includes('@')) {
|
if (id.includes('@')) {
|
||||||
result = await generateWKDProfile(id)
|
result = await generateWKDProfile(id)
|
||||||
|
|
||||||
|
@ -168,7 +184,9 @@ const generateKeybaseProfile = async (username, fingerprint) => {
|
||||||
|
|
||||||
return fetchKeybase(username, fingerprint)
|
return fetchKeybase(username, fingerprint)
|
||||||
.then(async profile => {
|
.then(async profile => {
|
||||||
|
if (process.env.DOMAIN) {
|
||||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
|
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
|
||||||
|
}
|
||||||
profile = processOpenPgpProfile(profile)
|
profile = processOpenPgpProfile(profile)
|
||||||
|
|
||||||
logger.debug('Generating a Keybase profile',
|
logger.debug('Generating a Keybase profile',
|
||||||
|
@ -215,7 +233,8 @@ const processAspProfile = async (/** @type {import('doipjs').Profile */ profile)
|
||||||
|
|
||||||
// Overwrite avatarUrl
|
// Overwrite avatarUrl
|
||||||
// TODO: don't overwrite avatarUrl once it's fully supported
|
// TODO: don't overwrite avatarUrl once it's fully supported
|
||||||
profile.personas[profile.primaryPersonaIndex].avatarUrl = `https://api.dicebear.com/6.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
|
profile.personas[profile.primaryPersonaIndex].avatarUrl =
|
||||||
|
`https://${process.env.DICEBEAR_API_HOSTNAME || 'api.dicebear.com'}/7.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
|
||||||
|
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ 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/>.
|
||||||
*/
|
*/
|
||||||
|
import logger from '../log.js'
|
||||||
import got from 'got'
|
import got from 'got'
|
||||||
import * as doipjs from 'doipjs'
|
import * as doipjs from 'doipjs'
|
||||||
import { readKey } from 'openpgp'
|
import { readKey } from 'openpgp'
|
||||||
|
@ -34,11 +35,20 @@ import { computeWKDLocalPart } from './utils.js'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import Keyv from 'keyv'
|
import Keyv from 'keyv'
|
||||||
|
|
||||||
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
|
let c = null
|
||||||
|
if (process.env.ENABLE_EXPERIMENTAL_CACHE) {
|
||||||
|
c = new Keyv()
|
||||||
|
|
||||||
|
logger.debug('OpenPGP cache started',
|
||||||
|
{ component: 'openpgp_cache', action: 'start' })
|
||||||
|
}
|
||||||
|
|
||||||
const fetchWKD = (id) => {
|
const fetchWKD = (id) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
logger.debug('Fetching an OpenPGP profile via WKD',
|
||||||
|
{ component: 'wkd_profile_fetcher', action: 'start', profile_id: id })
|
||||||
|
|
||||||
let publicKey = null
|
let publicKey = null
|
||||||
let profile = null
|
let profile = null
|
||||||
let fetchURL = null
|
let fetchURL = null
|
||||||
|
@ -58,7 +68,12 @@ const fetchWKD = (id) => {
|
||||||
|
|
||||||
const hash = createHash('md5').update(id).digest('hex')
|
const hash = createHash('md5').update(id).digest('hex')
|
||||||
if (c && await c.get(hash)) {
|
if (c && await c.get(hash)) {
|
||||||
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
|
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
|
||||||
|
|
||||||
|
logger.debug('WKD profile retrieved from OpenPGP cache',
|
||||||
|
{ component: 'openpgp_cache', action: 'retrieve_wkd' })
|
||||||
|
|
||||||
|
return resolve(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
|
@ -71,7 +86,10 @@ const fetchWKD = (id) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (errorAdvanced) {
|
||||||
|
logger.debug('Failed to fetch an OpenPGP profile via WKD (advanced URL)',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorAdvanced.message })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
plaintext = await got(urlDirect).then((response) => {
|
plaintext = await got(urlDirect).then((response) => {
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
|
@ -81,7 +99,10 @@ const fetchWKD = (id) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (errorDirect) {
|
||||||
|
logger.debug('Failed to fetch an OpenPGP profile via WKD (direct URL)',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorDirect.message })
|
||||||
|
|
||||||
return reject(new Error('No public keys could be fetched using WKD'))
|
return reject(new Error('No public keys could be fetched using WKD'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,6 +116,9 @@ const fetchWKD = (id) => {
|
||||||
binaryKey: plaintext
|
binaryKey: plaintext
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.debug('Failed to fetch an OpenPGP profile via WKD (reading key)',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
|
||||||
|
|
||||||
return reject(new Error('No public keys could be read from the data fetched using WKD'))
|
return reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +129,9 @@ const fetchWKD = (id) => {
|
||||||
try {
|
try {
|
||||||
profile = await doipjs.openpgp.parsePublicKey(publicKey)
|
profile = await doipjs.openpgp.parsePublicKey(publicKey)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.debug('Failed to fetch an OpenPGP profile via WKD (parsing key)',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
|
||||||
|
|
||||||
return reject(new Error('No public keys could be fetched using WKD'))
|
return reject(new Error('No public keys could be fetched using WKD'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,8 +142,14 @@ const fetchWKD = (id) => {
|
||||||
|
|
||||||
if (c && plaintext instanceof Uint8Array) {
|
if (c && plaintext instanceof Uint8Array) {
|
||||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||||
|
|
||||||
|
logger.debug('WKD profile stored in OpenPGP cache',
|
||||||
|
{ component: 'openpgp_cache', action: 'store_wkd' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug('Fetched an OpenPGP profile via WKD',
|
||||||
|
{ component: 'wkd_profile_fetcher', action: 'done', profile_id: id })
|
||||||
|
|
||||||
resolve(profile)
|
resolve(profile)
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
@ -125,6 +158,9 @@ const fetchWKD = (id) => {
|
||||||
const fetchHKP = (id, keyserverDomain) => {
|
const fetchHKP = (id, keyserverDomain) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
logger.debug('Fetching an OpenPGP profile via HKP',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||||
|
|
||||||
let profile = null
|
let profile = null
|
||||||
let fetchURL = null
|
let fetchURL = null
|
||||||
|
|
||||||
|
@ -147,13 +183,21 @@ const fetchHKP = (id, keyserverDomain) => {
|
||||||
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
|
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
|
||||||
|
|
||||||
if (c && await c.get(hash)) {
|
if (c && await c.get(hash)) {
|
||||||
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
|
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
|
||||||
|
|
||||||
|
logger.debug('HKP profile retrieved from OpenPGP cache',
|
||||||
|
{ component: 'openpgp_cache', action: 'store_hkp' })
|
||||||
|
|
||||||
|
return resolve(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
try {
|
try {
|
||||||
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
|
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.debug('Failed to fetch an OpenPGP profile via HKP',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, keyserver_domain: keyserverDomain || '', error: error.message })
|
||||||
|
|
||||||
profile = null
|
profile = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,8 +212,14 @@ const fetchHKP = (id, keyserverDomain) => {
|
||||||
|
|
||||||
if (c && profile instanceof doipjs.Profile) {
|
if (c && profile instanceof doipjs.Profile) {
|
||||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||||
|
|
||||||
|
logger.debug('HKP profile stored in OpenPGP cache',
|
||||||
|
{ component: 'openpgp_cache', action: 'store_hkp' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug('Fetched an OpenPGP profile via HKP',
|
||||||
|
{ component: 'hkp_profile_fetcher', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||||
|
|
||||||
resolve(profile)
|
resolve(profile)
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,6 +28,9 @@ 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'
|
||||||
|
import { param } from 'express-validator'
|
||||||
|
|
||||||
export async function computeWKDLocalPart (localPart) {
|
export async function computeWKDLocalPart (localPart) {
|
||||||
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
|
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
|
||||||
|
@ -80,10 +83,96 @@ export function encodeZBase32 (data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetaFromReq (req) {
|
export function getMetaFromReq (req) {
|
||||||
|
const versionDetails = (req.app.get('git_hash'))
|
||||||
|
? `+${req.app.get('git_hash').substring(0, 10)}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const semver = `${req.app.get('keyoxide_name')}/${req.app.get('keyoxide_version')}${versionDetails}`
|
||||||
|
|
||||||
|
const sourceUrl = req.app.get('git_hash')
|
||||||
|
? `https://codeberg.org/keyoxide/keyoxide-web/src/commit/${req.app.get('git_hash')}`
|
||||||
|
: 'https://codeberg.org/keyoxide/keyoxide-web'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
env: req.app.get('env'),
|
env: req.app.get('env'),
|
||||||
keyoxide: {
|
keyoxide: {
|
||||||
version: req.app.get('keyoxide_version')
|
name: req.app.get('keyoxide_name'),
|
||||||
|
version: req.app.get('keyoxide_version'),
|
||||||
|
branch: req.app.get('git_branch'),
|
||||||
|
hash: req.app.get('git_hash'),
|
||||||
|
semver,
|
||||||
|
sourceUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reEmailLike = /(<[^\s@<>]+@[^\s@<>]+>)/
|
||||||
|
|
||||||
|
export function escapedParam (/** @type {String} */ name) {
|
||||||
|
return param(name).customSanitizer(value => {
|
||||||
|
return value.split(reEmailLike).map(token => {
|
||||||
|
if (reEmailLike.test(token)) return token
|
||||||
|
return escapeString(token)
|
||||||
|
}).join('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from https://github.com/validatorjs/validator.js/blob/b958bd7d1026a434ad3bf90064d3dcb8b775f1a9/src/lib/escapeString.js
|
||||||
|
function escapeString (/** @type {String} */ input) {
|
||||||
|
return (input.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/\//g, '/')
|
||||||
|
.replace(/\\/g, '\')
|
||||||
|
.replace(/`/g, '`'))
|
||||||
|
}
|
||||||
|
|
7
static-src/files/img/keyoxide.svg
Normal file
7
static-src/files/img/keyoxide.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<svg role="image" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m14.662 2.7734c-0.86914 8.5e-6 -1.5742 0.70509-1.5742 1.5742 0 0.52528 0.26095 1.006 0.67773 1.2949 0.12007 0.083138 0.18912 0.092562 0.20508 0.22266 0.01604 0.13001-0.16211 0.25-0.16211 0.25l-3.3242 2.5859v-0.5293c0.14724-1.4561-0.83223-2.9405-2.2754-3.2793-1.719-0.502-3.7931 1.0298-3.7931 2.6452 0 1.9182 0.00778 8.9247 0.00211 13.429-0.00211 1.6772 1.3577 3.0332 3.0332 3.0332s3.0352-1.3576 3.0352-3.0332v-0.39648c1.365 0.90898 2.7303 1.8174 4.0938 2.7285 1.1299 0.93021 2.9078 0.93859 3.9902-0.07422 1.3712-1.1518 1.323-3.4999-0.0918-4.5957-1.935-1.3219-3.8897-2.6181-5.8418-3.916l5.7734-4.4883c1.1632-0.90466 1.4954-2.5005 0.83398-3.7832-0.04938-0.095755-0.06511-0.12998-0.16797-0.16992-0.10277-0.039938-0.18425 0.00439-0.28711 0.042969-0.14138 0.053062-0.29257 0.080078-0.44531 0.080078-0.69894 0-1.2656-0.56662-1.2656-1.2656 9.4e-5 -0.094853 0.01636-0.14996-0.01172-0.20898-0.02807-0.059023-0.10233-0.097092-0.17578-0.10547-0.12191-0.013926-0.24437-0.020476-0.36719-0.019531-0.12049 8.587e-4 -0.21169-0.00952-0.26367-0.097656-0.05189-0.088142-0.02344-0.18975-0.02344-0.34961 0-0.86915-0.70504-1.5742-1.5742-1.5742z"/>
|
||||||
|
<path d="m12.806 3.085a1.0735 1.0735 0 0 1-1.0735 1.0735 1.0735 1.0735 0 0 1-1.0735-1.0735 1.0735 1.0735 0 0 1 1.0735-1.0735 1.0735 1.0735 0 0 1 1.0735 1.0735z"/>
|
||||||
|
<path d="m13.458 1.0033a0.70038 0.70038 0 0 1-0.70038 0.70038 0.70038 0.70038 0 0 1-0.70038-0.70038 0.70038 0.70038 0 0 1 0.70038-0.70038 0.70038 0.70038 0 0 1 0.70038 0.70038z"/>
|
||||||
|
<path d="m11.339 0.48902a0.48902 0.48902 0 0 1-0.48902 0.48902 0.48902 0.48902 0 0 1-0.48902-0.48902 0.48902 0.48902 0 0 1 0.48902-0.48902 0.48902 0.48902 0 0 1 0.48902 0.48902z"/>
|
||||||
|
<path d="m19.203 5.1296a0.85797 0.85797 0 0 1-0.85797 0.85797 0.85797 0.85797 0 0 1-0.85797-0.85797 0.85797 0.85797 0 0 1 0.85797-0.85797 0.85797 0.85797 0 0 1 0.85797 0.85797z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static-src/files/img/keyoxide_asp_web_home.jpg
Normal file
BIN
static-src/files/img/keyoxide_asp_web_home.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
BIN
static-src/files/img/keyoxide_mobile_dark_home.jpg
Normal file
BIN
static-src/files/img/keyoxide_mobile_dark_home.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
BIN
static-src/files/img/keyoxide_mobile_dark_profile.jpg
Normal file
BIN
static-src/files/img/keyoxide_mobile_dark_profile.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
static-src/files/img/keyoxide_mobile_light_home.jpg
Normal file
BIN
static-src/files/img/keyoxide_mobile_light_home.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
3
static-src/files/img/keyoxide_outline.svg
Normal file
3
static-src/files/img/keyoxide_outline.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m10.89 0c-0.5512 2.702e-7 -1.008 0.4571-1.008 1.008 0 0.5512 0.4571 1.008 1.008 1.008 0.2431 0 0.4661-0.0887 0.6414-0.2354 0.01238 0.05092 0.0265 0.1007 0.04512 0.1491-0.7892 0.07774-1.414 0.7551-1.414 1.563 0 0.8594 0.706 1.563 1.565 1.563 0.2774 0 0.5392-0.07366 0.767-0.202 0.05714 0.4898 0.3658 0.8914 0.716 1.232l-2.219 1.72c-0.1251-1.779-1.54-3.225-3.35-3.225-1.891 0-3.441 1.542-3.441 3.433v6.724c-3.902e-4 -0.0156 3.8e-6 -0.026 0 0.02942v5.791c0 1.893 1.548 3.441 3.441 3.441 1.699 0 2.96-1.32 3.233-2.944l3.545 2.366c1.574 1.049 3.718 0.6164 4.767-0.9573 1.049-1.574 0.6242-3.718-0.9494-4.767l-4.716-3.137 4.916-3.825c1.293-1.006 1.683-2.776 0.9828-4.217 0.2337-0.2976 0.3747-0.6704 0.3747-1.075 0-0.9606-0.7891-1.75-1.75-1.75-0.6534 1e-7 -1.228 0.3646-1.528 0.9004-0.01712-6.411e-4 -0.03382-0.00323-0.051-0.00391-0.08719-1.045-0.87-1.942-1.936-1.942-0.4834 4.6e-6 -0.9293 0.1707-1.281 0.4551-0.04123-0.1579-0.1067-0.3062-0.1922-0.4414 0.4963-0.1534 0.8651-0.622 0.8651-1.163 0-0.6626-0.5516-1.208-1.214-1.208-0.3405 0-0.6503 0.1447-0.8709 0.3747-0.1427-0.3849-0.5149-0.665-0.9455-0.665zm0 0.9357c0.03545 0 0.07258 0.03713 0.07258 0.07258 0 0.03545-0.03712 0.07454-0.07258 0.07454-0.03545 0-0.08239-0.03909-0.08239-0.07454 0-0.03545 0.04694-0.07258 0.08239-0.07258zm1.816 0.4374c0.076 0 0.1255 0.04954 0.1255 0.1255 0 0.076-0.04954 0.1255-0.1255 0.1255-0.076 0-0.1275-0.04954-0.1275-0.1255 0-0.076 0.05151-0.1255 0.1275-0.1255zm-0.9788 1.638c0.2727 1e-7 0.4806 0.2098 0.4806 0.4826 0 0.2727-0.2078 0.4806-0.4806 0.4806-0.2727 0-0.4826-0.2078-0.4826-0.4806 0-0.2727 0.2098-0.4826 0.4826-0.4826zm2.801 0.7258c0.5365-1.55e-5 0.9573 0.4208 0.9573 0.9573 0 0.0449-0.0071 0.09728-0.0079 0.1922-7.64e-4 0.09495 0.01177 0.2592 0.104 0.4159 0.09067 0.1538 0.2755 0.2826 0.4159 0.3256 0.1403 0.04299 0.2351 0.03781 0.3099 0.03728 0.1106 0.8569 0.8508 1.528 1.736 1.528 0.1167 3e-7 0.2308-0.01313 0.3413-0.0353 0.3731 0.951 0.2233 2.066-0.6159 2.719l-5.508 4.284a0.5428 0.5428 0 0 0 0.02942 0.8827l5.337 3.558c1.085 0.7232 1.376 2.177 0.6532 3.262-0.7232 1.085-2.177 1.376-3.262 0.6532l-4.182-2.787a0.5428 0.5428 0 0 0-0.8376 0.4512v0.3786c0 1.306-1.051 2.358-2.358 2.358-1.306 0-2.35-1.051-2.35-2.358v-5.791c-3e-6 0.04601-8.017e-4 0.04336 0-0.02156a0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3v-6.724c0-1.304 1.046-2.35 2.35-2.35 1.304 0 2.35 1.046 2.35 2.35v0.8376a0.5428 0.5428 0 0 0 0.8749 0.4296l3.182-2.468-0.02942 0.01374s0.08749-0.05788 0.1765-0.155c0.08906-0.09708 0.2481-0.2676 0.2079-0.5924-0.01894-0.1544-0.1333-0.3798-0.2452-0.4747-0.1119-0.0949-0.1512-0.1021-0.1765-0.1197-0.2538-0.1759-0.4159-0.4705-0.4159-0.7925 0-0.5365 0.4266-0.9572 0.9631-0.9573zm3.515 1.038c0.374 0 0.6669 0.2949 0.6669 0.6689 0 0.2634-0.1462 0.4853-0.3629 0.5944-5.26e-4 2.647e-4 -0.0014-2.637e-4 -2e-3 0-0.03386 0.01188-0.0579 0.02194-0.07258 0.02744-0.03712 0.01393-0.0737 0.0257-0.1118 0.03337-0.03811 0.00763-0.07742 0.01177-0.1177 0.01177-0.04676 0-0.09349-0.00497-0.1373-0.01374-0.04382-0.00873-0.0855-0.02255-0.1255-0.03923-0.04004-0.01666-0.07833-0.03705-0.1138-0.06081-0.03545-0.02376-0.06807-0.05041-0.09807-0.08042-0.03003-0.03003-0.05667-0.06264-0.08042-0.09808-0.02376-0.03545-0.04415-0.07373-0.06081-0.1138-0.01667-0.04004-0.03048-0.08173-0.03923-0.1255-0.0087-0.04381-0.01177-0.0886-0.01177-0.1354 1e-5 -0.01184 0.0038-0.04005 0.0059-0.0922 2.62e-4 -0.00664-1.63e-4 -0.00398 0-0.01177 0.04808-0.3236 0.3221-0.5649 0.6611-0.5649z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -45,7 +45,7 @@ export class Claim extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify() {
|
async verify() {
|
||||||
const claim = doipjs.Claim.fromJson(JSON.parse(this.getAttribute('data-claim')));
|
const claim = doipjs.Claim.fromJSON(JSON.parse(this.getAttribute('data-claim')));
|
||||||
await claim.verify({
|
await claim.verify({
|
||||||
proxy: {
|
proxy: {
|
||||||
policy: 'adaptive',
|
policy: 'adaptive',
|
||||||
|
@ -59,25 +59,23 @@ export class Claim extends HTMLElement {
|
||||||
updateContent(value) {
|
updateContent(value) {
|
||||||
const root = this;
|
const root = this;
|
||||||
const claimJson = JSON.parse(value);
|
const claimJson = JSON.parse(value);
|
||||||
const claim = doipjs.Claim.fromJson(claimJson);
|
const claim = doipjs.Claim.fromJSON(claimJson);
|
||||||
|
|
||||||
console.log(claimJson);
|
root.querySelector('.info .title').innerText = claimJson.display.profileName;
|
||||||
|
|
||||||
root.querySelector('.info .title').innerText = claimJson.display.name;
|
|
||||||
root.querySelector('.info .subtitle').innerText = claimJson.display.serviceProviderName ??
|
root.querySelector('.info .subtitle').innerText = claimJson.display.serviceProviderName ??
|
||||||
(claim.status < 300 ? '???' : '---');
|
(claim.status < 300 ? '???' : '---');
|
||||||
|
root.querySelector('.info img').setAttribute('src',
|
||||||
|
`https://design.keyoxide.org/brands/service-providers/${claimJson.display.serviceProviderId
|
||||||
|
? claimJson.display.serviceProviderId : '_'}/icon.svg`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (claim.status >= 200) {
|
if (claim.status >= 200) {
|
||||||
root.setAttribute('data-status', claim.status < 300 ? 'success' : 'failed');
|
root.setAttribute('data-status', claim.status < 300 ? 'success' : 'failed');
|
||||||
// root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.status < 300 ? 'success' : 'failed');
|
|
||||||
} else {
|
} else {
|
||||||
root.setAttribute('data-status', 'running');
|
root.setAttribute('data-status', 'running');
|
||||||
// root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
root.setAttribute('data-status', 'failed');
|
root.setAttribute('data-status', 'failed');
|
||||||
// root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const elContent = root.querySelector('.content');
|
const elContent = root.querySelector('.content');
|
||||||
|
@ -110,15 +108,15 @@ export class Claim extends HTMLElement {
|
||||||
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
|
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
|
||||||
|
|
||||||
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
|
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||||
if (claim.matches[0].profile.uri) {
|
if (claimJson.display.profileUrl) {
|
||||||
profile_link.innerHTML = `Profile link: <a rel="me" href="${claim.matches[0].profile.uri}" aria-label="link to profile">${claim.matches[0].profile.uri}</a>`;
|
profile_link.innerHTML = `Profile link: <a rel="me" href="${claimJson.display.profileUrl}" aria-label="link to profile">${claimJson.display.profileUrl}</a>`;
|
||||||
} else {
|
} else {
|
||||||
profile_link.innerHTML = `Profile link: not accessible from browser`;
|
profile_link.innerHTML = `Profile link: not accessible from browser`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proof_link = subsection_links_text.appendChild(document.createElement('p'));
|
const proof_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||||
if (claim.matches[0].proof.request.uri) {
|
if (claimJson.display.proofUrl) {
|
||||||
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.request.uri}" aria-label="link to profile">${claim.matches[0].proof.request.uri}</a>`;
|
proof_link.innerHTML = `Proof link: <a href="${claimJson.display.proofUrl}" aria-label="link to profile">${claimJson.display.proofUrl}</a>`;
|
||||||
} else {
|
} else {
|
||||||
proof_link.innerHTML = `Proof link: not accessible from browser`;
|
proof_link.innerHTML = `Proof link: not accessible from browser`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -93,6 +94,9 @@ kx-claim {
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +104,17 @@ kx-claim {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translateY(3px);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.claim__links {
|
.claim__links {
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -199,15 +214,15 @@ kx-claim {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--button-text-color);
|
color: var(--button-text-color);
|
||||||
background-color: var(--button-background-color);
|
background-color: var(--button-background-color);
|
||||||
border: solid 2px var(--button-border-color);
|
border: solid 1px var(--button-border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--button-hover-background-color);
|
background-color: var(--button-background-color-hover);
|
||||||
border-color: var(--button-hover-border-color);
|
border-color: var(--button-border-color-hover);
|
||||||
color: var(--button-hover-text-color);
|
color: var(--button-text-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-status="running"] {
|
&[data-status="running"] {
|
||||||
|
|
|
@ -32,12 +32,12 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
@use './styles/typography.scss';
|
@use './styles/typography.scss';
|
||||||
@use './styles/forms.scss';
|
@use './styles/forms.scss';
|
||||||
|
|
||||||
@import '../node_modules/fork-awesome/css/fork-awesome.css';
|
|
||||||
@import '../node_modules/dialog-polyfill/dist/dialog-polyfill.css';
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
:focus {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
/* HELPERS */
|
/* HELPERS */
|
||||||
.spacer {
|
.spacer {
|
||||||
|
@ -72,14 +72,16 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
|
|
||||||
/* DIALOGS */
|
/* DIALOGS */
|
||||||
dialog {
|
dialog {
|
||||||
width: 100% !important;
|
max-width: 480px;
|
||||||
max-width: 800px !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
word-wrap: anywhere;
|
word-wrap: anywhere;
|
||||||
}
|
background-color: var(--section-background-color);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
dialog>div {
|
&::backdrop {
|
||||||
padding: 1em;
|
background-color: #000;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog form[method="Dialog"] {
|
dialog form[method="Dialog"] {
|
||||||
|
|
|
@ -73,7 +73,8 @@ form textarea {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input,
|
||||||
|
textarea {
|
||||||
color: var(--input-text-color);
|
color: var(--input-text-color);
|
||||||
background-color: var(--input-background-color);
|
background-color: var(--input-background-color);
|
||||||
border: solid 1px var(--input-border-color);
|
border: solid 1px var(--input-border-color);
|
||||||
|
@ -107,6 +108,22 @@ 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 {
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.inline {
|
button.inline {
|
||||||
|
@ -115,15 +132,21 @@ button.inline {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#search {
|
#search {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 50vh;
|
min-height: 50vh;
|
||||||
|
gap: 48px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
width: 96px;
|
||||||
|
fill: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -27,6 +27,10 @@ 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/>.
|
||||||
*/
|
*/
|
||||||
|
hr {
|
||||||
|
margin: 3em 0;
|
||||||
|
color: var(--line-color-subtle);
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -56,14 +60,12 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logo {
|
a.logo {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--primary-color);
|
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
@ -81,7 +83,7 @@ header {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.text {
|
a {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
||||||
|
@ -191,3 +193,28 @@ section {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.screenshots {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
img {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ h1 {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
margin: 2em 0 0.5em;
|
margin: 1em 0 0.5em;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--h2-color);
|
color: var(--h2-color);
|
||||||
|
|
|
@ -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: #fafafa;
|
||||||
|
--background-color-dark: #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,9 @@ 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);
|
||||||
|
|
||||||
--button-text-color: var(--text-color);
|
--button-text-color: var(--text-color);
|
||||||
--button-text-color-hover: var(--text-color);
|
--button-text-color-hover: var(--text-color);
|
||||||
|
@ -101,14 +114,17 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
--footer-text-color: var(--text-color-subtle);
|
--footer-text-color: var(--text-color-subtle);
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
--primary-color: var(--purple-300);
|
color-scheme: dark;
|
||||||
--primary-color-subtle: var(--purple-500);
|
|
||||||
|
|
||||||
--body-background-color: #0a0a0a;
|
--primary-color: var(--primary-color-dark);
|
||||||
|
--primary-color-subtle: var(--primary-color-subtle-dark);
|
||||||
|
|
||||||
|
--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);
|
||||||
|
@ -121,6 +137,8 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
--link-color-subtle: var(--text-color);
|
--link-color-subtle: var(--text-color);
|
||||||
--link-color-hover: var(--primary-color);
|
--link-color-hover: var(--primary-color);
|
||||||
|
|
||||||
|
--line-color-subtle: var(--grey-700);
|
||||||
|
|
||||||
--button-text-color: var(--text-color);
|
--button-text-color: var(--text-color);
|
||||||
--button-text-color-hover: var(--text-color);
|
--button-text-color-hover: var(--text-color);
|
||||||
--button-border-color: var(--grey-700);
|
--button-border-color: var(--grey-700);
|
||||||
|
|
|
@ -27,7 +27,6 @@ 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/>.
|
||||||
*/
|
*/
|
||||||
import dialogPolyfill from 'dialog-polyfill'
|
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import * as openpgp from 'openpgp'
|
import * as openpgp from 'openpgp'
|
||||||
import * as utils from './utils.js'
|
import * as utils from './utils.js'
|
||||||
|
@ -51,17 +50,6 @@ const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-
|
||||||
|
|
||||||
// Initialize UI elements and event listeners
|
// Initialize UI elements and event listeners
|
||||||
export function init() {
|
export function init() {
|
||||||
// Register modals
|
|
||||||
document.querySelectorAll('dialog').forEach(function(d) {
|
|
||||||
dialogPolyfill.registerDialog(d);
|
|
||||||
d.addEventListener('click', function(ev) {
|
|
||||||
if (ev && ev.target != d) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
d.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run context-dependent scripts
|
// Run context-dependent scripts
|
||||||
if (elFormEncrypt) {
|
if (elFormEncrypt) {
|
||||||
runEncryptionForm()
|
runEncryptionForm()
|
||||||
|
@ -349,8 +337,8 @@ const runArgon2GenerationUtility = () => {
|
||||||
elFeedback.innerHTML = "";
|
elFeedback.innerHTML = "";
|
||||||
} else {
|
} else {
|
||||||
let feedbackContent = "";
|
let feedbackContent = "";
|
||||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||||
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
||||||
}
|
}
|
||||||
if (!(elInput.value === elInput.value.toLowerCase())) {
|
if (!(elInput.value === elInput.value.toLowerCase())) {
|
||||||
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
||||||
|
@ -391,7 +379,7 @@ window.kx__fixArgon2Input = () => {
|
||||||
const elInput = document.querySelector('#form-util-argon2-generate .input');
|
const elInput = document.querySelector('#form-util-argon2-generate .input');
|
||||||
elInput.value = elInput.value.toLowerCase();
|
elInput.value = elInput.value.toLowerCase();
|
||||||
|
|
||||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||||
elInput.value = `openpgp4fpr:${elInput.value}`;
|
elInput.value = `openpgp4fpr:${elInput.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,8 +402,8 @@ const runBcryptGenerationUtility = () => {
|
||||||
elFeedback.innerHTML = "";
|
elFeedback.innerHTML = "";
|
||||||
} else {
|
} else {
|
||||||
let feedbackContent = "";
|
let feedbackContent = "";
|
||||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||||
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
||||||
}
|
}
|
||||||
if (!(elInput.value === elInput.value.toLowerCase())) {
|
if (!(elInput.value === elInput.value.toLowerCase())) {
|
||||||
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
||||||
|
@ -456,7 +444,7 @@ window.kx__fixBcryptInput = () => {
|
||||||
const elInput = document.querySelector('#form-util-bcrypt-generate .input');
|
const elInput = document.querySelector('#form-util-bcrypt-generate .input');
|
||||||
elInput.value = elInput.value.toLowerCase();
|
elInput.value = elInput.value.toLowerCase();
|
||||||
|
|
||||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||||
elInput.value = `openpgp4fpr:${elInput.value}`;
|
elInput.value = `openpgp4fpr:${elInput.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
28
template.env
28
template.env
|
@ -12,28 +12,22 @@
|
||||||
# $ while read -r line; do echo -nE "$line\n" ; done < public.pem > public-oneline.pem
|
# $ while read -r line; do echo -nE "$line\n" ; done < public.pem > public-oneline.pem
|
||||||
#ACTIVITYPUB_PUBLIC_KEY=
|
#ACTIVITYPUB_PUBLIC_KEY=
|
||||||
|
|
||||||
# Domain for DOIP Proxy server
|
# Domain for Keyoxide Proxy server
|
||||||
# Source code for the server can be found here https://codeberg.org/keyoxide/doip-proxy
|
# To host a Keyoxide Proxy server, refer to https://docs.keyoxide.org/self-hosting/
|
||||||
#PROXY_HOSTNAME=
|
#PROXY_HOSTNAME=
|
||||||
|
|
||||||
|
# Domain for Dicebear API server
|
||||||
|
# Defaults to: api.dicebear.com
|
||||||
|
#DICEBEAR_API_HOSTNAME=
|
||||||
|
|
||||||
# Tor Onion URL
|
# Tor Onion URL
|
||||||
# The full http:// onion url to add as an 'Onion-Location' header
|
# The full http:// onion url to add as an 'Onion-Location' header
|
||||||
#ONION_URL=
|
#ONION_URL=
|
||||||
|
|
||||||
# Highlights
|
|
||||||
# Highlight up to three profiles on the homepage
|
|
||||||
#KX_HIGHLIGHTS_1_NAME=
|
|
||||||
#KX_HIGHLIGHTS_1_DESCRIPTION=
|
|
||||||
#KX_HIGHLIGHTS_1_FINGERPRINT=
|
|
||||||
|
|
||||||
#KX_HIGHLIGHTS_2_NAME=
|
|
||||||
#KX_HIGHLIGHTS_2_DESCRIPTION=
|
|
||||||
#KX_HIGHLIGHTS_2_FINGERPRINT=
|
|
||||||
|
|
||||||
#KX_HIGHLIGHTS_3_NAME=
|
|
||||||
#KX_HIGHLIGHTS_3_DESCRIPTION=
|
|
||||||
#KX_HIGHLIGHTS_3_FINGERPRINT=
|
|
||||||
|
|
||||||
# Enable caching of keys (experimental)
|
# Enable caching of keys (experimental)
|
||||||
# Opt-in; to disable, omit the environment variable
|
# Opt-in; to disable, omit the environment variable
|
||||||
#ENABLE_EXPERIMENTAL_CACHE=
|
#ENABLE_EXPERIMENTAL_CACHE=true
|
||||||
|
|
||||||
|
# Enable profile request rate limiting (experimental)
|
||||||
|
# Opt-in; to disable, omit the environment variable
|
||||||
|
#ENABLE_EXPERIMENTAL_RATE_LIMITER=true
|
||||||
|
|
8
views/429.pug
Normal file
8
views/429.pug
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
extends templates/base.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1 429 TOO MANY REQUESTS
|
||||||
|
p
|
||||||
|
| Too many requests from this IP, please try again later.
|
||||||
|
br
|
||||||
|
| Limit: 3 profile requests per second.
|
48
views/apps.pug
Normal file
48
views/apps.pug
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
extends templates/base.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
section
|
||||||
|
h1 Apps
|
||||||
|
h2 Keyoxide mobile
|
||||||
|
p The app allows you to verify the online identities of Keyoxide profiles.
|
||||||
|
.screenshots
|
||||||
|
img(src="/static/img/keyoxide_mobile_dark_home.jpg"
|
||||||
|
alt="Screenshot of Keyoxide mobile app")
|
||||||
|
img(src="/static/img/keyoxide_mobile_dark_profile.jpg"
|
||||||
|
alt="Screenshot of Keyoxide mobile app")
|
||||||
|
img(src="/static/img/keyoxide_mobile_light_home.jpg"
|
||||||
|
alt="Screenshot of Keyoxide mobile app")
|
||||||
|
h3 Download
|
||||||
|
.banners
|
||||||
|
a(href="https://f-droid.org/packages/org.keyoxide.keyoxide/")
|
||||||
|
img(src="https://img.shields.io/badge/F--Droid-1976D2?style=for-the-badge&logo=f-droid&logoColor=white",
|
||||||
|
alt="Get it on F-Droid")
|
||||||
|
a(href="https://play.google.com/store/apps/details?id=org.keyoxide.keyoxide&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1")
|
||||||
|
img(src="https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white",
|
||||||
|
alt="Get it on Google Play")
|
||||||
|
a(href="https://apps.apple.com/us/app/keyoxide/id1670664318")
|
||||||
|
img(src="https://img.shields.io/badge/App_Store-0D96F6?style=for-the-badge&logo=app-store&logoColor=white",
|
||||||
|
alt="Get it on App Store")
|
||||||
|
a(href="https://codeberg.org/Berker/keyoxide-flutter/releases")
|
||||||
|
img(src="https://img.shields.io/badge/Codeberg.org-2185d0?style=for-the-badge&logo=codeberg&logoColor=white",
|
||||||
|
alt="Get it on Codeberg.org")
|
||||||
|
p
|
||||||
|
| Developer:
|
||||||
|
a(href="https://keyoxide.org/aspe:keyoxide.org:WHM3OC7UFRARIVEXDXUV4GVXNQ") Berker Sen
|
||||||
|
br
|
||||||
|
| Source code:
|
||||||
|
a(href="https://codeberg.org/Berker/keyoxide-flutter") Codeberg.org
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
h2 Keyoxide ASP web tool
|
||||||
|
p The web tool lets you create and maintain Keyoxide profiles using the ASP method.
|
||||||
|
.screenshots
|
||||||
|
img(src="/static/img/keyoxide_asp_web_home.jpg"
|
||||||
|
alt="Screenshot of Keyoxide ASP web tool")
|
||||||
|
p
|
||||||
|
| Homepage:
|
||||||
|
a(href="https://asp.keyoxide.org") asp.keyoxide.org
|
||||||
|
br
|
||||||
|
| Source code:
|
||||||
|
a(href="https://codeberg.org/keyoxide/kx-aspe-web") Codeberg.org
|
|
@ -3,4 +3,4 @@ extends templates/base.pug
|
||||||
block content
|
block content
|
||||||
section
|
section
|
||||||
h1= title
|
h1= title
|
||||||
!{ content }
|
| !{ content }
|
||||||
|
|
|
@ -2,6 +2,9 @@ extends templates/base.pug
|
||||||
|
|
||||||
block content
|
block content
|
||||||
section#search.form-wrapper
|
section#search.form-wrapper
|
||||||
|
<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m10.87 0c-0.4551 0-0.832 0.3769-0.832 0.832 0 0.4551 0.3769 0.832 0.832 0.832 0.4551 0 0.832-0.3769 0.832-0.832 0-0.4551-0.3769-0.832-0.832-0.832zm1.85 0.293c-0.5683 0-1.037 0.4688-1.037 1.037 0 0.2368 0.0807 0.4573 0.2168 0.6328-0.05687-0.007139-0.1151-0.01172-0.1738-0.01172-0.7683 0-1.398 0.6302-1.398 1.398 0 0.7683 0.6302 1.4 1.398 1.4 0.3773 0 0.7222-0.1523 0.9746-0.3984-0.00891 0.07397-0.01562 0.1483-0.01562 0.2246 0 0.5999 0.3126 1.131 0.7734 1.48l-2.586 2.012v-0.1152c0-1.817-1.482-3.299-3.299-3.299-1.817 0-3.299 1.482-3.299 3.299v6.828c-1.699e-4 -0.01435-0.001953-0.009604-0.001953 0.03516v5.883c-3.1e-6 1.819 1.482 3.301 3.301 3.301 1.73 0 3.115-1.354 3.25-3.051l3.744 2.494c1.512 1.008 3.566 0.5976 4.574-0.9141 1.008-1.512 0.5976-3.566-0.9141-4.574l-5.014-3.344 5.209-4.053c1.263-0.9824 1.63-2.72 0.9102-4.115-0.006296-0.01221-0.0141-0.02849-0.02148-0.04297 0.2572-0.2824 0.4141-0.657 0.4141-1.066 0-0.8712-0.7128-1.584-1.584-1.584-0.6345 0-1.187 0.3779-1.439 0.9199-0.08446-0.006921-0.1692-0.01237-0.2539-0.01367 0.0014-0.03013 0.003906-0.04019 0.003906-0.08008 0-1.036-0.8484-1.885-1.885-1.885-0.579 5.6e-6 -1.097 0.2653-1.443 0.6797 1.08e-4 -0.007109 0-0.01435 0-0.02148 0-0.3811-0.1542-0.7293-0.4043-0.9824 0.5684 0 1.037-0.4688 1.037-1.037 0-0.5683-0.4688-1.037-1.037-1.037zm-1.85 0.4219c0.06898 0 0.1172 0.04821 0.1172 0.1172s-0.04821 0.1172-0.1172 0.1172c-0.06898 0-0.1172-0.04821-0.1172-0.1172s0.0482-0.1172 0.1172-0.1172zm1.85 0.293c0.1822 0 0.3223 0.14 0.3223 0.3223 0 0.1822-0.14 0.3223-0.3223 0.3223-0.1822 0-0.3223-0.14-0.3223-0.3223 0-0.1822 0.14-0.3223 0.3223-0.3223zm-0.9941 1.658c0.3822 0 0.6836 0.3014 0.6836 0.6836 0 0.3822-0.3014 0.6855-0.6836 0.6855-0.3822 0-0.6836-0.3034-0.6836-0.6855 0-0.3822 0.3014-0.6836 0.6836-0.6836zm2.842 0.7402c0.6502-1.88e-5 1.17 0.5197 1.17 1.17 0 0.05687-0.005159 0.1097-0.005859 0.1914-6.61e-4 0.08173 0.01051 0.21 0.08008 0.3281 0.06857 0.1163 0.1972 0.2073 0.3047 0.2402 0.1075 0.03295 0.1919 0.03178 0.2617 0.03125 0.0602-4.678e-4 0.1197 0.005507 0.1797 0.009766 0.0172 0.6402 0.4194 1.188 0.9824 1.422 0.08721 0.03618 0.1774 0.06577 0.2715 0.08594 0.003893 8.346e-4 0.007816 0.001148 0.01172 0.001953 0.00711 0.001489 0.01434 0.002515 0.02148 0.003906 0.04462 0.008569 0.08895 0.01678 0.1348 0.02148 0.001285 1.341e-4 0.00262-1.31e-4 0.003906 0 0.05184 0.00519 0.1051 0.007812 0.1582 0.007812 0.09398 0 0.1858-0.009457 0.2754-0.02539 0.002495-4.438e-4 0.005321 4.557e-4 0.007812 0 0.003259-5.976e-4 0.006512-0.001335 0.009766-0.001953 0.08604-0.01629 0.1694-0.04058 0.25-0.07031 0.004443-0.001639 0.009247-0.002228 0.01367-0.003906 0.0038-0.00142 0.002259-6.138e-4 0.005859-0.001953 0.5338 1.086 0.2618 2.415-0.7188 3.178l-5.604 4.357a0.3573 0.3573 0 0 0 0.02148 0.5801l5.428 3.617c1.19 0.7933 1.51 2.394 0.7168 3.584-0.7933 1.19-2.394 1.51-3.584 0.7168l-4.25-2.832a0.3573 0.3573 0 0 0-0.5547 0.2969v0.3848c0 1.433-1.153 2.586-2.586 2.586-1.433 0-2.586-1.153-2.586-2.586v-5.883c0 0.02601 0.001373 0.01561 0.001953-0.03125a0.3573 0.3573 0 0 0 0-0.001953 0.3573 0.3573 0 0 0 0-0.001953v-6.828c0-1.43 1.154-2.584 2.584-2.584 1.43 0 2.584 1.154 2.584 2.584v0.8457a0.3573 0.3573 0 0 0 0.5762 0.2832l3.207-2.496s0.07527-0.04914 0.1484-0.1289c0.07317-0.07976 0.1932-0.2171 0.1641-0.4531-0.01519-0.1239-0.1002-0.2811-0.1855-0.3535-0.08537-0.07239-0.1288-0.08753-0.166-0.1133-0.3095-0.2145-0.502-0.5695-0.502-0.9609 0-0.6502 0.5177-1.17 1.168-1.17zm3.574 1.057c0.4851 0 0.8691 0.386 0.8691 0.8711 0 0.3632-0.2156 0.671-0.5273 0.8027-5.5e-4 2.323e-4 -0.001402-2.313e-4 -0.001953 0-0.008517 0.003475-0.0281 0.009073-0.03516 0.01172-0.04666 0.01751-0.09382 0.03145-0.1426 0.04102-0.002029 3.979e-4 -0.003828 0.00157-0.00586 0.001953-0.003817 6.812e-4 -0.007883-6.32e-4 -0.01172 0-0.04722 0.008255-0.09583 0.01367-0.1445 0.01367-0.06064 0-0.1188-0.006076-0.1758-0.01758s-0.1119-0.0289-0.1641-0.05078c-0.0522-0.02188-0.1021-0.04893-0.1484-0.08008s-0.08962-0.06619-0.1289-0.1055-0.07432-0.0826-0.1055-0.1289-0.0582-0.09624-0.08008-0.1484c-0.02188-0.0522-0.03928-0.1071-0.05078-0.1641-0.0115-0.05698-0.01758-0.1151-0.01758-0.1758 1e-5 -0.01046 0.001317-0.032 0.001954-0.04492 7.45e-4 -0.01496 4.8e-4 -0.03018 0.001953-0.04492 0.04418-0.4421 0.4124-0.7812 0.8672-0.7812z"/>
|
||||||
|
</svg>
|
||||||
form(action="post")
|
form(action="post")
|
||||||
input#query(type="search" name="query" required placeholder="Search for a profile")
|
input#query(type="search" name="query" required placeholder="Search for a profile")
|
||||||
input(type="submit" value="Search")
|
input(type="submit" value="Search")
|
||||||
|
|
|
@ -25,10 +25,9 @@ footer
|
||||||
a(href="https://ariadne.id") ariadne.id
|
a(href="https://ariadne.id") ariadne.id
|
||||||
|
|
||||||
p
|
p
|
||||||
| Version #{meta.keyoxide.version}
|
| Version
|
||||||
if (meta.env === "development")
|
a(href=meta.keyoxide.sourceUrl)= meta.keyoxide.semver
|
||||||
| -dev
|
|
||||||
br
|
br
|
||||||
|
|
||||||
| © 2023 Keyoxide project contributors
|
| © 2020-2024 Keyoxide project contributors
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
header
|
header
|
||||||
.container
|
.container
|
||||||
nav
|
nav
|
||||||
a.logo(href='/' aria-label='Home')
|
a.logo(href='/' aria-label='Home') Keyoxide
|
||||||
img(src='/static/img/logo_circle.png' alt='Keyoxide' aria-hidden='true')
|
.spacer
|
||||||
| Keyoxide
|
|
||||||
.links
|
.links
|
||||||
a.text(href='https://docs.keyoxide.org/getting-started') Getting started
|
a.text(href='https://docs.keyoxide.org/getting-started') Getting started
|
||||||
|
a.text(href='/apps') Apps
|
||||||
a.text(href='https://docs.keyoxide.org') Docs
|
a.text(href='https://docs.keyoxide.org') Docs
|
||||||
a.text(href='https://blog.keyoxide.org') Blog
|
a.text(href='https://blog.keyoxide.org') Blog
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
extends templates/base.pug
|
extends templates/base.pug
|
||||||
|
|
||||||
mixin generatePersona(persona, isPrimary)
|
mixin generatePersona(persona, isPrimary)
|
||||||
|
if persona.claims.length > 0
|
||||||
h2
|
h2
|
||||||
if persona.email
|
if persona.email
|
||||||
| Identity claims (
|
| Identity claims (
|
||||||
|
@ -20,8 +21,9 @@ mixin generatePersona(persona, isPrimary)
|
||||||
details(aria-label="Claim")
|
details(aria-label="Claim")
|
||||||
summary
|
summary
|
||||||
.info
|
.info
|
||||||
|
img(src=`https://design.keyoxide.org/brands/service-providers/_/icon.svg` onerror="this.src='https://design.keyoxide.org/brands/service-providers/_/icon.svg'")
|
||||||
p
|
p
|
||||||
span.title= claim.display.name
|
span.title= claim.display.profileName
|
||||||
span.subtitle-wrapper
|
span.subtitle-wrapper
|
||||||
| [
|
| [
|
||||||
span.subtitle= claim.display.serviceproviderName
|
span.subtitle= claim.display.serviceproviderName
|
||||||
|
@ -39,16 +41,8 @@ mixin generatePersona(persona, isPrimary)
|
||||||
.subsection
|
.subsection
|
||||||
img(src='/static/img/link.png')
|
img(src='/static/img/link.png')
|
||||||
div
|
div
|
||||||
if (claim.display.url)
|
p Claim link:
|
||||||
p Profile link:
|
a(rel="me" href=claim.uri aria-label="Link to claim")= claim.uri
|
||||||
a(rel='me' href=claim.display.url aria-label="Link to profile")= claim.display.url
|
|
||||||
else
|
|
||||||
p Profile link: not accessible from browser
|
|
||||||
if (claim.matches.length === 1 && claim.matches[0].proof.uri)
|
|
||||||
p Proof link:
|
|
||||||
a(href=claim.matches[0].proof.uri aria-label="Link to proof")= claim.matches[0].proof.uri
|
|
||||||
else
|
|
||||||
p Proof link: not accessible from browser
|
|
||||||
|
|
||||||
block content
|
block content
|
||||||
if (data && 'publicKey' in data)
|
if (data && 'publicKey' in data)
|
||||||
|
@ -117,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")
|
||||||
|
|
||||||
|
@ -136,9 +140,14 @@ block content
|
||||||
each persona, index in data.personas
|
each persona, index in data.personas
|
||||||
unless index == data.primaryPersonaIndex
|
unless index == data.primaryPersonaIndex
|
||||||
+generatePersona(persona, false)
|
+generatePersona(persona, false)
|
||||||
|
h2 Profile
|
||||||
|
if data.verifiers.length > 0
|
||||||
|
button.themed(onClick=`showQR('${data.verifiers[0].url}', 'profile_verifier_url')` aria-label='Show profile ID QR') Keyoxide profile 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
|
||||||
|
if (data && data.publicKey)
|
||||||
h3 Public key
|
h3 Public key
|
||||||
kx-key.kx-item(data-keydata=data.publicKey)
|
kx-key.kx-item(data-keydata=data.publicKey)
|
||||||
details(aria-label="Key")
|
details(aria-label="Key")
|
||||||
|
@ -156,9 +165,3 @@ block content
|
||||||
div
|
div
|
||||||
p Key link:
|
p Key link:
|
||||||
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
|
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
|
||||||
hr
|
|
||||||
if (data.profileType === 'openpgp')
|
|
||||||
.subsection
|
|
||||||
img(src='/static/img/qrcode.png')
|
|
||||||
div
|
|
||||||
button(onClick=`showQR('${data.publicKey.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR
|
|
|
@ -6,6 +6,7 @@ html(lang='en')
|
||||||
meta(name='theme-color' content='#fff')
|
meta(name='theme-color' content='#fff')
|
||||||
meta(name='description' content='Modern and secure platform to manage a decentralized identity based on cryptographic keys')
|
meta(name='description' content='Modern and secure platform to manage a decentralized identity based on cryptographic keys')
|
||||||
link(rel='shortcut icon' href='/favicon.svg')
|
link(rel='shortcut icon' href='/favicon.svg')
|
||||||
|
link(rel='stylesheet' href='/static/main.css')
|
||||||
title= (title ? title : 'Keyoxide')
|
title= (title ? title : 'Keyoxide')
|
||||||
|
|
||||||
include ../partials/header.pug
|
include ../partials/header.pug
|
||||||
|
@ -16,7 +17,6 @@ html(lang='en')
|
||||||
|
|
||||||
include ../partials/footer.pug
|
include ../partials/footer.pug
|
||||||
|
|
||||||
link(rel='stylesheet' href='/static/main.css')
|
|
||||||
script(type='application/javascript' defer src='/static/openpgp.js' charset='utf-8')
|
script(type='application/javascript' defer src='/static/openpgp.js' charset='utf-8')
|
||||||
script(type='application/javascript' defer src='/static/doipFetchers.js' charset='utf-8')
|
script(type='application/javascript' defer src='/static/doipFetchers.js' charset='utf-8')
|
||||||
script(type='application/javascript' defer src='/static/doip.js' charset='utf-8')
|
script(type='application/javascript' defer src='/static/doip.js' charset='utf-8')
|
||||||
|
|
|
@ -11,7 +11,7 @@ block content
|
||||||
a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
|
a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
|
||||||
| hashes useful to
|
| hashes useful to
|
||||||
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
|
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
|
||||||
| . Be sure to include "openpgp4fpr:" for a valid proof!
|
| . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
|
||||||
h3 Input
|
h3 Input
|
||||||
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
|
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
|
||||||
h3 Hash
|
h3 Hash
|
||||||
|
|
|
@ -11,7 +11,7 @@ block content
|
||||||
a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt
|
a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt
|
||||||
| hashes useful to
|
| hashes useful to
|
||||||
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
|
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
|
||||||
| . Be sure to include "openpgp4fpr:" for a valid proof!
|
| . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
|
||||||
h3 Input
|
h3 Input
|
||||||
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
|
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
|
||||||
h3 Hash
|
h3 Hash
|
||||||
|
|
Loading…
Reference in a new issue