Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions client/src/common/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,14 @@ export const claimAndBootstrapLinkValidator: IValidator = async function (value:
if (value.length === 0) {
return { validity: Validity.Intermediate };
}
// TODO: REPLACE WHEN LIBPARSEC HANDLES PKI LINKS
if (value.includes('a=pki_enrollment')) {
return { validity: Validity.Valid };
}
const result = await parseParsecAddr(value);

if (
result.ok &&
(result.value.tag === ParsedParsecAddrTag.OrganizationBootstrap ||
result.value.tag === ParsedParsecAddrTag.InvitationUser ||
result.value.tag === ParsedParsecAddrTag.InvitationDevice)
result.value.tag === ParsedParsecAddrTag.InvitationDevice ||
result.value.tag === ParsedParsecAddrTag.PkiEnrollment)
) {
return { validity: Validity.Valid };
}
Expand Down Expand Up @@ -245,9 +242,9 @@ export const pkiLinkValidator: IValidator = async function (value: string) {
if (value.length === 0) {
return { validity: Validity.Intermediate };
}
// TODO: REPLACE WHEN LIBPARSEC HANDLES PKI LINKS
if (value.includes('a=pki_enrollment')) {
return { validity: Validity.Valid };
const result = await parseParsecAddr(value);
if (result.ok) {
return result.value.tag === ParsedParsecAddrTag.PkiEnrollment ? { validity: Validity.Valid } : { validity: Validity.Invalid };
}
return { validity: Validity.Invalid };
return { validity: Validity.Invalid, reason: '' };
};
1 change: 0 additions & 1 deletion client/src/components/devices/ChooseAuthentication.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
</ion-radio>

<ion-radio
v-show="smartcardAvailable"
class="item-radio radio-list-item"
label-placement="end"
justify="start"
Expand Down
52 changes: 24 additions & 28 deletions client/src/components/invitations/PkiRequestItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@
<!-- request - mobile version -->
<div
class="pkiRequest-mobile"
v-if="isSmallDisplay"
v-if="isSmallDisplay && request.tag === PkiEnrollmentListItemTag.Valid"
>
<div class="pkiRequest-mobile-header">
<ion-text class="pkiRequest-mobile-header__name subtitles-normal">{{ request.humanHandle.label }}</ion-text>
<ion-text class="pkiRequest-mobile-header__email button-medium">{{ request.humanHandle.email }}</ion-text>
<ion-text class="pkiRequest-mobile-header__name subtitles-normal">
{{ (request as PkiEnrollmentListItemValid).payload.humanHandle.label }}
</ion-text>
<ion-text class="pkiRequest-mobile-header__email button-medium">
{{ (request as PkiEnrollmentListItemValid).payload.humanHandle.email }}
</ion-text>
</div>
<div class="pkiRequest-mobile-content">
<ion-text class="pkiRequest-mobile-content__createdOn body-sm">
{{ $msTranslate(formatTimeSince(request.createdOn, '--', 'short')) }}
{{ $msTranslate(formatTimeSince(request.submittedOn, '--', 'short')) }}
</ion-text>
<div
class="certificate button-small"
:class="`certificate-${request.validity}`"
>
<div class="certificate button-small certificate-valid">
<ion-icon
:icon="validity.icon"
class="certificate-icon"
Expand All @@ -35,23 +36,23 @@
<!-- request avatar -->
<div
class="pkiRequest-name"
v-if="isLargeDisplay"
v-if="isLargeDisplay && request.tag === PkiEnrollmentListItemTag.Valid"
>
<ion-text class="pkiRequest-name__label cell">
<user-avatar-name
:user-avatar="request.humanHandle.label"
:user-name="request.humanHandle.label"
:user-avatar="(request as PkiEnrollmentListItemValid).payload.humanHandle.label"
:user-name="(request as PkiEnrollmentListItemValid).payload.humanHandle.label"
/>
</ion-text>
</div>

<!-- request mail -->
<div
class="pkiRequest-email"
v-if="isLargeDisplay"
v-if="isLargeDisplay && request.tag === PkiEnrollmentListItemTag.Valid"
>
<ion-text class="pkiRequest-email__label cell">
{{ request.humanHandle.email }}
{{ (request as PkiEnrollmentListItemValid).payload.humanHandle.email }}
</ion-text>
</div>

Expand All @@ -61,20 +62,17 @@
v-if="isLargeDisplay"
>
<ion-text class="pkiRequest-createdOn__label cell">
{{ $msTranslate(formatTimeSince(request.createdOn, '--', 'short')) }}
{{ $msTranslate(formatTimeSince(request.submittedOn, '--', 'short')) }}
</ion-text>
</div>

<!-- request certificate -->
<div
class="pkiRequest-certificate"
v-if="isLargeDisplay"
v-if="isLargeDisplay && request.tag === PkiEnrollmentListItemTag.Valid"
>
<ion-text class="pkiRequest-certificate__label cell">
<div
class="certificate button-small"
:class="`certificate-${request.validity}`"
>
<div class="certificate button-small certificate-valid">
<ion-icon
:icon="validity.icon"
class="certificate-icon"
Expand Down Expand Up @@ -112,31 +110,29 @@

<script setup lang="ts">
import UserAvatarName from '@/components/users/UserAvatarName.vue';
import { JoinRequestValidity, OrganizationJoinRequest } from '@/parsec';
import { PkiEnrollmentListItem, PkiEnrollmentListItemTag, PkiEnrollmentListItemValid } from '@/parsec';
import { IonButton, IonIcon, IonItem, IonText } from '@ionic/vue';
import { checkmarkCircle, closeCircle, warning } from 'ionicons/icons';
import { checkmarkCircle, closeCircle } from 'ionicons/icons';
import { attachMouseOverTooltip, formatTimeSince, useWindowSize } from 'megashark-lib';
import { computed, onMounted, useTemplateRef } from 'vue';

const { isSmallDisplay, isLargeDisplay } = useWindowSize();
const rejectButtonRef = useTemplateRef<InstanceType<typeof IonButton>>('rejectButton');

const props = defineProps<{
request: OrganizationJoinRequest;
request: PkiEnrollmentListItem;
}>();

defineEmits<{
(e: 'acceptClick', invitation: OrganizationJoinRequest): void;
(e: 'rejectClick', invitation: OrganizationJoinRequest): void;
(e: 'acceptClick', invitation: PkiEnrollmentListItem): void;
(e: 'rejectClick', invitation: PkiEnrollmentListItem): void;
}>();

const validity = computed(() => {
if (props.request.validity === JoinRequestValidity.Valid) {
if (props.request.tag === PkiEnrollmentListItemTag.Valid) {
return { text: 'InvitationsPage.pkiRequests.certificate.valid', icon: checkmarkCircle };
} else if (props.request.validity === JoinRequestValidity.Invalid) {
return { text: 'InvitationsPage.pkiRequests.certificate.invalid', icon: closeCircle };
} else {
return { text: 'InvitationsPage.pkiRequests.certificate.unknown', icon: warning };
return { text: 'InvitationsPage.pkiRequests.certificate.invalid', icon: closeCircle };
}
});

Expand Down
88 changes: 57 additions & 31 deletions client/src/components/organizations/OrganizationPkiRequest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,39 @@
<template>
<div
class="organization-request"
:class="request.status"
:class="{
pending: request.info.tag === PKIInfoItemTag.Submitted,
rejected: request.info.tag === PKIInfoItemTag.Rejected,
accepted: request.info.tag === PKIInfoItemTag.Accepted,
cancelled: request.info.tag === PKIInfoItemTag.Cancelled,
}"
>
<ion-card-content class="organization-request-content">
<div class="organization-request-info">
<ion-text class="organization-request-organization title-h4">{{ request.organization }}</ion-text>
<ion-text class="organization-request-username body">{{ request.humanHandle.label }}</ion-text>
<ion-text class="organization-request-organization title-h4">{{ addr?.organizationId ?? '' }}</ion-text>
<ion-text class="organization-request-username body">{{ request.enrollment.payload.humanHandle.label }}</ion-text>
</div>
<div class="organization-request-status-container">
<ion-text
class="organization-request-status button-small"
:class="`status-${request.status}`"
ref="statusText"
:class="{
'status-pending': request.info.tag === PKIInfoItemTag.Submitted,
'status-rejected': request.info.tag === PKIInfoItemTag.Rejected,
'status-accepted': request.info.tag === PKIInfoItemTag.Accepted,
'status-cancelled': request.info.tag === PKIInfoItemTag.Cancelled,
}"
ref="statusTextEl"
>
<span v-if="statusText">{{ $msTranslate(statusText) }}</span>
</ion-text>
<ion-icon
v-if="request.status !== JoinRequestStatus.Accepted"
v-if="request.info.tag !== PKIInfoItemTag.Accepted"
@click="$emit('deleteRequest', request)"
:icon="closeCircle"
class="organization-request-icon"
/>
<ion-button
v-if="request.status === JoinRequestStatus.Accepted"
v-if="request.info.tag === PKIInfoItemTag.Accepted"
fill="clear"
class="organization-request-button"
@click="$emit('joinOrganization', request)"
Expand All @@ -42,49 +52,61 @@
</template>

