forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
18 Commits
increment-
...
project-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 22de6113e9 | |||
| 87139f203c | |||
| c8de13d376 | |||
| 2ccfb283b4 | |||
| 552fce3281 | |||
| 12de3dec4f | |||
| b171e1ae13 | |||
| dc54006fca | |||
| 9b4db018f5 | |||
| 519f320a2e | |||
|
|
f1b3094026 | ||
|
|
e5ad87f4d5 | ||
|
|
7de6171911 | ||
|
|
bb6bacac97 | ||
|
|
40fc6a29a4 | ||
|
|
9ec19fa4ee | ||
| 28b20f86ea | |||
| 502109de4b |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,13 +1,16 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
signature.bin
|
||||||
|
*.pem
|
||||||
|
verified.txt
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# Log files
|
# Log filesopenssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
jq
|
||||||
|
|
||||||
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
||||||
|
|
||||||
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
||||||
|
|||||||
25
openssl_signing_console.sh
Executable file
25
openssl_signing_console.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
|
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
||||||
|
|
||||||
|
# Read binary signature from file and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
|
# Construct the JWT
|
||||||
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
|
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
||||||
|
|
||||||
|
|
||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -61,6 +61,7 @@
|
|||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
@@ -9639,6 +9641,24 @@
|
|||||||
"uint8arrays": "^3.0.0"
|
"uint8arrays": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue-leaflet/vue-leaflet": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.10.1.tgz",
|
||||||
|
"integrity": "sha512-RNEDk8TbnwrJl8ujdbKgZRFygLCxd0aBcWLQ05q/pGv4+d0jamE3KXQgQBqGAteE1mbQsk3xoNcqqUgaIGfWVg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/leaflet": "^1.5.7",
|
||||||
|
"leaflet": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/leaflet": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
|
||||||
@@ -19067,6 +19087,12 @@
|
|||||||
"launch-editor": "^2.6.0"
|
"launch-editor": "^2.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
|
|||||||
@@ -2,24 +2,20 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
|
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
|
||||||
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
||||||
- 01 add a location for a project via map pin :
|
|
||||||
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
|
|
||||||
- 40 notifications :
|
- 40 notifications :
|
||||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
|
|
||||||
- 01 add a location for a project via map pin
|
- 01 add my bounding box(es) of interest for searches on Nearby part of Discovery page
|
||||||
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
|
- .5 search by a bounding box(s) of interest for local projects (see API by clicking on "Nearby")
|
||||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||||
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image (discovery page display also)
|
|
||||||
- SEE: https://github.com/dmester/jdenticon assignee:jose
|
|
||||||
|
|
||||||
- 08 Scan QR code to import into contacts assignee:matthew
|
- 08 Scan QR code to import into contacts assignee:matthew
|
||||||
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
||||||
|
|
||||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew
|
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
||||||
|
|
||||||
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) assignee-group:ui
|
||||||
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose
|
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
|
||||||
|
|
||||||
- Home Feed & Quick Give screen :
|
- Home Feed & Quick Give screen :
|
||||||
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
||||||
@@ -33,10 +29,10 @@ tasks:
|
|||||||
- .5 add project ID to the URL, to make a project publicly-accessible
|
- .5 add project ID to the URL, to make a project publicly-accessible
|
||||||
- .5 remove edit from project page for projects owned by others
|
- .5 remove edit from project page for projects owned by others
|
||||||
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
||||||
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
|
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui
|
||||||
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose
|
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
|
||||||
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||||
- .2 move 'switch identity' to the advanced section
|
- .2 move 'switch identity' to the advanced section assignee-group:ui
|
||||||
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
|
|
||||||
@@ -45,10 +41,12 @@ tasks:
|
|||||||
- 04 allow user to download claims, mine + ones I can see about me from others
|
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||||
- .5 change the derivation path, and regenerate test IDs
|
- .5 change the derivation path, and regenerate test IDs
|
||||||
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
|
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
|
||||||
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
|
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages assignee-group:ui
|
||||||
- .5 customize favicon
|
- .5 customize favicon assignee-group:ui
|
||||||
- .5 Do we want to combine first name & last name?
|
- .5 Do we want to combine first name & last name?
|
||||||
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
|
||||||
|
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||||
|
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -66,7 +64,6 @@ tasks:
|
|||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
- Add disclaimers.
|
- Add disclaimers.
|
||||||
- Rename DB to TimeSafari.
|
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
- Deploy to a server.
|
- Deploy to a server.
|
||||||
- Ensure public server has limits that work for group adoption.
|
- Ensure public server has limits that work for group adoption.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export enum AppString {
|
|||||||
APP_NAME = "Kick-Start with Time",
|
APP_NAME = "Kick-Start with Time",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
faGift,
|
faGift,
|
||||||
faHand,
|
faHand,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
@@ -80,6 +81,7 @@ library.add(
|
|||||||
faGift,
|
faGift,
|
||||||
faHand,
|
faHand,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-xl font-bold">Quick Action</h2>
|
<h2 class="text-xl font-bold">Quick Action</h2>
|
||||||
<p class="mb-4">Show appreciation to a contact:</p>
|
<p class="mb-4">Record a gift from a contact:</p>
|
||||||
|
|
||||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
<li @click="openDialog()">
|
<li @click="openDialog()">
|
||||||
|
|||||||
@@ -39,6 +39,40 @@
|
|||||||
{{ description.length }}/500 max. characters
|
{{ description.length }}/500 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
v-model="includeLocation"
|
||||||
|
@change="includeLocation = true"
|
||||||
|
/>
|
||||||
|
<label for="includeLocation">Include Location</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
v-model:zoom="zoom"
|
||||||
|
:center="[0, 0]"
|
||||||
|
@click="
|
||||||
|
(event) => {
|
||||||
|
latitude = event.latlng.lat;
|
||||||
|
longitude = event.latlng.lng;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="latitude || longitude"
|
||||||
|
:lat-lng="[latitude, longitude]"
|
||||||
|
@click="maybeEraseLatLong()"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
:disabled="isHiddenSave"
|
:disabled="isHiddenSave"
|
||||||
@@ -71,9 +105,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
@@ -83,17 +119,21 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage },
|
components: { AlertMessage, LMap, LMarker, LTileLayer },
|
||||||
})
|
})
|
||||||
export default class NewEditProjectView extends Vue {
|
export default class NewEditProjectView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
|
||||||
projectName = "";
|
|
||||||
description = "";
|
|
||||||
errorMessage = "";
|
|
||||||
numAccounts = 0;
|
|
||||||
alertTitle = "";
|
alertTitle = "";
|
||||||
alertMessage = "";
|
alertMessage = "";
|
||||||
|
apiServer = "";
|
||||||
|
description = "";
|
||||||
|
errorMessage = "";
|
||||||
|
includeLocation = false;
|
||||||
|
latitude = 0;
|
||||||
|
longitude = 0;
|
||||||
|
numAccounts = 0;
|
||||||
|
projectName = "";
|
||||||
|
zoom = 2;
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -185,6 +225,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
vcClaim.identifier = this.projectId;
|
vcClaim.identifier = this.projectId;
|
||||||
}
|
}
|
||||||
|
if (this.includeLocation) {
|
||||||
|
vcClaim.location = {
|
||||||
|
geo: {
|
||||||
|
"@type": "GeoCoordinates",
|
||||||
|
latitude: this.latitude,
|
||||||
|
longitude: this.longitude,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
@@ -299,6 +348,14 @@ export default class NewEditProjectView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public maybeEraseLatLong() {
|
||||||
|
if (window.confirm("Are you sure you don't want to mark a location?")) {
|
||||||
|
this.latitude = 0;
|
||||||
|
this.longitude = 0;
|
||||||
|
this.includeLocation = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,16 @@
|
|||||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
{{ timeSince }}
|
{{ timeSince }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="latitude || longitude">
|
||||||
|
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
||||||
|
<a
|
||||||
|
:href="getOpenStreetMapUrl()"
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Map View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,6 +241,8 @@ export default class ProjectViewView extends Vue {
|
|||||||
expanded = false;
|
expanded = false;
|
||||||
givesToThis: Array<GiveServerRecord> = [];
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
givesByThis: Array<GiveServerRecord> = [];
|
givesByThis: Array<GiveServerRecord> = [];
|
||||||
|
latitude = 0;
|
||||||
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
issuer = "";
|
issuer = "";
|
||||||
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||||
@@ -326,6 +338,8 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.name = resp.data.claim?.name || "(no name)";
|
this.name = resp.data.claim?.name || "(no name)";
|
||||||
this.description = resp.data.claim?.description || "(no description)";
|
this.description = resp.data.claim?.description || "(no description)";
|
||||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||||
|
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
||||||
|
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
||||||
} else if (resp.status === 404) {
|
} else if (resp.status === 404) {
|
||||||
// actually, axios throws an error so we never get here
|
// actually, axios throws an error so we never get here
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -441,6 +455,20 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.$refs.customDialog.open(contact);
|
this.$refs.customDialog.open(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOpenStreetMapUrl() {
|
||||||
|
// Google URL is https://maps.google.com/?q=LAT,LONG
|
||||||
|
return (
|
||||||
|
"https://www.openstreetmap.org/?mlat=" +
|
||||||
|
this.latitude +
|
||||||
|
"&mlon=" +
|
||||||
|
this.longitude +
|
||||||
|
"#map=15/" +
|
||||||
|
this.latitude +
|
||||||
|
"/" +
|
||||||
|
this.longitude
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handleDialogResult(result) {
|
handleDialogResult(result) {
|
||||||
if (result.action === "confirm") {
|
if (result.action === "confirm") {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
316
web-push.md
Normal file
316
web-push.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
Web Push notifications is a web browser messaging protocol defined by the W3C.
|
||||||
|
|
||||||
|
Discussions of this interesting technology are clouded because of a
|
||||||
|
terminological morass.
|
||||||
|
|
||||||
|
To understand how Web Push operates, we need to observe that are three (and
|
||||||
|
potentially four) parties involved. These are:
|
||||||
|
|
||||||
|
1) The user's web browser. Let's call that BROWSER
|
||||||
|
2) The Web Push Service Provider which is operated by the organization
|
||||||
|
controlling the web browser's source code. Here named PROVIDER. An example of a
|
||||||
|
PROVIDER is FCM (Firebase Cloud Messaging) which is owned by Google.
|
||||||
|
3) The Web Application that a user is visiting from their web browser. Let's
|
||||||
|
call this the SERVICE (short for Web Push application service)
|
||||||
|
4) A Custom Web Push Intermediary Service, either third party or self-hosted.
|
||||||
|
Called INTERMEDIARY here. FCM also may fit in this category if the SERVICE
|
||||||
|
has an API key from FCM.]
|
||||||
|
|
||||||
|
The workflow works like this:
|
||||||
|
|
||||||
|
BROWSER visits a website which hosts a SERVICE.
|
||||||
|
|
||||||
|
The SERVICE asks BROWSER for its permission to subscribe to messages coming
|
||||||
|
from the SERVICE.
|
||||||
|
|
||||||
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
|
for notification permission:
|
||||||
|
|
||||||
|
In order to provide this context and explict permission a two-step opt-in process
|
||||||
|
where the user is first presented with a pre-permission dialog box that explains
|
||||||
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
|
Now, to explain what happens in Typescript, we can activate a browser's
|
||||||
|
permission dialogue in this manner:
|
||||||
|
|
||||||
|
```
|
||||||
|
function askPermission(): Promise<NotificationPermission> {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const permissionResult = Notification.requestPermission(function(result) {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissionResult) {
|
||||||
|
permissionResult.then(resolve, reject);
|
||||||
|
}
|
||||||
|
}).then(function(permissionResult) {
|
||||||
|
if (permissionResult !== 'granted') {
|
||||||
|
throw new Error("We weren't granted permission.");
|
||||||
|
}
|
||||||
|
return permissionResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The Notification.permission property indicates the permission level for the
|
||||||
|
current session and returns one of the following string values:
|
||||||
|
|
||||||
|
'granted': The user has granted permission for notifications.
|
||||||
|
'denied': The user has denied permission for notifications.
|
||||||
|
'default': The user has not made a choice yet.
|
||||||
|
|
||||||
|
Once the user has granted permission, the client application registers a service
|
||||||
|
worker using the `ServiceWorkerRegistration` API.
|
||||||
|
|
||||||
|
The `ServiceWorkerRegistration` API is accessible via the browser's `navigator`
|
||||||
|
object and the `navigator.serviceWorker` child object and ultimately directly
|
||||||
|
accessible via the navigator.serviceWorker.register method which also creates
|
||||||
|
the service worker or the navigator.serviceWorker.getRegistration method.
|
||||||
|
|
||||||
|
Once you have a `ServiceWorkerRegistration` object, that object will provide a
|
||||||
|
child object named `pushManager` through which subscription and management of
|
||||||
|
subscriptions may be done.
|
||||||
|
|
||||||
|
Let's go through the `register` method first:
|
||||||
|
|
||||||
|
```
|
||||||
|
navigator.serviceWorker.register('sw.js', { scope: '/' })
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('Service worker registered successfully:', registration);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log('Service worker registration failed:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `sw.js` file contains the logic for what a service worker should do.
|
||||||
|
It executes in a separate thread of execution from the web page but provides a
|
||||||
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
|
Note that there is a scope can specify what network requests it may
|
||||||
|
intercept.
|
||||||
|
|
||||||
|
The Vue project already has its own service worker but it is possible to
|
||||||
|
create multiple service worker files by registering them on different scopes.
|
||||||
|
|
||||||
|
It is useful architecturally to specify a separate server worker file.
|
||||||
|
|
||||||
|
In the case of web push, the path of the scope only has reference to the domain
|
||||||
|
of the service worker and no relationship to the pathing for the web push
|
||||||
|
server. In order to specify more than one server workers each needs to be on
|
||||||
|
different scope paths!
|
||||||
|
|
||||||
|
Here's a version which can be used for testing locally. Note there can be
|
||||||
|
caching issues in your browser! Incognito is highly recommended.
|
||||||
|
|
||||||
|
sw-dev.ts
|
||||||
|
```
|
||||||
|
self.addEventListener('push', function(event: PushEvent) {
|
||||||
|
console.log('Received a push message', event);
|
||||||
|
|
||||||
|
const title = 'Push message';
|
||||||
|
const body = 'The message body';
|
||||||
|
const icon = '/images/icon-192x192.png';
|
||||||
|
const tag = 'simple-push-demo-notification-tag';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: tag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
vue.config.js
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
pwa: {
|
||||||
|
workboxOptions: {
|
||||||
|
importScripts: ['sw-dev.ts']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once we have the service worker registered and the ServiceWorkerRegistration is
|
||||||
|
returned, we then have access to a `pushManager` property object. This property
|
||||||
|
allows us to continue with the web push work flow.
|
||||||
|
|
||||||
|
In the next step, BROWSER requests a data structure from SERVICE called a VAPID
|
||||||
|
(Voluntary Application Server Identification) which is the public key from a
|
||||||
|
key-pair.
|
||||||
|
|
||||||
|
The VAPID is a specification used to identify the application server (i.e. the
|
||||||
|
SERVICE server) that is sending push messages through a push PROVIDER. It's an
|
||||||
|
authentication mechanism that allows the server to demonstrate its identity to
|
||||||
|
the push PROVIDER, by use of a public and private key pair. These keys are used
|
||||||
|
by the SERVICE in encrypting messages being sent to the BROWSER, as well as
|
||||||
|
being used by the BROWSER in decrypting the messages coming from the SERVICE.
|
||||||
|
|
||||||
|
The VAPID (Voluntary Application Server Identification) key provides more
|
||||||
|
security and authenticity for web push notifications in the following ways:
|
||||||
|
|
||||||
|
Identifying the Application Server:
|
||||||
|
|
||||||
|
The VAPID key is used to identify the application server that is sending
|
||||||
|
the push notifications. This ensures that the push notifications are
|
||||||
|
authentic and not sent by a malicious third party.
|
||||||
|
|
||||||
|
Encrypting the Messages:
|
||||||
|
|
||||||
|
The VAPID key is used to sign the push notifications sent by the
|
||||||
|
application server, ensuring that they are not tampered with during
|
||||||
|
transmission. This provides an additional layer of security and
|
||||||
|
authenticity for the push notifications.
|
||||||
|
|
||||||
|
Adding Contact Information:
|
||||||
|
|
||||||
|
The VAPID key allows a web application to add contact information to
|
||||||
|
the push messages sent to the browser push service. This enables the
|
||||||
|
push service to contact the application server in case of need or
|
||||||
|
provide additional debug information about the push messages.
|
||||||
|
|
||||||
|
Improving Delivery Rates:
|
||||||
|
|
||||||
|
Using the VAPID key can help improve the overall performance of web push
|
||||||
|
notifications, specifically improving delivery rates. By streamlining the
|
||||||
|
delivery process, the chance of delivery errors along the way is lessened.
|
||||||
|
|
||||||
|
If the BROWSER accepts and grants permission to subscribe to receiving from the
|
||||||
|
SERVICE Web Push messages, then the BROWSER makes a subscription request to
|
||||||
|
PROVIDER which creates and stores a special URL for that BROWSER.
|
||||||
|
|
||||||
|
Here's a bit of code describing the above process:
|
||||||
|
|
||||||
|
```
|
||||||
|
// b64 is the VAPID
|
||||||
|
b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U';
|
||||||
|
const applicationServerKey = urlBase64ToUint8Array(b64);
|
||||||
|
const options: PushSubscriptionOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey
|
||||||
|
};
|
||||||
|
|
||||||
|
registration.pushManager.subscribe(options)
|
||||||
|
.then(function(subscription) {
|
||||||
|
console.log('Push subscription successful:', subscription);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Push subscription failed:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `applicationServerKey` variable contains the VAPID public
|
||||||
|
key, which is converted to a `Uint8Array` using a function such as this:
|
||||||
|
|
||||||
|
```
|
||||||
|
export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array {
|
||||||
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
|
||||||
|
const rawData = atobFn(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The options object is of type `PushSubscriptionOptions`, which includes the
|
||||||
|
`userVisibleOnly` and `applicationServerKey` (ie VAPID public key) properties.
|
||||||
|
|
||||||
|
options: An object that contains the options used for creating the
|
||||||
|
subscription. This object itself has the following sub-properties:
|
||||||
|
|
||||||
|
applicationServerKey: A public key your push service uses for application
|
||||||
|
server identification. This is normally a Uint8Array.
|
||||||
|
|
||||||
|
userVisibleOnly: A boolean value indicating that the push messages that
|
||||||
|
are sent should be made visible to the user through a notification.
|
||||||
|
This is often set to true.
|
||||||
|
|
||||||
|
The subscribe() method returns a `Promise` that resolves to a `PushSubscription`
|
||||||
|
object containing details of the subscription, such as the endpoint URL and the
|
||||||
|
public key. The returned data would have a form like this:
|
||||||
|
|
||||||
|
{
|
||||||
|
"endpoint": "https://some.pushservice.com/some/unique/identifier",
|
||||||
|
"expirationTime": null,
|
||||||
|
"keys": {
|
||||||
|
"p256dh": "some_base64_encoded_string",
|
||||||
|
"auth": "some_other_base64_encoded_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint: A string representing the endpoint URL for the push service. This
|
||||||
|
URL is essentially the push service address to which the push message would
|
||||||
|
be sent for this particular subscription.
|
||||||
|
|
||||||
|
expirationTime: A DOMHighResTimeStamp (which is basically a number or null)
|
||||||
|
representing the subscription's expiration time in milliseconds since
|
||||||
|
01 January, 1970 UTC. This can be null if the subscription never expires.
|
||||||
|
|
||||||
|
The BROWSER will, internally, then use that URL to check for incoming messages
|
||||||
|
by way of the service worker we described earlier. The BROWSER also sends this
|
||||||
|
URL back to SERVICE which will use that URL to send messages to the BROWSER via
|
||||||
|
the PROVIDER.
|
||||||
|
|
||||||
|
Ultimately, the actual internal process of receiving messages varies from BROWSER
|
||||||
|
to BROWSER. Approaches vary from long-polling HTTP connections to WebSockets. A
|
||||||
|
lot of handwaving and voodoo magic. The bottom line is that the BROWSER itself
|
||||||
|
manages the connection to the PROVIDER whilst the SERVICE must send messages
|
||||||
|
via the PROVIDER so that they reach the BROWSER service worker.
|
||||||
|
|
||||||
|
Just to remind us that in our service worker our code for receiving messages
|
||||||
|
will look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
self.addEventListener('push', function(event: PushEvent) {
|
||||||
|
console.log('Received a push message', event);
|
||||||
|
|
||||||
|
const title = 'Push message';
|
||||||
|
const body = 'The message body';
|
||||||
|
const icon = '/images/icon-192x192.png';
|
||||||
|
const tag = 'simple-push-demo-notification-tag';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: tag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Now to address the issue of receiving notification messages on mobile devices.
|
||||||
|
It should be noted that Web Push messages are only received when BROWSER is
|
||||||
|
open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS, the
|
||||||
|
mobile application (in our case a PWA) must be added to the Home Screen and
|
||||||
|
permissions must be explicitly granted that allow the application to receive
|
||||||
|
push notifications. Further, with an iOS device the user must enable wake on
|
||||||
|
notification to have their device light-up when it receives a notification
|
||||||
|
(https://support.apple.com/enus/HT208081).
|
||||||
|
|
||||||
|
So what about #4? - The INTERMEDIARY. Well, It is possible under very special
|
||||||
|
circumstances to create your own Web Push PROVIDER. The only case I've found so
|
||||||
|
far relates to making an Android Custom ROM. (An Android Custom ROM is a
|
||||||
|
customized version of the Android Operating System.) There are open source
|
||||||
|
IMTERMEDIARY products such as UnifiedPush (https://unifiedpush.org/) which can
|
||||||
|
fulfill this role. If you are using iOS you are not permitted to make or use
|
||||||
|
your own custom Web Push PROVIDER. Apple will never allow anyone to do that.
|
||||||
|
Apple has none of its own.
|
||||||
|
|
||||||
|
It is, however, possible to have a sort of proxy working between your SERVICE
|
||||||
|
and FCM (or iOS). Services that mash up various Push notification services (like
|
||||||
|
OneSignal) can perform in the role of such proxies.
|
||||||
|
|
||||||
|
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our
|
||||||
|
time on.
|
||||||
Reference in New Issue
Block a user