|
|
|
<template>
|
|
|
|
<!-- CONTENT -->
|
|
|
|
<section id="Content" class="p-6 pb-24">
|
|
|
|
<!-- 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>
|
|
|
|
[New/Edit] Plan
|
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Project Details -->
|
|
|
|
<!-- Image - (see design model) Empty -->
|
|
|
|
|
|
|
|
<div>
|
|
|
|
{{ errorMessage }}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
placeholder="Project Name"
|
|
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
|
|
v-model="projectName"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
placeholder="Description"
|
|
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
|
|
rows="5"
|
|
|
|
v-model="description"
|
|
|
|
maxlength="500"
|
|
|
|
></textarea>
|
|
|
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
|
|
|
{{ description.length }}/500 max. characters
|
|
|
|
</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 }"
|
|
|
|
><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>
|
|
|
|
<div v-bind:class="computedAlertClassNames()">
|
|
|
|
<button
|
|
|
|
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
|
|
@click="onClickClose()"
|
|
|
|
>
|
|
|
|
<fa icon="xmark"></fa>
|
|
|
|
</button>
|
|
|
|
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
|
|
<p>{{ alertMessage }}</p>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import { Options, Vue } from "vue-class-component";
|
|
|
|
import { AppString } from "@/constants/app";
|
|
|
|
import { db } from "../db";
|
|
|
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
|
|
|
import * as didJwt from "did-jwt";
|
|
|
|
import { IIdentifier } from "@veramo/core";
|
|
|
|
import { useAppStore } from "@/store/app";
|
|
|
|
import { AxiosError } from "axios";
|
|
|
|
|
|
|
|
interface VerifiableCredential {
|
|
|
|
"@context": string;
|
|
|
|
"@type": string;
|
|
|
|
name: string;
|
|
|
|
description: string;
|
|
|
|
identifier?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Options({
|
|
|
|
components: {},
|
|
|
|
})
|
|
|
|
export default class NewEditProjectView extends Vue {
|
|
|
|
projectName = "";
|
|
|
|
description = "";
|
|
|
|
errorMessage = "";
|
|
|
|
alertTitle = "";
|
|
|
|
alertMessage = "";
|
|
|
|
|
|
|
|
projectId = localStorage.getItem("projectId") || "";
|
|
|
|
isHiddenSave = false;
|
|
|
|
isHiddenSpinner = true;
|
|
|
|
|
|
|
|
// 'created' hook runs when the Vue instance is first created
|
|
|
|
async created() {
|
|
|
|
if (this.projectId === "") {
|
|
|
|
console.log("This is a new project");
|
|
|
|
} else {
|
|
|
|
await db.open();
|
|
|
|
const num_accounts = await db.accounts.count();
|
|
|
|
if (num_accounts === 0) {
|
|
|
|
console.log("Problem! Should have a profile!");
|
|
|
|
} else {
|
|
|
|
const accounts = await db.accounts.toArray();
|
|
|
|
const identity = JSON.parse(accounts[0].identity);
|
|
|
|
this.LoadProject(identity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async LoadProject(identity: IIdentifier) {
|
|
|
|
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
|
|
|
|
// eslint-disable-next-line prettier/prettier
|
|
|
|
const url = endorserApiServer + "/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 });
|
|
|
|
console.log(resp.status, resp.data);
|
|
|
|
if (resp.status === 200) {
|
|
|
|
const claim = resp.data.claim;
|
|
|
|
this.projectName = claim.name;
|
|
|
|
this.description = claim.description;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.log(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async SaveProject(identity: IIdentifier) {
|
|
|
|
// Make a claim
|
|
|
|
const vcClaim: VerifiableCredential = {
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
"@type": "PlanAction",
|
|
|
|
name: this.projectName,
|
|
|
|
description: this.description,
|
|
|
|
identifier: this.projectId || undefined,
|
|
|
|
};
|
|
|
|
if (this.projectId) {
|
|
|
|
vcClaim.identifier = this.projectId;
|
|
|
|
}
|
|
|
|
// Make a payload for the claim
|
|
|
|
const vcPayload = {
|
|
|
|
sub: "PlanAction",
|
|
|
|
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 !== "undefined" &&
|
|
|
|
identity.keys[0].privateKeyHex !== null
|
|
|
|
) {
|
|
|
|
// eslint-disable-next-line
|
|
|
|
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 endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
|
|
|
|
const url = endorserApiServer + "/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 });
|
|
|
|
console.log("Got resp data:", resp.data);
|
|
|
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
|
|
|
// version shows up here: https://endorser.ch:3000/api-docs/
|
|
|
|
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
|
|
|
this.errorMessage = "";
|
|
|
|
this.alertTitle = "";
|
|
|
|
this.alertMessage = "";
|
|
|
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
|
|
|
// version shows up here: https://endorser.ch:3000/api-docs/
|
|
|
|
useAppStore().setProjectId(
|
|
|
|
resp.data.success.handleId || resp.data.success.fullIri
|
|
|
|
);
|
|
|
|
setTimeout(
|
|
|
|
function (that: Vue) {
|
|
|
|
const route = {
|
|
|
|
name: "project",
|
|
|
|
};
|
|
|
|
console.log(route);
|
|
|
|
that.$router.push(route);
|
|
|
|
},
|
|
|
|
2000,
|
|
|
|
this
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
let userMessage = "There was an error. See logs for more info.";
|
|
|
|
const serverError = error as AxiosError;
|
|
|
|
if (serverError) {
|
|
|
|
this.isAlertVisible = true;
|
|
|
|
if (serverError.message) {
|
|
|
|
this.alertTitle = "User Message";
|
|
|
|
userMessage = serverError.message; // This is info for the user.
|
|
|
|
this.alertMessage = userMessage;
|
|
|
|
} else {
|
|
|
|
this.alertTitle = "Server Message";
|
|
|
|
this.alertMessage = JSON.stringify(serverError.toJSON());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log("Here's the full error trying to save the claim:", error);
|
|
|
|
this.alertTitle = "Claim Error";
|
|
|
|
this.alertMessage = error as string;
|
|
|
|
}
|
|
|
|
// Now set that error for the user to see.
|
|
|
|
this.errorMessage = userMessage;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public onClickClose() {
|
|
|
|
this.isAlertVisible = false;
|
|
|
|
this.alertTitle = "";
|
|
|
|
this.alertMessage = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
public async onSaveProjectClick() {
|
|
|
|
this.isHiddenSave = true;
|
|
|
|
this.isHiddenSpinner = false;
|
|
|
|
await db.open();
|
|
|
|
const num_accounts = await db.accounts.count();
|
|
|
|
if (num_accounts === 0) {
|
|
|
|
console.log("Problem! Should have a profile!");
|
|
|
|
} else {
|
|
|
|
const accounts = await db.accounts.toArray();
|
|
|
|
const identity = JSON.parse(accounts[0].identity);
|
|
|
|
this.SaveProject(identity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public onCancelClick() {
|
|
|
|
this.$router.back();
|
|
|
|
}
|
|
|
|
|
|
|
|
isAlertVisible = false;
|
|
|
|
public computedAlertClassNames() {
|
|
|
|
return {
|
|
|
|
hidden: !this.isAlertVisible,
|
|
|
|
"dismissable-alert": true,
|
|
|
|
"bg-slate-100": true,
|
|
|
|
"p-5": true,
|
|
|
|
rounded: true,
|
|
|
|
"drop-shadow-lg": true,
|
|
|
|
absolute: true,
|
|
|
|
"top-3": true,
|
|
|
|
"inset-x-3": true,
|
|
|
|
"transition-transform": true,
|
|
|
|
"ease-in": true,
|
|
|
|
"duration-300": true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|