Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix <HomeSelector> race condition #751

Merged
merged 8 commits into from
Aug 23, 2024
47 changes: 47 additions & 0 deletions src/composables/useUserType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { computed } from 'vue';
import { AUTH_USER_TYPE } from '@/constants/auth';
import _isEmpty from 'lodash/isEmpty';

/**
* Get user type
*
* Composable function to determine the user type based on the user claims. The user type can be either an admin or a
* participant. The user type is determined based on the user claims, where a user is considered an admin if they have
* the corresponding super_admin or miniamlAdminOrgs claims.
*
* @param {Object} userClaims - The user claims object.
* @returns {Object} The user type and related computed properties.
*/
export default function useUserType(userClaims) {
const userType = computed(() => {
// Abort the user type determination if the user claims are not available yet.
if (!userClaims.value) return;

const claims = userClaims.value.claims;

// Check if the user is a super admin.
if (claims?.super_admin) {
return AUTH_USER_TYPE.ADMIN;
}

// Check if the user has any minimal admin organizations.
const minimalAdminOrgs = claims.minimalAdminOrgs || {};
const hasMinimalAdminOrgs = Object.values(minimalAdminOrgs).some((org) => !_isEmpty(org));

if (hasMinimalAdminOrgs) {
return AUTH_USER_TYPE.ADMIN;
}

// Otherwise, default to participant user type.
return AUTH_USER_TYPE.PARTICIPANT;
});

const isAdmin = computed(() => userType.value === AUTH_USER_TYPE.ADMIN);
const isParticipant = computed(() => userType.value === AUTH_USER_TYPE.PARTICIPANT);

return {
userType,
isAdmin,
isParticipant,
};
}
54 changes: 54 additions & 0 deletions src/composables/useUserType.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import { computed } from 'vue';
import { AUTH_USER_TYPE } from '@/constants/auth';
import useUserType from './useUserType';

describe('useUserType', () => {
it('should return admin user type when user is a super admin', () => {
const userClaims = computed(() => ({ claims: { super_admin: true } }));
const { userType, isAdmin, isParticipant } = useUserType(userClaims);

expect(userType.value).toBe(AUTH_USER_TYPE.ADMIN);
expect(isAdmin.value).toBe(true);
expect(isParticipant.value).toBe(false);
});

it('should return admin user type when user has minimal admin orgs', () => {
const userClaims = computed(() => ({
claims: {
minimalAdminOrgs: {
org1: [{ id: 1 }],
org2: [{ id: 2 }],
},
},
}));
const { userType, isAdmin, isParticipant } = useUserType(userClaims);

expect(userType.value).toBe(AUTH_USER_TYPE.ADMIN);
expect(isAdmin.value).toBe(true);
expect(isParticipant.value).toBe(false);
});

it('should return participant user type when user is not a super admin and has no minimal admin orgs', () => {
const userClaims = computed(() => ({
claims: {
super_admin: false,
minimalAdminOrgs: {},
},
}));
const { userType, isAdmin, isParticipant } = useUserType(userClaims);

expect(userType.value).toBe(AUTH_USER_TYPE.PARTICIPANT);
expect(isAdmin.value).toBe(false);
expect(isParticipant.value).toBe(true);
});

it('should return undefined user type when no claims are provided', () => {
const userClaims = computed(() => null);
const { userType, isAdmin, isParticipant } = useUserType(userClaims);

expect(userType.value).toBe(undefined);
expect(isAdmin.value).toBe(false);
expect(isParticipant.value).toBe(false);
});
});
10 changes: 10 additions & 0 deletions src/constants/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@ export const AUTH_SESSION_TIMEOUT_IDLE_THRESHOLD =
parseInt(import.meta.env.VITE_AUTH_SESSION_TIMEOUT_IDLE_THRESHOLD, 10) || 15 * oneMinuteInMs;
export const AUTH_SESSION_TIMEOUT_COUNTDOWN_DURATION =
parseInt(import.meta.env.VITE_AUTH_SESSION_TIMEOUT_COUNTDOWN_DURATION, 10) || 60 * oneSecondInMs;

/**
* Auth User Type
*
* @constant {Object} AUTH_USER_TYPE - User type, admin or participant.
*/
export const AUTH_USER_TYPE = {
ADMIN: 'admin',
PARTICIPANT: 'participant',
};
35 changes: 16 additions & 19 deletions src/pages/HomeSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
<p class="text-center">{{ $t('homeSelector.loading') }}</p>
</div>
</div>

<div v-else>
<HomeParticipant v-if="!isAdmin" />
<HomeParticipant v-if="isParticipant" />
<HomeAdministrator v-else-if="isAdmin" />
</div>

<ConsentModal
v-if="showConsent && isAdmin"
v-if="!isLoading && showConsent && isAdmin"
:consent-text="confirmText"
:consent-type="consentType"
@accepted="updateConsent"
Expand All @@ -19,20 +21,21 @@
</template>

<script setup>
import { computed, onMounted, ref, toRaw, watch } from 'vue';
import { computed, defineAsyncComponent, onMounted, ref, toRaw, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import _get from 'lodash/get';
import { useAuthStore } from '@/store/auth';
import { useGameStore } from '@/store/game';
import _get from 'lodash/get';
import _isEmpty from 'lodash/isEmpty';
import _union from 'lodash/union';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

import useUserType from '@/composables/useUserType';
import useUserDataQuery from '@/composables/queries/useUserDataQuery';
import useUserClaimsQuery from '@/composables/queries/useUserClaimsQuery';

let HomeParticipant, HomeAdministrator, ConsentModal;
const HomeParticipant = defineAsyncComponent(() => import('@/pages/HomeParticipant.vue'));
const HomeAdministrator = defineAsyncComponent(() => import('@/pages/HomeAdministrator.vue'));
const ConsentModal = defineAsyncComponent(() => import('@/components/ConsentModal.vue'));

const isLevante = import.meta.env.MODE === 'LEVANTE';
const authStore = useAuthStore();
Expand Down Expand Up @@ -71,13 +74,9 @@ const { isLoading: isLoadingClaims, data: userClaims } = useUserClaimsQuery({
enabled: initialized,
});

const isLoading = computed(() => isLoadingClaims.value || isLoadingUserData.value);
const { isAdmin, isParticipant } = useUserType(userClaims);

const isAdmin = computed(() => {
if (userClaims.value?.claims?.super_admin) return true;
if (_isEmpty(_union(...Object.values(userClaims.value?.claims?.minimalAdminOrgs ?? {})))) return false;
return true;
});
const isLoading = computed(() => isLoadingClaims.value || isLoadingUserData.value);

const consentType = computed(() => {
if (isAdmin.value) {
Expand Down Expand Up @@ -106,11 +105,13 @@ async function checkConsent() {
// skip the consent for levante
return;
}

// Check for consent
if (isAdmin.value) {
const consentStatus = _get(userData.value, `legal.${consentType.value}`);
const consentDoc = await authStore.getLegalDoc(consentType.value);
consentVersion.value = consentDoc.version;

if (!_get(toRaw(consentStatus), consentDoc.version)) {
confirmText.value = consentDoc.text;
showConsent.value = true;
Expand All @@ -119,10 +120,6 @@ async function checkConsent() {
}

onMounted(async () => {
HomeParticipant = (await import('@/pages/HomeParticipant.vue')).default;
HomeAdministrator = (await import('@/pages/HomeAdministrator.vue')).default;
ConsentModal = (await import('@/components/ConsentModal.vue')).default;

if (requireRefresh.value) {
requireRefresh.value = false;
router.go(0);
Expand Down