Repository: bcgov/EDUC-CCOF
Pull request #695: fix: re-login URL for ministry users
Author: login: trev-dev
Created: 2025-04-17T22:36:49Z | Merged: 2025-04-23T22:43:19Z



Diff:
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index 5f03ae545..1e6a8eed4 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -6,7 +6,7 @@ const express = require('express');
 const auth = require('../components/auth');
 const log = require('../components/logger');
 const {v4: uuidv4} = require('uuid');
-const {getUserGuid, isIdir, isIdirUser} = require('../components/utils');
+const {getUserGuid} = require('../components/utils');
 
 const {
   body,
@@ -64,6 +64,7 @@ router.get('/login-idir', passport.authenticate('oidcIdir', {
 //removes tokens and destroys session
 router.get('/logout', async (req, res, next) => {
   const idToken = req.session?.passport?.user?.idToken;
+  const isIdir = !!req.session?.passport?.user?._json?.idir_username;
 
   req.logout(function(err) {
     if (err) return next(err);
@@ -72,9 +73,9 @@ router.get('/logout', async (req, res, next) => {
 
     let endpoint = '';
     if (req.query?.sessionExpired) {
-      endpoint = '/session-expired';
+      endpoint = `/session-expired?idir=${isIdir}`;
     } else {
-      endpoint = '/logout';
+      endpoint = `/logout?idir=${isIdir}`;
     }
 
     const redirectUri = `${config.get('server:frontend')}${endpoint}`;
@@ -141,8 +142,7 @@ router.get('/token', auth.refreshJWT, (req, res) => {
   }
 });
 async function generateTokens(req, res) {
-  let isIdir = (req.session?.passport?.user?._json?.idir_user_guid) ? true : false;
-  const result = await auth.renew(req.user.refreshToken, isIdir);
+  const result = await auth.renew(req.user.refreshToken);
   if (result && result.jwt && result.refreshToken) {
     req.user.jwt = result.jwt;
     req.user.refreshToken = result.refreshToken;
diff --git a/frontend/src/components/Logout.vue b/frontend/src/components/Logout.vue
index 21c49c8a1..a4d21084d 100644
--- a/frontend/src/components/Logout.vue
+++ b/frontend/src/components/Logout.vue
@@ -1,39 +1,36 @@
 <template>
-  <v-container fluid class="full-height">
-    <!-- login article -->
-    <article name="logout-banner">
-      <v-row align="center" justify="center">
-        <v-col xs="10" sm="10" md="8" lg="4" xl="3">
-          <v-card class="session-expired-card">
-            <v-card-title class="gov-header">
-              <h4 id="logout_text">Logged Out</h4>
-            </v-card-title>
-            <v-card-text id="logout_descriptor">
-              <v-row style="margin: 0.3rem"> You have Logged out. </v-row>
-              <a id="login-button" :href="routes.LOGIN" class="ma-1" dark color="#003366" @click="clearStorage"
-                >Log In</a
-              ><span>again if you wish to continue.</span>
-            </v-card-text>
-          </v-card>
-        </v-col>
-      </v-row>
-    </article>
+  <v-container fluid>
+    <AppSimpleCard>
+      <template #title>Logged Out</template>
+      <template #text>
+        You have Logged out.
+        <br />
+        <a id="login-button" :href="loginUrl" @click="clearStorage">Log In</a>
+        again if you wish to continue.
+      </template>
+    </AppSimpleCard>
   </v-container>
 </template>
 
 <script>
-import { AuthRoutes } from '../utils/constants.js';
+import { AuthRoutes } from '@/utils/constants.js';
+import { useAuthStore } from '@/store/auth.js';
 
-import { useAuthStore } from '../store/auth.js';
+import AppSimpleCard from '@/components/util/AppSimpleCard.vue';
 
 export default {
   name: 'LogoutComponent',
-
+  components: { AppSimpleCard },
   data() {
     return {
       routes: AuthRoutes,
     };
   },
+  computed: {
+    loginUrl() {
+      return this.$route.query.idir === 'true' ? AuthRoutes.LOGIN_IDIR : AuthRoutes.LOGIN;
+    },
+  },
   mounted() {
     const authStore = useAuthStore();
     authStore.setJwtToken();
diff --git a/frontend/src/components/SessionExpired.vue b/frontend/src/components/SessionExpired.vue
index b74657a91..f80df694f 100644
--- a/frontend/src/components/SessionExpired.vue
+++ b/frontend/src/components/SessionExpired.vue
@@ -1,39 +1,39 @@
 <template>
-  <v-container fluid class="full-height">
-    <!-- login article -->
-    <article name="session-expired-banner" class="top-banner">
-      <v-row align="center" justify="center">
-        <v-col xs="10" sm="10" md="8" lg="4" xl="3">
-          <v-card class="session-expired-card">
-            <v-card-title class="gov-header">
-              <h4 id="session-expired-text">Session Expired</h4>
-            </v-card-title>
-            <v-card-text id="session-expired-descriptor">
-              <v-row style="margin: 0.3rem"> Your secure session has ended as a result of inactivity. </v-row>
-              <a id="login-button" :href="routes.LOGIN" class="ma-1" dark color="#003366" @click="clearStorage"
-                >Log In</a
-              ><span>again to continue.</span>
-            </v-card-text>
-          </v-card>
-        </v-col>
-      </v-row>
-    </article>
+  <v-container fluid>
+    <AppSimpleCard>
+      <template #title>Session Expired</template>
+      <template #text>
+        Your secure session has ended as a result of inactivity.
+        <br />
+        <a id="login-button" :href="loginUrl" @click="clearStorage">Log In</a>
+        again to continue.
+      </template>
+    </AppSimpleCard>
   </v-container>
 </template>
 
 <script>
-import { AuthRoutes } from '../utils/constants.js';
+import { AuthRoutes } from '@/utils/constants.js';
 
-import { useAuthStore } from '../store/auth.js';
+import { useAuthStore } from '@/store/auth.js';
+
+import AppSimpleCard from '@/components/util/AppSimpleCard.vue';
 
 export default {
   name: 'SessionExpired',
-
+  components: {
+    AppSimpleCard,
+  },
   data() {
     return {
       routes: AuthRoutes,
     };
   },
+  computed: {
+    loginUrl() {
+      return this.$route.query.idir === 'true' ? AuthRoutes.LOGIN_IDIR : AuthRoutes.LOGIN;
+    },
+  },
   mounted() {
     const authStore = useAuthStore();
     authStore.setJwtToken();
diff --git a/frontend/src/components/util/AppSimpleCard.vue b/frontend/src/components/util/AppSimpleCard.vue
new file mode 100644
index 000000000..7b08f7c5a
--- /dev/null
+++ b/frontend/src/components/util/AppSimpleCard.vue
@@ -0,0 +1,21 @@
+<template>
+  <v-row align="center" justify="center">
+    <v-col xs="8" sm="7" md="5" lg="4" xl="3">
+      <v-card class="simple-card">
+        <v-card-title>
+          <h4><slot name="title" /></h4>
+        </v-card-title>
+        <v-card-text>
+          <slot name="text" />
+        </v-card-text>
+      </v-card>
+    </v-col>
+  </v-row>
+</template>
+
+<style scoped>
+.simple-card {
+  margin-top: 5rem;
+  border-top: 5px solid #003366 !important;
+}
+</style>


Reviews:
user: login: weskubo-cgi
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T19:21:01Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T20:49:09Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T21:39:26Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T21:39:37Z
user: login: weskubo-cgi
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:19:02Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:31:44Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:39:05Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:39:17Z
user: login: trev-dev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:39:35Z
user: login: weskubo-cgi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:42:10Z
user: login: weskubo-cgi
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:42:35Z

Review comments:
diff_hunk: @@ -64,6 +64,7 @@ router.get('/login-idir', passport.authenticate('oidcIdir', {
 //removes tokens and destroys session
 router.get('/logout', async (req, res, next) => {
   const idToken = req.session?.passport?.user?.idToken;
+  const isIdir = req.session?.passport?.user?._json?.idir_username !== undefined;
path: backend/src/routes/auth.js
user: login: weskubo-cgi
body: This implementation works fine but it's inconsistent with line 145. Though the isIdir function in utils.js uses this style so we can probably stick with it.

However, the !== undefined check means null would result in isIdir = true. I think `const isIdir = !!req.session?.passport?.user?._json?.idir_username;` is probably the safest option.
created_at: 2025-04-23T19:06:09Z
author_association: COLLABORATOR
diff_hunk: @@ -74,7 +75,7 @@ router.get('/logout', async (req, res, next) => {
     if (req.query?.sessionExpired) {
       endpoint = '/session-expired';
path: backend/src/routes/auth.js
user: login: weskubo-cgi
body: While we're at it I feel like we should fix up SessionExpired.vue as well. Bring the UI up to date and support BCeID/IDIR.
created_at: 2025-04-23T19:19:29Z
author_association: COLLABORATOR
diff_hunk: @@ -64,6 +64,7 @@ router.get('/login-idir', passport.authenticate('oidcIdir', {
 //removes tokens and destroys session
 router.get('/logout', async (req, res, next) => {
   const idToken = req.session?.passport?.user?.idToken;
+  const isIdir = req.session?.passport?.user?._json?.idir_username !== undefined;
path: backend/src/routes/auth.js
user: login: trev-dev
body: I'm not sure that Keycloak would ever return a null value, but I agree with the double bang approach
created_at: 2025-04-23T20:49:08Z
author_association: COLLABORATOR
diff_hunk: @@ -64,6 +64,7 @@ router.get('/login-idir', passport.authenticate('oidcIdir', {
 //removes tokens and destroys session
 router.get('/logout', async (req, res, next) => {
   const idToken = req.session?.passport?.user?.idToken;
+  const isIdir = req.session?.passport?.user?._json?.idir_username !== undefined;
path: backend/src/routes/auth.js
user: login: trev-dev
body: A note on 145, the `isIdir` boolean does not work, and `auth.renew` does not accept a second argument. I removed that dead code: https://github.com/bcgov/EDUC-CCOF/pull/695/commits/0803e10be3bb8bba496029a39c73a7a409c89b49
created_at: 2025-04-23T21:39:26Z
author_association: COLLABORATOR
diff_hunk: @@ -74,7 +75,7 @@ router.get('/logout', async (req, res, next) => {
     if (req.query?.sessionExpired) {
       endpoint = '/session-expired';
path: backend/src/routes/auth.js
user: login: trev-dev
body: Done!
created_at: 2025-04-23T21:39:37Z
author_association: COLLABORATOR
diff_hunk: @@ -74,7 +75,7 @@ router.get('/logout', async (req, res, next) => {
     if (req.query?.sessionExpired) {
       endpoint = '/session-expired';
path: backend/src/routes/auth.js
user: login: weskubo-cgi
body: I think this also needs `?idir=${isIdir}`.
created_at: 2025-04-23T22:15:51Z
author_association: COLLABORATOR
diff_hunk: @@ -1,39 +1,36 @@
 <template>
-  <v-container fluid class="full-height">
-    <!-- login article -->
-    <article name="logout-banner">
-      <v-row align="center" justify="center">
-        <v-col xs="10" sm="10" md="8" lg="4" xl="3">
-          <v-card class="session-expired-card">
-            <v-card-title class="gov-header">
-              <h4 id="logout_text">Logged Out</h4>
-            </v-card-title>
-            <v-card-text id="logout_descriptor">
-              <v-row style="margin: 0.3rem"> You have Logged out. </v-row>
-              <a id="login-button" :href="routes.LOGIN" class="ma-1" dark color="#003366" @click="clearStorage"
-                >Log In</a
-              ><span>again if you wish to continue.</span>
-            </v-card-text>
-          </v-card>
-        </v-col>
-      </v-row>
-    </article>
+  <v-container fluid>
+    <AppSimpleCard>
+      <template #title>Logged Out</template>
+      <template #text>
+        You have Logged out.
+        <br />
+        <a id="login-button" :href="loginUrl" @click="clearStorage">Log In</a>
+        again if you wish to continue.
+      </template>
+    </AppSimpleCard>
   </v-container>
 </template>
 
 <script>
 import { AuthRoutes } from '../utils/constants.js';
-
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
path: frontend/src/components/Logout.vue
user: login: weskubo-cgi
body: We've been updating imports to use absolute paths using @ as we update files.
created_at: 2025-04-23T22:16:49Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +17,23 @@ import { AuthRoutes } from '../utils/constants.js';
 
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
+
 export default {
   name: 'SessionExpired',
-
+  components: {
+    AppSimpleCard
path: frontend/src/components/SessionExpired.vue
user: login: weskubo-cgi
body: I'm getting both Prettier and eslint warnings in this file. Lack of trailing comma here and double instead of single quotes on line 34 (same for Logout.vue).
created_at: 2025-04-23T22:17:22Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +17,23 @@ import { AuthRoutes } from '../utils/constants.js';
 
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
path: frontend/src/components/SessionExpired.vue
user: login: weskubo-cgi
body: See previous comment about imports.
created_at: 2025-04-23T22:17:37Z
author_association: COLLABORATOR
diff_hunk: @@ -1,39 +1,36 @@
 <template>
-  <v-container fluid class="full-height">
-    <!-- login article -->
-    <article name="logout-banner">
-      <v-row align="center" justify="center">
-        <v-col xs="10" sm="10" md="8" lg="4" xl="3">
-          <v-card class="session-expired-card">
-            <v-card-title class="gov-header">
-              <h4 id="logout_text">Logged Out</h4>
-            </v-card-title>
-            <v-card-text id="logout_descriptor">
-              <v-row style="margin: 0.3rem"> You have Logged out. </v-row>
-              <a id="login-button" :href="routes.LOGIN" class="ma-1" dark color="#003366" @click="clearStorage"
-                >Log In</a
-              ><span>again if you wish to continue.</span>
-            </v-card-text>
-          </v-card>
-        </v-col>
-      </v-row>
-    </article>
+  <v-container fluid>
+    <AppSimpleCard>
+      <template #title>Logged Out</template>
+      <template #text>
+        You have Logged out.
+        <br />
+        <a id="login-button" :href="loginUrl" @click="clearStorage">Log In</a>
+        again if you wish to continue.
+      </template>
+    </AppSimpleCard>
   </v-container>
 </template>
 
 <script>
 import { AuthRoutes } from '../utils/constants.js';
-
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
path: frontend/src/components/Logout.vue
user: login: trev-dev
body: It feels like this abstraction is unnecessary and less clear than a relative path. It's also not a normal part of JavaScript. Maybe a debate for another day
created_at: 2025-04-23T22:31:43Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +17,23 @@ import { AuthRoutes } from '../utils/constants.js';
 
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
path: frontend/src/components/SessionExpired.vue
user: login: trev-dev
body: Done
created_at: 2025-04-23T22:39:05Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +17,23 @@ import { AuthRoutes } from '../utils/constants.js';
 
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
+
 export default {
   name: 'SessionExpired',
-
+  components: {
+    AppSimpleCard
path: frontend/src/components/SessionExpired.vue
user: login: trev-dev
body: Thanks for catching this
created_at: 2025-04-23T22:39:17Z
author_association: COLLABORATOR
diff_hunk: @@ -74,7 +75,7 @@ router.get('/logout', async (req, res, next) => {
     if (req.query?.sessionExpired) {
       endpoint = '/session-expired';
path: backend/src/routes/auth.js
user: login: trev-dev
body: Thanks for catching this
created_at: 2025-04-23T22:39:35Z
author_association: COLLABORATOR
diff_hunk: @@ -1,39 +1,36 @@
 <template>
-  <v-container fluid class="full-height">
-    <!-- login article -->
-    <article name="logout-banner">
-      <v-row align="center" justify="center">
-        <v-col xs="10" sm="10" md="8" lg="4" xl="3">
-          <v-card class="session-expired-card">
-            <v-card-title class="gov-header">
-              <h4 id="logout_text">Logged Out</h4>
-            </v-card-title>
-            <v-card-text id="logout_descriptor">
-              <v-row style="margin: 0.3rem"> You have Logged out. </v-row>
-              <a id="login-button" :href="routes.LOGIN" class="ma-1" dark color="#003366" @click="clearStorage"
-                >Log In</a
-              ><span>again if you wish to continue.</span>
-            </v-card-text>
-          </v-card>
-        </v-col>
-      </v-row>
-    </article>
+  <v-container fluid>
+    <AppSimpleCard>
+      <template #title>Logged Out</template>
+      <template #text>
+        You have Logged out.
+        <br />
+        <a id="login-button" :href="loginUrl" @click="clearStorage">Log In</a>
+        again if you wish to continue.
+      </template>
+    </AppSimpleCard>
   </v-container>
 </template>
 
 <script>
 import { AuthRoutes } from '../utils/constants.js';
-
 import { useAuthStore } from '../store/auth.js';
 
+import AppSimpleCard from './util/AppSimpleCard.vue';
path: frontend/src/components/Logout.vue
user: login: weskubo-cgi
body: I'd say it's the defacto standard for Vite/Vue. Relative paths work fine for simple apps but for larger apps it's easier to follow a well-defined path from the top rather than wherever the relative path takes you.
created_at: 2025-04-23T22:42:10Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-23T22:39:35Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bcgov_EDUC-CCOF&pullRequest=695) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bcgov_EDUC-CCOF&pullRequest=695&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bcgov_EDUC-CCOF&pullRequest=695&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bcgov_EDUC-CCOF&pullRequest=695&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bcgov_EDUC-CCOF&pullRequest=695&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bcgov_EDUC-CCOF&pullRequest=695&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bcgov_EDUC-CCOF&pullRequest=695)


author_association: NONE