Skip to content

Commit 7caa063

Browse files
authored
Merge pull request #239 from bcgov/idp
Changes to support BCSC auth
2 parents 06f4d43 + 3df2011 commit 7caa063

File tree

14 files changed

+160
-94
lines changed

14 files changed

+160
-94
lines changed

bcgovpubcode.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ product_external_dependencies:
77
- Common-Object-Management-Service
88
identity_authorization:
99
- IDIR
10+
- BCSC
1011
- BceId
1112
- Business-BceId
1213
notification_standard: []

frontend/src/assets/main.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ div:focus-visible {
125125
max-width: 400px !important;
126126
}
127127

128+
.help-link {
129+
font-size: 100%;
130+
cursor: pointer;
131+
position: relative;
132+
top: -.25rem;
133+
margin-left: .1rem;
134+
opacity: 1;
135+
color: $bcbox-link-text;
136+
}
137+
128138
/* layout */
129139
.layout-main {
130140
margin: 1rem;

frontend/src/components/bucket/BucketList.vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ onMounted(async () => {
109109
{{ bucketConfigTitle }}
110110
</h3>
111111

112+
<Message severity="warn">
113+
If you intend to share files in your bucket with BCeID or BC Services Card users, please notify
114+
<a href="mailto:[email protected]">[email protected]</a> that you plan to use BCBox.
115+
</Message>
116+
112117
<Message severity="info">
113118
Please contact
114119
<a
@@ -121,12 +126,6 @@ onMounted(async () => {
121126
location sources.
122127
</Message>
123128

124-
<Message severity="warn">
125-
If you intend to share files in your bucket with BCeID users, you are required to email
126-
127-
to share your BCeID-related intentions and where you intend to advertise this.
128-
</Message>
129-
130129
<BucketConfigForm
131130
:bucket="bucketToUpdate"
132131
@submit-bucket-config="closeBucketConfig"

frontend/src/components/bucket/BucketPermissionAddUser.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@ import { storeToRefs } from 'pinia';
44
import SearchUsers from '@/components/form/SearchUsers.vue';
55
import { useConfigStore, usePermissionStore } from '@/store';
66
7-
import type { User } from '@/types';
7+
import type { User, IdentityProvider } from '@/types';
88
99
// Store
1010
const { getConfig } = storeToRefs(useConfigStore());
1111
const permissionStore = usePermissionStore();
1212
1313
// Actions
1414
const onAdd = (selectedUser: User) => {
15-
const idp = getConfig.value.idpList.find((idp: any) => idp.idp === selectedUser?.idp);
15+
const configuredIdp = getConfig.value.idpList.find((idp: IdentityProvider) => idp.idp === selectedUser.idp);
16+
const idpName = configuredIdp?.name || 'BCSC';
17+
const idpElevated = configuredIdp?.elevatedRights || false;
1618
1719
permissionStore.addBucketUser({
1820
userId: selectedUser.userId,
19-
idpName: idp?.name,
20-
elevatedRights: idp?.elevatedRights,
21+
idpName: idpName,
22+
elevatedRights: idpElevated,
2123
fullName: selectedUser.fullName,
2224
create: false,
2325
read: false,

frontend/src/components/bucket/BucketTable.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ function createDummyNodes(neighbour: BucketTreeNode, node: BucketTreeNode) {
134134
// Fix broken endpoints caused by delimiter splitting
135135
fullPath = fullPath.replace(/^https?:\//i, (match) => `${match}/`);
136136
137+
// TODO: exclude un-mapped parent folders
138+
// if(getBuckets.value.find(b => b.key === key || (key).startsWith(b.bucket+'/'+b.key))){
137139
dummyNodes.push({
138140
key: fullPath,
139141
data: {
@@ -305,7 +307,7 @@ watch(getBuckets, () => {
305307
label-text="Folder"
306308
/>
307309
<BucketChildConfig
308-
v-if="permissionStore.isBucketActionAllowed(node.data.bucketId, getUserId, Permissions.MANAGE)"
310+
v-if="permissionStore.isBucketActionAllowed(node.data.bucketId, getUserId, Permissions.CREATE)"
309311
:parent-bucket="node.data"
310312
/>
311313
<Button

frontend/src/components/common/BulkPermission.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,8 @@ const onSubmit = handleSubmit(async (values: any, { resetForm }) => {
386386
class="block"
387387
>
388388
Enter an email address for each person whose permissions you wish to update for this
389-
{{ props.resourceType === 'bucket' ? 'folder' : 'file' }}. The email address should be associated with the IDIR
390-
or BCeID account they will use to sign in to BCBox.
389+
{{ props.resourceType === 'bucket' ? 'folder' : 'file' }}. The email address should be associated with the
390+
account they will use to sign in to BCBox.
391391
</small>
392392
<ErrorMessage name="multiEmail" />
393393
</div>

frontend/src/components/form/SearchUsers.vue

Lines changed: 99 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1+
<!-- eslint-disable vue/no-v-html -->
12
<script setup lang="ts">
23
import { storeToRefs } from 'pinia';
3-
import { isProxy, onMounted, ref, watch } from 'vue';
4+
import { computed, isProxy, onMounted, ref, watch } from 'vue';
45
5-
import { Button, Dropdown, RadioButton } from '@/lib/primevue';
6-
import { useConfigStore, useUserStore } from '@/store';
6+
import { Button, OverlayPanel, Dropdown, RadioButton } from '@/lib/primevue';
7+
import { useUserStore, useAuthStore } from '@/store';
78
import { Regex } from '@/utils/constants';
89
910
import type { Ref } from 'vue';
1011
import type { IInputEvent } from '@/interfaces';
1112
import type { DropdownChangeEvent } from '@/lib/primevue';
12-
import type { IdentityProvider, User, UserPermissions } from '@/types';
13+
import type { User, UserPermissions } from '@/types';
1314
1415
// Props
1516
type Props = {
@@ -23,20 +24,26 @@ const emit = defineEmits(['add-user', 'cancel-search-users']);
2324
2425
// Store
2526
const userStore = useUserStore();
26-
const { getConfig } = storeToRefs(useConfigStore());
27-
const { userSearch } = storeToRefs(useUserStore());
27+
const { getExternalUsers, userSearch } = storeToRefs(useUserStore());
28+
const { getUserId } = storeToRefs(useAuthStore());
2829
2930
// State
3031
const invalidSelectedUser: Ref<boolean> = ref(false);
31-
const selectedIDP: Ref<IdentityProvider | null> = ref(null);
32+
const selectedIdpType: Ref<string | undefined> = ref('internal');
3233
const selectedUser: Ref<User | null> = ref(null);
3334
const userSearchInput: Ref<string | undefined> = ref('');
34-
const userSearchPlaceholder: Ref<string | undefined> = ref('');
35+
const userSearchPlaceholder: Ref<string | undefined> = ref('Name or email address');
36+
// search results
37+
const userSearchResults: Ref<User[]> = computed(() => {
38+
return (selectedIdpType.value === 'internal' ? userSearch.value : getExternalUsers.value)
39+
// filter current user
40+
.filter((user: User) => user.userId !== getUserId.value);
41+
});
3542
3643
// Actions
3744
const getUserDropdownLabel = (option: User) => {
38-
if (selectedIDP.value?.idp) {
39-
if (selectedIDP.value.searchable) {
45+
if (selectedIdpType.value) {
46+
if (selectedIdpType.value === 'internal') {
4047
return `${option.fullName} [${option.email}]`;
4148
} else {
4249
return option.email;
@@ -73,15 +80,15 @@ const onChange = (event: DropdownChangeEvent) => {
7380
7481
const onInput = (event: IInputEvent) => {
7582
const input: string = event.target.value;
76-
if (selectedIDP.value?.idp) {
83+
if (selectedIdpType.value) {
7784
// Reset selection on any input change
7885
selectedUser.value = null;
7986
invalidSelectedUser.value = false;
80-
81-
if (selectedIDP.value.searchable && input.length >= 3) {
82-
userStore.fetchUsers({ idp: selectedIDP.value.idp, search: input });
83-
} else if (input.match(Regex.EMAIL)) {
84-
userStore.fetchUsers({ idp: selectedIDP.value.idp, email: input });
87+
if (selectedIdpType.value === 'internal' && input.length >= 3) {
88+
userStore.fetchUsers({ idp: 'idir', search: input });
89+
}
90+
else if (input.match(Regex.EMAIL)) {
91+
userStore.fetchUsers({ email: input });
8592
} else {
8693
userStore.clearSearch();
8794
}
@@ -96,59 +103,98 @@ const onReset = () => {
96103
userSearchInput.value = '';
97104
};
98105
99-
watch(selectedIDP, () => {
100-
if (selectedIDP.value?.searchable) {
101-
userSearchPlaceholder.value = `Enter the full name or email address of an existing ${selectedIDP.value?.name}`;
102-
} else {
103-
userSearchPlaceholder.value = `Enter an existing user's ${selectedIDP.value?.name} email address`;
104-
}
106+
watch(selectedIdpType, () => {
107+
// user search field placeholder
108+
userSearchPlaceholder.value = selectedIdpType.value === 'internal' ?
109+
'Name or email address' : 'Complete email address';
105110
});
106111
107-
onMounted(() => {
108-
// Set default IDP
109-
selectedIDP.value = getConfig.value.idpList[0];
112+
const helpText = ref('');
113+
const help1 = ref();
114+
const help2 = ref();
115+
116+
const toggleHelp = (id: string | undefined, event:any) => {
117+
switch(id) {
118+
case 'help1':
119+
help1.value.toggle(event);
120+
helpText.value = 'Select the authentication method this person will use to sign into BCBox.';
121+
break;
122+
case 'help2':
123+
help2.value.toggle(event);
124+
helpText.value = 'Note: If the person you are adding is new to BCBox, ' +
125+
'please ask them to first sign in to the website, so we can find them in the system.';
126+
break;
127+
}
128+
};
110129
130+
onMounted(() => {
111131
onReset();
112132
});
113133
</script>
114134

115135
<template>
116136
<div>
117-
<ul v-if="getConfig.idpList.length <= 3">
118-
<li
119-
v-for="idp of getConfig.idpList"
120-
:key="idp.idp"
121-
class="field-radiobutton mt-1"
137+
<h4 class="mb-3">Add User(s)</h4>
138+
<div class="mb-2">
139+
<label>How will his person sign in to BCBox?</label>
140+
<span
141+
class="help-link material-icons-outlined"
142+
@click="toggleHelp('help1', $event)"
122143
>
123-
<RadioButton
124-
v-model="selectedIDP"
125-
:input-id="idp.idp"
126-
name="idp"
127-
:value="idp"
128-
@click="onReset"
129-
/>
130-
<label :for="idp.idp">{{ idp.name }}</label>
131-
</li>
132-
</ul>
133-
<div v-else>
134-
<Dropdown
135-
v-model="selectedIDP"
136-
:options="getConfig.idpList"
137-
:option-label="
138-
(option: any) => {
139-
return `${option.name} (${option.elevatedRights ? 'internal' : 'external'})`;
140-
}
141-
"
142-
class="mt-1"
143-
@change="onReset"
144-
/>
144+
help_outline
145+
</span>
146+
<OverlayPanel
147+
ref="help1"
148+
class="max-w-25rem"
149+
>
150+
<span>{{ helpText }}</span>
151+
</OverlayPanel>
152+
</div>
153+
<div class="card flex justify-center mb-4">
154+
<div class="flex flex-wrap gap-4">
155+
<div class="flex items-center align-items-center gap-2">
156+
<RadioButton
157+
v-model="selectedIdpType"
158+
input-id="internal"
159+
name="idpType"
160+
value="internal"
161+
@click="onReset"
162+
/>
163+
<label for="internal">Government IDIR</label>
164+
</div>
165+
<div class="flex align-items-center items-center gap-2">
166+
<RadioButton
167+
v-model="selectedIdpType"
168+
input-id="external"
169+
name="idpType"
170+
value="external"
171+
@click="onReset"
172+
/>
173+
<label for="external">External (eg: BCeID or BC Services Card)</label>
174+
</div>
175+
</div>
145176
</div>
146177

178+
<div class="mb-2">
179+
<label>Search for user</label>
180+
<span
181+
class="help-link material-icons-outlined"
182+
@click="toggleHelp('help2', $event)"
183+
>
184+
help_outline
185+
</span>
186+
<OverlayPanel
187+
ref="help2"
188+
class="max-w-25rem"
189+
>
190+
<span>{{ helpText }}</span>
191+
</OverlayPanel>
192+
</div>
147193
<div class="flex">
148194
<div class="flex flex-auto">
149195
<Dropdown
150196
v-model="userSearchInput"
151-
:options="userSearch"
197+
:options="userSearchResults"
152198
:option-label="(option: any) => getUserDropdownLabel(option)"
153199
editable
154200
:placeholder="userSearchPlaceholder"

frontend/src/components/layout/Navbar.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import { Toolbar } from '@/lib/primevue';
55
import { useAuthStore } from '@/store';
66
import { RouteNames } from '@/utils/constants';
77
8-
// const { home, items } = storeToRefs(useNavStore());
9-
const { getIsAuthenticated } = storeToRefs(useAuthStore());
8+
const { getIsAuthenticated, getProfile } = storeToRefs(useAuthStore());
109
</script>
1110

1211
<template>
1312
<nav class="navigation-main lg:px-7">
1413
<Toolbar>
1514
<template #start>
1615
<ol class="list-none m-0 p-0 flex flex-row align-items-center font-semibold">
17-
<li class="mr-2">
16+
<!-- hide Home nav for non-idir users-->
17+
<li
18+
v-if="!getIsAuthenticated || (getIsAuthenticated && getProfile?.identity_provider === 'idir')"
19+
class="mr-2"
20+
>
1821
<router-link :to="{ name: RouteNames.HOME }">Home</router-link>
1922
</li>
2023
<li

frontend/src/lib/primevue/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as Button } from 'primevue/button';
55
export { default as Checkbox } from 'primevue/checkbox';
66
export { default as Column } from 'primevue/column';
77
export { default as ConfirmDialog } from 'primevue/confirmdialog';
8+
export { default as OverlayPanel } from 'primevue/overlaypanel';
89
export { default as DataTable } from 'primevue/datatable';
910
export { default as Dialog } from 'primevue/dialog';
1011
export { default as Divider } from 'primevue/divider';

frontend/src/store/authStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ export const useAuthStore = defineStore('auth', () => {
7070
const user = await authService.getUser();
7171
const profile = user?.profile;
7272
const isAuthenticated = !!user && !user.expired;
73-
const identityId = configService
73+
// gets identityId from jwt.<first found identityKey idpList>
74+
let identityId = configService
7475
.getConfig()
7576
.idpList.map((provider: IdentityProvider) => (profile ? profile[provider.identityKey] : undefined))
7677
.filter((item?: string) => item)[0];
78+
// try and get using one of configured identityKey's otherwise use `sub` field
79+
if(profile) identityId = identityId ?? profile['sub'];
7780

7881
state.accessToken.value = user?.access_token;
7982
state.expiresAt.value = user?.expires_at;

0 commit comments

Comments
 (0)