timesafari
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
12 KiB

<template>
<QuickNav selected="Projects"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
2 years ago
<!-- Cancel -->
<router-link
:to="{ name: 'project' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Edit Idea
</h1>
</div>
<!-- Project Details -->
<!-- Image - (see design model) Empty -->
2 years ago
<div>
{{ errorMessage }}
</div>
<input
type="text"
placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="fullClaim.name"
/>
<input
type="text"
placeholder="Other Authorized Representative"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="agentDid"
/>
<div class="mb-4">
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
<span class="text-red-500">Beware!</span>
If you save this, the original project owner will no longer be able to
edit it.
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
Click here to make the original owner an authorized representative.
</button>
</p>
</div>
<textarea
placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5"
v-model="fullClaim.description"
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ fullClaim.description?.length }}/5000 max. characters
</div>
12 months ago
<input
v-model="fullClaim.url"
placeholder="Website"
autocapitalize="none"
12 months ago
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="flex items-center mb-4">
<input
type="checkbox"
class="mr-2"
v-model="includeLocation"
@click="includeLocation = !includeLocation"
/>
<label for="includeLocation">Include Location</label>
</div>
8 months ago
<div v-if="includeLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500">
For your security, choose a location nearby but not exactly at the
place.
8 months ago
</p>
<l-map
ref="map"
v-model:zoom="zoom"
:center="[0, 0]"
8 months ago
class="!z-40 rounded-md"
@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">
<button
:disabled="isHiddenSave"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="onSaveProjectClick()"
>
<!-- SHOW if in idle state -->
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }">
<!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving...</span
>
</button>
<button
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
@Component({
components: { LMap, LMarker, LTileLayer, QuickNav },
})
export default class NewEditProjectView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
agentDid = "";
apiServer = "";
errorMessage = "";
fullClaim: PlanVerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: "",
description: "",
12 months ago
}; // this default is only to avoid errors before plan is loaded
includeLocation = false;
isHiddenSave = false;
isHiddenSpinner = true;
lastClaimJwtId = "";
latitude = 0;
longitude = 0;
numAccounts = 0;
projectId = localStorage.getItem("projectId") || "";
projectIssuerDid = "";
zoom = 2;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
if (this.projectId) {
if (this.numAccounts === 0) {
console.error("Error: no account was found.");
} else {
const identity = await this.getIdentity(this.activeDid);
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
this.loadProject(identity);
}
}
}
async loadProject(identity: IIdentifier) {
const url =
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.projectIssuerDid = resp.data.issuer;
this.fullClaim = resp.data.claim;
this.lastClaimJwtId = resp.data.id;
if (this.fullClaim?.location) {
this.includeLocation = true;
this.latitude = this.fullClaim.location.geo.latitude;
this.longitude = this.fullClaim.location.geo.longitude;
}
if (this.fullClaim?.agent?.identifier) {
this.agentDid = this.fullClaim.agent.identifier;
}
}
} catch (error) {
console.error("Got error retrieving that project", error);
}
}
2 years ago
private async saveProject(identity: IIdentifier) {
// Make a claim
const vcClaim: PlanVerifiableCredential = this.fullClaim;
if (this.projectId) {
vcClaim.lastClaimId = this.lastClaimJwtId;
}
if (this.agentDid) {
vcClaim.agent = {
identifier: this.agentDid,
};
} else {
delete vcClaim.agent;
}
if (this.includeLocation) {
vcClaim.location = {
geo: {
"@type": "GeoCoordinates",
latitude: this.latitude,
longitude: this.longitude,
},
};
} else {
delete vcClaim.location;
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// create a signature using private key of identity
if (identity.keys[0].privateKeyHex != null) {
const privateKeyHex: string = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
2 years ago
const alg = undefined;
// create a JWT for the request
2 years ago
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
2 years ago
});
2 years ago
// Make the xhr request payload
2 years ago
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
2 years ago
const token = await accessToken(identity);
2 years ago
const headers = {
2 years ago
"Content-Type": "application/json",
Authorization: "Bearer " + token,
2 years ago
};
2 years ago
try {
2 years ago
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) {
this.errorMessage = "";
useAppStore()
.setProjectId(resp.data.success.handleId)
.then(() => {
this.$router.push({ name: "project" });
});
} else {
console.error(
"Got unexpected 'data' inside response from server",
resp,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Saving Idea",
text: "Server did not save the idea. Try again.",
},
-1,
);
}
} catch (error) {
let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError<{
error?: { message?: string };
}>;
if (serverError) {
console.error("Got error from server", serverError);
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
userMessage =
(serverError.response?.data?.error?.message as string) ||
userMessage;
this.$notify(
{
group: "alert",
type: "danger",
title: "User Message",
text: userMessage,
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Message",
text: JSON.stringify(serverError.toJSON()),
},
-1,
);
}
} else {
console.error(
"Here's the full error trying to save the claim:",
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Claim Error",
text: error as string,
},
-1,
);
}
// Now set that error for the user to see.
this.errorMessage = userMessage;
2 years ago
}
}
}
2 years ago
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
if (this.numAccounts === 0) {
console.error("Error: there is no account.");
} else {
const identity = await this.getIdentity(this.activeDid);
this.saveProject(identity);
}
}
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() {
this.$router.back();
}
}
</script>