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