7 changed files with 616 additions and 60 deletions
@ -0,0 +1,197 @@ |
|||
<template> |
|||
<QuickNav selected="Contacts" /> |
|||
<TopMessage /> |
|||
|
|||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> |
|||
<!-- Heading --> |
|||
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8"> |
|||
Onboarding Meetings |
|||
</h1> |
|||
|
|||
<!-- Loading State --> |
|||
<div v-if="isLoading" class="flex justify-center items-center py-8"> |
|||
<fa icon="spinner" class="fa-spin-pulse" /> |
|||
</div> |
|||
|
|||
<!-- Meeting List --> |
|||
<div v-else class="space-y-4"> |
|||
<div v-for="meeting in meetings" :key="meeting.groupId" |
|||
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer" |
|||
@click="promptPassword(meeting)"> |
|||
<h2 class="text-xl font-medium">{{ meeting.name }}</h2> |
|||
<p class="text-sm text-gray-600">Group ID: {{ meeting.groupId }}</p> |
|||
</div> |
|||
|
|||
<p v-if="meetings.length === 0" class="text-center text-gray-500 py-8"> |
|||
No onboarding meetings available |
|||
</p> |
|||
</div> |
|||
|
|||
<!-- Password Dialog --> |
|||
<div v-if="showPasswordDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"> |
|||
<div class="bg-white rounded-lg p-6 max-w-sm w-full"> |
|||
<h3 class="text-lg font-medium mb-4">Enter Meeting Password</h3> |
|||
<input |
|||
v-model="password" |
|||
type="text" |
|||
class="w-full px-3 py-2 border rounded-md mb-4" |
|||
placeholder="Enter password" |
|||
@keyup.enter="submitPassword" |
|||
/> |
|||
<div class="flex justify-end space-x-4"> |
|||
<button |
|||
@click="cancelPasswordDialog" |
|||
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300" |
|||
> |
|||
Cancel |
|||
</button> |
|||
<button |
|||
@click="submitPassword" |
|||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" |
|||
> |
|||
Submit |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue } from 'vue-facing-decorator'; |
|||
import QuickNav from '@/components/QuickNav.vue'; |
|||
import TopMessage from '@/components/TopMessage.vue'; |
|||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; |
|||
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; |
|||
import { encryptMessage } from '@/libs/crypto'; |
|||
|
|||
interface Meeting { |
|||
name: string; |
|||
groupId: number; |
|||
} |
|||
|
|||
@Component({ |
|||
components: { |
|||
QuickNav, |
|||
TopMessage, |
|||
}, |
|||
}) |
|||
export default class OnboardMeetingListView extends Vue { |
|||
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; |
|||
|
|||
meetings: Meeting[] = []; |
|||
isLoading = false; |
|||
showPasswordDialog = false; |
|||
password = ''; |
|||
selectedMeeting: Meeting | null = null; |
|||
activeDid = ''; |
|||
apiServer = ''; |
|||
firstName = ''; |
|||
|
|||
async created() { |
|||
const settings = await retrieveSettingsForActiveAccount(); |
|||
this.activeDid = settings.activeDid || ''; |
|||
this.apiServer = settings.apiServer || ''; |
|||
this.firstName = settings.firstName || ''; |
|||
await this.fetchMeetings(); |
|||
} |
|||
|
|||
async fetchMeetings() { |
|||
this.isLoading = true; |
|||
try { |
|||
const headers = await getHeaders(this.activeDid); |
|||
const response = await this.axios.get( |
|||
this.apiServer + '/api/partner/groupsOnboarding', |
|||
{ headers } |
|||
); |
|||
|
|||
if (response.data && response.data.data) { |
|||
this.meetings = response.data.data; |
|||
} |
|||
} catch (error) { |
|||
logConsoleAndDb('Error fetching meetings: ' + errorStringForLog(error), true); |
|||
this.$notify( |
|||
{ |
|||
group: 'alert', |
|||
type: 'danger', |
|||
title: 'Error', |
|||
text: serverMessageForUser(error) || 'Failed to fetch meetings.', |
|||
}, |
|||
5000 |
|||
); |
|||
} finally { |
|||
this.isLoading = false; |
|||
} |
|||
} |
|||
|
|||
promptPassword(meeting: Meeting) { |
|||
this.password = ''; |
|||
this.selectedMeeting = meeting; |
|||
this.showPasswordDialog = true; |
|||
} |
|||
|
|||
cancelPasswordDialog() { |
|||
this.password = ''; |
|||
this.selectedMeeting = null; |
|||
this.showPasswordDialog = false; |
|||
} |
|||
|
|||
async submitPassword() { |
|||
if (!this.selectedMeeting) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
// Create member data object |
|||
const memberData = { |
|||
name: this.firstName, |
|||
did: this.activeDid |
|||
}; |
|||
const memberDataString = JSON.stringify(memberData); |
|||
const encryptedMemberData = await encryptMessage(memberDataString, this.password); |
|||
|
|||
// Get headers for authentication |
|||
const headers = await getHeaders(this.activeDid); |
|||
|
|||
// Encrypt the member data |
|||
const postResult = await this.axios.post( |
|||
this.apiServer + '/api/partner/groupOnboardMember', |
|||
{ |
|||
groupId: this.selectedMeeting.groupId, |
|||
content: encryptedMemberData |
|||
}, |
|||
{ headers } |
|||
); |
|||
|
|||
if (postResult.data && postResult.data.success) { |
|||
// Navigate to members view with password and groupId |
|||
this.$router.push({ |
|||
name: 'onboard-meeting-members', |
|||
params: { |
|||
groupId: this.selectedMeeting.groupId.toString() |
|||
}, |
|||
query: { |
|||
password: this.password, |
|||
memberId: postResult.data.memberId |
|||
} |
|||
}); |
|||
|
|||
this.cancelPasswordDialog(); |
|||
} else { |
|||
throw { response: postResult }; |
|||
} |
|||
} catch (error) { |
|||
logConsoleAndDb('Error joining meeting: ' + errorStringForLog(error), true); |
|||
this.$notify( |
|||
{ |
|||
group: 'alert', |
|||
type: 'danger', |
|||
title: 'Error', |
|||
text: serverMessageForUser(error) || 'Failed to join meeting.', |
|||
}, |
|||
5000 |
|||
); |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,157 @@ |
|||
<template> |
|||
<QuickNav selected="Contacts" /> |
|||
<TopMessage /> |
|||
|
|||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> |
|||
<!-- Heading --> |
|||
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8"> |
|||
Meeting Members |
|||
</h1> |
|||
|
|||
<!-- Back Button --> |
|||
<button |
|||
@click="$router.back()" |
|||
class="mb-6 flex items-center text-blue-600 hover:text-blue-800" |
|||
> |
|||
<fa icon="arrow-left" class="mr-2" /> |
|||
Back to Meetings |
|||
</button> |
|||
|
|||
<!-- Loading State --> |
|||
<div v-if="isLoading" class="flex justify-center items-center py-8"> |
|||
<fa icon="spinner" class="fa-spin-pulse" /> |
|||
</div> |
|||
|
|||
<!-- Error State --> |
|||
<div v-else-if="errorMessage"> |
|||
<div class="text-center text-red-600 py-8"> |
|||
{{ errorMessage }} |
|||
</div> |
|||
<div class="text-center"> |
|||
For authorization, wait for your meeting organizer to approve you. |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Members List --> |
|||
<div v-else class="space-y-4"> |
|||
<div v-for="member in decryptedMembers" :key="member.memberId" |
|||
class="p-4 bg-white rounded-lg shadow"> |
|||
<h2 class="text-xl font-medium">{{ member.name }}</h2> |
|||
<p class="text-sm text-gray-600">DID: {{ member.did }}</p> |
|||
</div> |
|||
|
|||
<p v-if="decryptedMembers.length === 0 && !decryptFailure" class="text-center text-gray-500 py-8"> |
|||
No members found in this meeting |
|||
</p> |
|||
<p v-if="decryptFailure" class="text-center text-red-600 py-8"> |
|||
That password failed. You may be in the wrong meeting. Go back and try again. |
|||
</p> |
|||
</div> |
|||
</section> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue } from 'vue-facing-decorator'; |
|||
import QuickNav from '@/components/QuickNav.vue'; |
|||
import TopMessage from '@/components/TopMessage.vue'; |
|||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; |
|||
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; |
|||
import { decryptMessage } from '@/libs/crypto'; |
|||
|
|||
interface Member { |
|||
memberId: number; |
|||
content: string; |
|||
} |
|||
|
|||
interface DecryptedMember { |
|||
memberId: number; |
|||
name: string; |
|||
did: string; |
|||
} |
|||
|
|||
@Component({ |
|||
components: { |
|||
QuickNav, |
|||
TopMessage, |
|||
}, |
|||
}) |
|||
export default class OnboardMeetingMembersView extends Vue { |
|||
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; |
|||
|
|||
activeDid = ''; |
|||
apiServer = ''; |
|||
decryptedMembers: DecryptedMember[] = []; |
|||
decryptFailure = false; |
|||
errorMessage = ''; |
|||
isLoading = false; |
|||
members: Member[] = []; |
|||
|
|||
get groupId(): string { |
|||
return this.$route.params.groupId as string; |
|||
} |
|||
|
|||
get password(): string { |
|||
return this.$route.query.password as string; |
|||
} |
|||
|
|||
async created() { |
|||
if (!this.groupId) { |
|||
this.errorMessage = 'The group info is missing. Go back and try again.'; |
|||
return; |
|||
} |
|||
if (!this.password) { |
|||
this.errorMessage = 'The password is missing. Go back and try again.'; |
|||
return; |
|||
} |
|||
|
|||
const settings = await retrieveSettingsForActiveAccount(); |
|||
this.activeDid = settings.activeDid || ''; |
|||
this.apiServer = settings.apiServer || ''; |
|||
await this.fetchMembers(); |
|||
} |
|||
|
|||
async fetchMembers() { |
|||
this.isLoading = true; |
|||
this.errorMessage = ''; |
|||
|
|||
try { |
|||
const headers = await getHeaders(this.activeDid); |
|||
const response = await this.axios.get( |
|||
`${this.apiServer}/api/partner/groupOnboardMembers/`, |
|||
{ headers } |
|||
); |
|||
|
|||
if (response.data && response.data.data) { |
|||
this.members = response.data.data; |
|||
await this.decryptMemberContents(); |
|||
} else { |
|||
throw { response: response }; |
|||
} |
|||
} catch (error) { |
|||
logConsoleAndDb('Error fetching members: ' + errorStringForLog(error), true); |
|||
this.errorMessage = serverMessageForUser(error) || 'Failed to fetch members.'; |
|||
} finally { |
|||
this.isLoading = false; |
|||
} |
|||
} |
|||
|
|||
async decryptMemberContents() { |
|||
this.decryptedMembers = []; |
|||
|
|||
for (const member of this.members) { |
|||
try { |
|||
const decryptedContent = await decryptMessage(member.content, this.password); |
|||
const content = JSON.parse(decryptedContent); |
|||
|
|||
this.decryptedMembers.push({ |
|||
memberId: member.memberId, |
|||
name: content.name, |
|||
did: content.did, |
|||
}); |
|||
} catch (error) { |
|||
this.decryptFailure = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
Loading…
Reference in new issue