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.
		
		
		
		
		
			
		
			
				
					
					
						
							409 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							409 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">
							 | 
						|
								        <!-- 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 -->
							 | 
						|
								
							 | 
						|
								    <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>
							 | 
						|
								
							 | 
						|
								    <input
							 | 
						|
								      v-model="fullClaim.url"
							 | 
						|
								      placeholder="Website"
							 | 
						|
								      autocapitalize="none"
							 | 
						|
								      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>
							 | 
						|
								    <div v-if="includeLocation" style="height: 600px; width: 800px">
							 | 
						|
								      <div class="px-2 py-2">
							 | 
						|
								        For your security, choose a location nearby but not exactly at the
							 | 
						|
								        place.
							 | 
						|
								      </div>
							 | 
						|
								
							 | 
						|
								      <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">
							 | 
						|
								      <button
							 | 
						|
								        :disabled="isHiddenSave"
							 | 
						|
								        class="block w-full text-center text-lg font-bold uppercase bg-blue-600 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-slate-500 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: "",
							 | 
						|
								  }; // 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);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  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,
							 | 
						|
								      };
							 | 
						|
								    }
							 | 
						|
								    if (this.includeLocation) {
							 | 
						|
								      vcClaim.location = {
							 | 
						|
								        geo: {
							 | 
						|
								          "@type": "GeoCoordinates",
							 | 
						|
								          latitude: this.latitude,
							 | 
						|
								          longitude: this.longitude,
							 | 
						|
								        },
							 | 
						|
								      };
							 | 
						|
								    }
							 | 
						|
								    // 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);
							 | 
						|
								      const alg = undefined;
							 | 
						|
								      // create a JWT for the request
							 | 
						|
								      const vcJwt: string = await didJwt.createJWT(vcPayload, {
							 | 
						|
								        alg: alg,
							 | 
						|
								        issuer: identity.did,
							 | 
						|
								        signer: signer,
							 | 
						|
								      });
							 | 
						|
								
							 | 
						|
								      // Make the xhr request payload
							 | 
						|
								
							 | 
						|
								      const payload = JSON.stringify({ jwtEncoded: vcJwt });
							 | 
						|
								      const url = this.apiServer + "/api/v2/claim";
							 | 
						|
								      const token = await accessToken(identity);
							 | 
						|
								      const headers = {
							 | 
						|
								        "Content-Type": "application/json",
							 | 
						|
								        Authorization: "Bearer " + token,
							 | 
						|
								      };
							 | 
						|
								
							 | 
						|
								      try {
							 | 
						|
								        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;
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  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>
							 | 
						|
								
							 |