Browse Source

add ability to import from Endorser Mobile CSV

starred-projects
Trent Larson 8 months ago
parent
commit
f253f0af0f
  1. 2
      project.task.yaml
  2. 2
      src/libs/endorserServer.ts
  3. 94
      src/views/ContactsView.vue

2
project.task.yaml

@ -1,6 +1,7 @@
tasks: tasks:
- anchor hash into BTC
- image on give - image on give
- Show a camera to take a picture - Show a camera to take a picture
- Scale the image to a reasonable size - Scale the image to a reasonable size
@ -11,7 +12,6 @@ tasks:
- Rates - images erased? - Rates - images erased?
- image not associated with JWT ULID since that's assigned later - image not associated with JWT ULID since that's assigned later
- mark a project as inactive - mark a project as inactive
- make a shortcut for BVC
- add share button for sending a message to confirmers when we can't see the claim - add share button for sending a message to confirmers when we can't see the claim
- add TimeSafari as a shareable app https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target - add TimeSafari as a shareable app https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
- choose a project's alternative agent ("authorized representative") via a contact chooser (not just copy-paste a DID) - choose a project's alternative agent ("authorized representative") via a contact chooser (not just copy-paste a DID)

2
src/libs/endorserServer.ts

@ -8,6 +8,8 @@ import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
// the header line for contacts exported via Endorser Mobile
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
// the prefix for the contact URL // the prefix for the contact URL
export const CONTACT_URL_PREFIX = "https://endorser.ch"; export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL // the suffix for the contact URL

94
src/views/ContactsView.vue

@ -26,10 +26,10 @@
> >
<fa icon="qrcode" class="fa-fw text-2xl" /> <fa icon="qrcode" class="fa-fw text-2xl" />
</router-link> </router-link>
<input <textarea
type="text" type="text"
placeholder="URL or DID, Name, Public Key, Next Public Key Hash" placeholder="URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
v-model="contactInput" v-model="contactInput"
/> />
<button <button
@ -297,6 +297,7 @@ import {
SimpleSigner, SimpleSigner,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { import {
CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiveServerRecord, GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
@ -307,6 +308,7 @@ import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { IndexableType } from "dexie";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@ -365,7 +367,7 @@ export default class ContactsView extends Vue {
); );
if (this.contactEndorserUrl) { if (this.contactEndorserUrl) {
await this.newContactFromScan(this.contactEndorserUrl); await this.addContactFromScan(this.contactEndorserUrl);
localStorage.removeItem("contactEndorserUrl"); localStorage.removeItem("contactEndorserUrl");
this.contactEndorserUrl = ""; this.contactEndorserUrl = "";
} }
@ -535,7 +537,46 @@ export default class ContactsView extends Vue {
} }
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) { if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
await this.newContactFromScan(this.contactInput); await this.addContactFromScan(this.contactInput);
return;
}
if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) {
const lines = this.contactInput.split(/\n/);
const lineAdded = [];
for (const line of lines) {
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
continue;
}
lineAdded.push(this.addContactFromEndorserMobileLine(line));
}
try {
await Promise.all(lineAdded);
this.$notify(
{
group: "alert",
type: "success",
title: "Contacts Added",
text: "Each contact was added. Nothing was sent to the server.",
},
-1, // keeping it up so that the "visibility" message is seen
);
} catch (e) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Contacts Maybe Added",
text: "An error occurred. Some contacts may have been added.",
},
-1,
);
}
const allContacts = await db.contacts.toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
return; return;
} }
@ -576,7 +617,48 @@ export default class ContactsView extends Vue {
await this.addContact(newContact); await this.addContact(newContact);
} }
async newContactFromScan(url: string): Promise<void> { async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
// Note that Endorser Mobile puts name first, then did, etc.
let name = line;
let did = "";
let publicKeyInput, seesMe, registered;
const commaPos1 = line.indexOf(",");
if (commaPos1 > -1) {
name = line.substring(0, commaPos1).trim();
did = line.substring(commaPos1 + 1).trim();
const commaPos2 = line.indexOf(",", commaPos1 + 1);
if (commaPos2 > -1) {
did = line.substring(commaPos1 + 1, commaPos2).trim();
publicKeyInput = line.substring(commaPos2 + 1).trim();
const commaPos3 = line.indexOf(",", commaPos2 + 1);
if (commaPos3 > -1) {
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
seesMe = line.substring(commaPos3 + 1).trim() == "true";
const commaPos4 = line.indexOf(",", commaPos3 + 1);
if (commaPos4 > -1) {
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
registered = line.substring(commaPos4 + 1).trim() == "true";
}
}
}
}
// help with potential mistakes while this sharing requires copy-and-paste
let publicKeyBase64 = publicKeyInput;
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = {
did,
name,
publicKeyBase64,
seesMe,
registered,
};
return db.contacts.add(newContact);
}
async addContactFromScan(url: string): Promise<void> {
const payload = getContactPayloadFromJwtUrl(url); const payload = getContactPayloadFromJwtUrl(url);
if (!payload) { if (!payload) {
this.$notify( this.$notify(
@ -661,7 +743,7 @@ export default class ContactsView extends Vue {
} }
if (err.name === "ConstraintError") { if (err.name === "ConstraintError") {
message += message +=
"Check that the contact doesn't conflict with any you already have."; " Check that the contact doesn't conflict with any you already have.";
} }
this.$notify( this.$notify(
{ {

Loading…
Cancel
Save