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

feat: #1655 restrict client selection when d admins assign roles #1701

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { inject, type Ref } from "vue";
import Dropdown from "../UI/Dropdown.vue";
import NotificationMessage from "../UI/NotificationMessage.vue";
import SubsectionTitle from "../UI/SubsectionTitle.vue";
import ForestClientSection from "./ForestClientSection.vue";
import ForestClientSection from "./ForestClientAddTable.vue";

const formData = inject<Ref<AppPermissionFormType>>(APP_PERMISSION_FORM_KEY);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ onUnmounted(() => {
</script>

<template>
<div class="foresnt-client-section-container">
<div class="foresnt-client-add-table-container">
<SubsectionTitle
title="Restrict access by organizations"
subtitle="Add one or more organizations for this user to have access to"
Expand Down Expand Up @@ -237,7 +237,7 @@ onUnmounted(() => {
</template>

<style lang="scss">
.foresnt-client-section-container {
.foresnt-client-add-table-container {
.subsection-title-container {
margin: 1.5rem 0;
}
Expand Down
167 changes: 167 additions & 0 deletions frontend/src/components/AddPermissions/ForestClientSelectTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script setup lang="ts">
import DataTable from "primevue/datatable";
import Column from "primevue/column";
import Checkbox from "primevue/checkbox";
import RadioButton from "primevue/radiobutton";
import { AdminMgmtApiService } from "@/services/ApiServiceFactory";
import { useQuery } from "@tanstack/vue-query";
import { Field, useField } from "vee-validate";
import { computed, inject, watch, type Ref } from "vue";
import { APP_PERMISSION_FORM_KEY } from "@/constants/InjectionKeys";
import type { AppPermissionFormType } from "@/views/AddAppPermission/utils";
import Label from "../UI/Label.vue";
import SubsectionTitle from "../UI/SubsectionTitle.vue";
import { getForestClientsUnderApp } from "@/utils/AuthUtils";
import type { FamForestClientBase } from "fam-admin-mgmt-api/model";
import ErrorText from "../UI/ErrorText.vue";

const formData = inject<Ref<AppPermissionFormType>>(APP_PERMISSION_FORM_KEY);

if (!formData) {
throw new Error("formData is required but not provided");
}

const props = defineProps<{
appId: number;
fieldId: string;
}>();

const { validate: validateForestClients } = useField(props.fieldId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure how this validate works...
Just curious, how does useField(props.fieldId) know what to validate?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's validating the based on the field id provided
in this case it's forestClients inside the validation logic

image


const adminUserAccessQuery = useQuery({
queryKey: ["admin-user-access"],
queryFn: () =>
AdminMgmtApiService.adminUserAccessesApi
.adminUserAccessPrivilege()
.then((res) => res.data),
select: (data) => getForestClientsUnderApp(props.appId, data),
refetchOnMount: true,
});

const availableForestClients = computed<FamForestClientBase[]>(() => {
const data = adminUserAccessQuery.data.value;
return data ?? [];
});

watch(
availableForestClients,
() => {
if (availableForestClients.value.length === 1) {
formData.value.forestClients = availableForestClients.value;
}
},
{ immediate: true }
);

const isForestClientSelected = (client: FamForestClientBase) =>
formData.value.forestClients.some(
(selectedClient) =>
selectedClient.forest_client_number === client.forest_client_number
);

const toggleForestClient = (client: FamForestClientBase) => {
const index = formData.value.forestClients.findIndex(
(selectedClient) =>
selectedClient.forest_client_number === client.forest_client_number
);
if (index >= 0) {
// Remove client if already selected
formData.value.forestClients.splice(index, 1);
} else {
// Add client if not selected
formData.value.forestClients.push(client);
}
// Validate again to remove resolved error if there is any
validateForestClients();
};
</script>

<template>
<div class="foresnt-client-select-table-container">
<SubsectionTitle
title="Restrict access by organizations"
subtitle="Select one or more organizations for this to access"
/>

<Field
:name="props.fieldId"
v-slot="{ errorMessage }"
v-model="formData.forestClients"
>
<Label label-text="Organizations" required />

<ErrorText
v-if="errorMessage"
show-icon
:error-msg="errorMessage"
/>

<!-- Table section -->
<DataTable class="fam-table" :value="availableForestClients">
<template #empty>No organization available</template>

<Column v-if="availableForestClients.length === 1" header="">
<template #body="{ data }">
<RadioButton
class="fam-checkbox"
:value="data"
v-model="formData.forestClients[0]"
readonly
/>
</template>
</Column>

<Column v-else header="">
<template #body="{ data }">
<Checkbox
class="fam-checkbox"
:binary="true"
:model-value="isForestClientSelected(data)"
@change="toggleForestClient(data)"
/>
</template>
</Column>

<Column header="Name" field="client_name" />

<Column header="Client number" field="forest_client_number" />
</DataTable>
</Field>
</div>
</template>

<style lang="scss">
.foresnt-client-select-table-container {
.error-text-container {
padding: 0;
height: fit-content;
margin-bottom: 0.5rem;
}

.subsection-title-container {
margin: 1.5rem 0;
}

.input-with-verify-button {
.add-organization-button {
width: 12rem;
}
}

.fam-table {
.p-datatable-emptymessage {
background-color: var(--layer-01);
}
}

.fam-checkbox {
display: flex;
flex-direction: row;
align-items: center;
.p-checkbox-box {
width: 1rem;
height: 1rem;
}
}
}
</style>
16 changes: 13 additions & 3 deletions frontend/src/components/AddPermissions/RoleSelectTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { ErrorMessage, Field } from "vee-validate";
import { computed, inject, ref, type Ref } from "vue";
import Label from "../UI/Label.vue";
import DelegatedAdminSection from "./DelegatedAdminSection.vue";
import ForestClientSection from "./ForestClientSection.vue";
import ForestClientAddTable from "./ForestClientAddTable.vue";
import ForestClientSelectTable from "./ForestClientSelectTable.vue";

const props = defineProps<{
appId: number;
Expand Down Expand Up @@ -145,9 +146,18 @@ const handleRoleSelect = (role: FamRoleGrantDto) => {
<Column field="roleDescription" header="Description">
<template #body="{ data }">
<span>{{ data.description }}</span>

<ForestClientSection
<ForestClientSelectTable
v-if="
isDelegatedAdminOnly &&
isAbstractRoleSelected(formData) &&
formData.role?.id === data.id
"
:app-id="props.appId"
:field-id="props.forestClientsFieldId"
/>

<ForestClientAddTable
v-else-if="
selectedRole?.id !== delegatedAdminRow.id &&
isAbstractRoleSelected(formData) &&
formData.role?.id === data.id
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/components/ManagePermissionsTable/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -729,9 +729,9 @@ const handleFilter = (searchValue: string, isChanged: boolean) => {
</template>
</Column>

<Column header="Action">
<Column header="Action" class="action-col">
<template #body="{ data }">
<div class="nowrap-cell">
<div class="nowrap-cell action-button-group">
<button
v-if="!isAppAdminTable"
title="User permission history"
Expand Down Expand Up @@ -770,5 +770,22 @@ const handleFilter = (searchValue: string, isChanged: boolean) => {
align-items: center;
gap: 0.25rem;
}

tr > td.action-col {
padding: 0 1rem 0 1rem;

.action-button-group {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 100%;
.btn-icon {
padding: 0.5rem;
display: flex;
flex-direction: column;
}
}
}
}
</style>
8 changes: 4 additions & 4 deletions frontend/src/components/UI/ErrorText.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import ErrorOutline from "@carbon/icons-vue/es/error--outline/16";
import WarnOutline from "@carbon/icons-vue/es/warning--filled/16";

defineProps<{
showIcon?: boolean;
Expand All @@ -8,7 +8,7 @@ defineProps<{
</script>
<template>
<div class="error-text-container">
<ErrorOutline v-if="showIcon" />
<WarnOutline v-if="showIcon" />
<p v-if="errorMsg">{{ errorMsg }}</p>
</div>
</template>
Expand All @@ -20,12 +20,12 @@ defineProps<{

p {
margin: 0;
color: var(--support-error);
color: var(--text-error);
}

svg {
margin-right: 0.5rem;
stroke: var(--support-error);
fill: var(--support-error);
}
}
</style>
43 changes: 43 additions & 0 deletions frontend/src/utils/AuthUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AdminRoleAuthGroup,
type AdminUserAccessResponse,
type FamAuthGrantDto,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

man we need a linter installed

type FamForestClientBase,
} from "fam-admin-mgmt-api/model";

/**
Expand Down Expand Up @@ -55,3 +56,45 @@ export const isUserDelegatedAdminOnly = (

return !isAppAdmin && isDelegatedAdmin;
};

/**
* Retrieves the list of forest clients associated with a specific application
* for which the user is a delegated admin.
*
* @param {number} appId - The ID of the application to retrieve forest clients for.
* @param {AdminUserAccessResponse} [userAccess] - The response containing user access information.
* @returns {FamForestClientBase[]} An array of forest clients if the user is a delegated admin
* for the specified application; returns an empty array if the user does not have delegated admin access
* or if the userAccess data is invalid.
*
*/
export const getForestClientsUnderApp = (
appId: number,
userAccess?: AdminUserAccessResponse
): FamForestClientBase[] | null => {
if (
!userAccess ||
!isSelectedAppAuthorized(
AdminRoleAuthGroup.DelegatedAdmin,
appId,
userAccess
)
) {
return [];
}

const forestClients: FamForestClientBase[] =
userAccess.access
.find(
(grantDto) =>
grantDto.auth_key === AdminRoleAuthGroup.DelegatedAdmin
)
?.grants.find(
(grantDetailDto) => grantDetailDto.application.id === appId
)
?.roles?.flatMap(
(roleGrantDto) => roleGrantDto.forest_clients ?? []
) || [];

return forestClients;
};
Loading