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.

219 lines
6.4 KiB

<template>
<QuickNav selected="Invite" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 class="text-4xl text-center font-light">Invitations</h1>
<!-- New Project -->
<button
v-if="isRegistered"
class="fixed right-6 top-12 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
@click="createInvite()"
>
<fa icon="plus" class="fa-fw"></fa>
</button>
<InviteDialog ref="inviteDialog" />
<!-- Invites Table -->
<div v-if="invites.length" class="mt-6">
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">ID</th>
<th class="py-2">Notes</th>
<th class="py-2">Expires At</th>
<th class="py-2">Redeemed By</th>
</tr>
</thead>
<tbody>
<tr
v-for="invite in invites"
:key="invite.inviteIdentifier"
class="border-t"
>
<td
class="py-2 text-center text-blue-500"
@click="copyInviteAndNotify(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</td>
<td class="py-2 text-left">{{ invite.notes }}</td>
<td class="py-2 text-center">
{{ invite.expiresAt.substring(0, 10) }}
</td>
<td class="py-2 text-center">
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
</td>
</tr>
</tbody>
</table>
</div>
<p v-else class="mt-6 text-center">No invites found.</p>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { db, retrieveSettingsForActiveAccount } from "../db";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import InviteDialog from "@/components/InviteDialog.vue";
import { APP_SERVER, NotificationIface } from "@/constants/app";
import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
interface Invite {
inviteIdentifier: string;
expiresAt: string;
jwt: string;
notes: string;
redeemedBy: string | null;
}
@Component({
components: { QuickNav, TopMessage, InviteDialog },
})
export default class InviteOneView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
invites: Invite[] = [];
activeDid: string = "";
apiServer: string = "";
isRegistered: boolean = false;
async mounted() {
try {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
const headers = await getHeaders(this.activeDid);
const response = await axios.get(
this.apiServer + "/api/userUtil/invite",
{ headers },
);
this.invites = response.data.data;
} catch (error) {
console.error("Error fetching invites:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Load Error",
text: "Got an error loading your invites.",
},
5000,
);
}
}
getTruncatedInviteId(inviteId: string): string {
if (inviteId.length <= 9) return inviteId;
return `${inviteId.slice(0, 3)}...${inviteId.slice(-3)}`;
}
getTruncatedRedeemedBy(redeemedBy: string | null): string {
if (!redeemedBy) return "";
if (redeemedBy.length <= 19) return redeemedBy;
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
}
copyInviteAndNotify(jwt: string) {
const link = APP_SERVER + "/contacts?inviteJwt=" + jwt;
useClipboard().copy(link);
this.$notify(
{
group: "alert",
type: "success",
title: "Copied",
text: "Invitation link is copied to clipboard.",
},
3000,
);
}
async createInvite() {
(this.$refs.inviteDialog as InviteDialog).open(
"Invitation Note",
`These notes are only for your use, to make comments for a link to recall later if redeemed by someone.
Note that this is sent to the server.`,
async (notes, expiresAt) => {
try {
const inviteIdentifier =
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2);
const headers = await getHeaders(this.activeDid);
if (!expiresAt) {
throw {
response: {
data: { error: "You must select an expiration date." },
},
};
}
const expiresIn = (new Date(expiresAt).getTime() - Date.now()) / 1000;
const inviteJwt = await createInviteJwt(
this.activeDid,
undefined,
inviteIdentifier,
expiresIn,
);
await axios.post(
this.apiServer + "/api/userUtil/invite",
{ inviteJwt: inviteJwt, notes: notes },
{ headers },
);
this.invites.push({
inviteIdentifier: inviteIdentifier,
expiresAt: expiresAt,
jwt: inviteJwt,
notes: notes,
redeemedBy: null,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error creating invite:", error);
let message = "Got an error creating your invite.";
if (
error.response &&
error.response.data &&
error.response.data.error
) {
if (error.response.data.error.message) {
message = error.response.data.error.message;
} else {
message = error.response.data.error;
}
}
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Creating Invite",
text: message,
},
5000,
);
}
},
);
}
}
</script>