<script lang="ts" setup>
import { JoinRequestStatus, LocalJoinRequest } from '@/parsec';
import { ParsedParsecAddrPkiEnrollment, ParsedParsecAddrTag, parseParsecAddr, PKIInfoItemTag, PkiLocalRequest } from '@/parsec';
import { IonButton, IonCardContent, IonIcon, IonText } from '@ionic/vue';
import { arrowForward, closeCircle } from 'ionicons/icons';
import { attachMouseOverTooltip } from 'megashark-lib';
import { computed, onMounted, useTemplateRef, watch } from 'vue';
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';

const statusTextRef = useTemplateRef<InstanceType<typeof IonText>>('statusText');
const statusTextRef = useTemplateRef<InstanceType<typeof IonText>>('statusTextEl');
const addr = ref<ParsedParsecAddrPkiEnrollment | undefined>(undefined);

const props = defineProps<{
request: LocalJoinRequest;
request: PkiLocalRequest;
}>();

onMounted(async () => {
if (statusTextRef.value && props.request.status === JoinRequestStatus.Pending) {
attachMouseOverTooltip(statusTextRef.value.$el, 'HomePage.organizationRequest.pending.tooltip');
} else if (statusTextRef.value && props.request.status === JoinRequestStatus.Rejected) {
attachMouseOverTooltip(statusTextRef.value.$el, 'HomePage.organizationRequest.rejected.tooltip');
attachTooltip(statusTextRef.value?.$el, props.request.info.tag);
const addrResult = await parseParsecAddr(props.request.enrollment.addr);
if (addrResult.ok && addrResult.value.tag === ParsedParsecAddrTag.PkiEnrollment) {
addr.value = addrResult.value;
}
});

watch(
() => props.request.status,
() => props.request.info.tag,
(newStatus) => {
if (statusTextRef.value && newStatus === JoinRequestStatus.Pending) {
attachMouseOverTooltip(statusTextRef.value.$el, 'HomePage.organizationRequest.pending.tooltip');
} else if (statusTextRef.value && newStatus === JoinRequestStatus.Rejected) {
attachMouseOverTooltip(statusTextRef.value.$el, 'HomePage.organizationRequest.rejected.tooltip');
}
attachTooltip(statusTextRef.value?.$el, newStatus);
},
);

function attachTooltip(el: HTMLElement | undefined, status: PKIInfoItemTag): void {
if (!el) {
return;
}
if (status === PKIInfoItemTag.Submitted) {
attachMouseOverTooltip(el, 'HomePage.organizationRequest.pending.tooltip');
} else if (status === PKIInfoItemTag.Rejected) {
attachMouseOverTooltip(el, 'HomePage.organizationRequest.rejected.tooltip');
} else if (status === PKIInfoItemTag.Cancelled) {
attachMouseOverTooltip(el, 'HomePage.organizationRequest.cancelled.tooltip');
} else if (status === PKIInfoItemTag.Accepted) {
attachMouseOverTooltip(el, 'HomePage.organizationRequest.accepted.tooltip');
}
}

defineEmits<{
(e: 'joinOrganization', request: LocalJoinRequest): void;
(e: 'deleteRequest', request: LocalJoinRequest): void;
(e: 'joinOrganization', request: PkiLocalRequest): void;
(e: 'deleteRequest', request: PkiLocalRequest): void;
}>();

const statusText = computed(() => {
switch (props.request.status) {
case JoinRequestStatus.Pending:
switch (props.request.info.tag) {
case PKIInfoItemTag.Submitted:
return 'HomePage.organizationRequest.status.pending';
case JoinRequestStatus.Rejected:
case PKIInfoItemTag.Rejected:
return 'HomePage.organizationRequest.status.rejected';
case JoinRequestStatus.Cancelled:
case PKIInfoItemTag.Cancelled:
return 'HomePage.organizationRequest.status.cancelled';
default:
return undefined;
Expand Down Expand Up @@ -149,16 +171,20 @@ const statusText = computed(() => {
padding: 0.125rem 0.5rem;
flex-shrink: 0;

&.status-pending {
&.status-cancelled {
background-color: var(--parsec-color-light-secondary-grey);
color: var(--parsec-color-light-secondary-white);
}

&.status-rejected,
&.status-cancelled {
&.status-rejected {
background-color: var(--parsec-color-light-danger-500);
color: var(--parsec-color-light-secondary-white);
}

&.status-pending {
background-color: var(--parsec-color-light-warning-100);
color: var(--parsec-color-light-warning-700);
}
}

.organization-request-icon {
Expand Down Expand Up @@ -205,7 +231,7 @@ const statusText = computed(() => {
border-color: var(--parsec-color-light-secondary-light);

.organization-request-icon {
color: var(--parsec-color-light-secondary-light);
color: var(--parsec-color-light-secondary-grey);

&:hover {
color: var(--parsec-color-light-secondary-text);
Expand Down
10 changes: 10 additions & 0 deletions client/src/components/users/UserInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,22 @@ defineExpose({
email,
fullName,
setFocus,
getEmail,
getFullName,
});

async function setFocus(): Promise<void> {
await firstInputFieldRef.value?.setFocus();
}

function getEmail(): string {
return email.value;
}

function getFullName(): string {
return fullName.value;
}

async function areFieldsCorrect(): Promise<boolean> {
return (
(await emailValidator(email.value)).validity === Validity.Valid && (await userNameValidator(fullName.value)).validity === Validity.Valid
Expand Down
Loading
Loading