Repository: DEFRA/ffc-ahwr-backoffice
Pull request #274: chore(AHWR-653): Dev Sign in update
Author: login: RobCatton
Created: 2025-03-25T12:47:16Z | Merged: 2025-03-26T13:16:12Z

Changing dev sign in so you can sign in with a different username/email combo as needed.


Diff:
diff --git a/app/auth/dev-auth.js b/app/auth/dev-auth.js
index 95fea4da..4ef8af98 100644
--- a/app/auth/dev-auth.js
+++ b/app/auth/dev-auth.js
@@ -5,28 +5,44 @@ const {
   recommender,
   authoriser,
 } = require("./permissions");
-const { developerName, developerUsername } = require("../config");
 
-const devAccount = {
-  name: developerName || "Developer",
-  username: developerUsername || "developer@defra.gov.uk",
+let cachedUserId;
+
+const getDevAccount = (userId) => {
+  if (userId) {
+    cachedUserId = userId;
+
+    return {
+      name: `Developer-${userId}`,
+      username: `developer+${userId}@defra.gov.uk`,
+    };
+  }
+
+  return {
+    name: "Developer",
+    username: "developer@defra.gov.uk",
+  };
 };
 
-const getAuthenticationUrl = () => {
+const getAuthenticationUrl = (userId) => {
+  if (userId) {
+    return `/dev-auth?userId=${userId}`;
+  }
+
   return "/dev-auth";
 };
 
-const authenticate = async (_, cookieAuth) => {
+const authenticate = async (userId, cookieAuth) => {
   cookieAuth.set({
     scope: [administrator, processor, user, recommender, authoriser],
-    account: devAccount,
+    account: getDevAccount(userId),
   });
 };
 
 const refresh = async (_account, cookieAuth) => {
   cookieAuth.set({
     scope: [administrator, processor, user, recommender, authoriser],
-    account: devAccount,
+    account: getDevAccount(cachedUserId),
   });
 
   return [administrator, processor, user, recommender, authoriser];
@@ -39,4 +55,5 @@ module.exports = {
   authenticate,
   refresh,
   logout,
+  getDevAccount,
 };
diff --git a/app/config/index.js b/app/config/index.js
index 796d2855..8789eae5 100644
--- a/app/config/index.js
+++ b/app/config/index.js
@@ -113,8 +113,6 @@ const config = {
         user.trim().toLowerCase(),
       )
     : [],
-  developerName: process.env.DEVELOPER_NAME,
-  developerUsername: process.env.DEVELOPER_USERNAME,
 };
 const { error, value } = schema.validate(config, { abortEarly: false });
 
diff --git a/app/routes/dev-auth.js b/app/routes/dev-auth.js
index 87c60411..396ac1f8 100644
--- a/app/routes/dev-auth.js
+++ b/app/routes/dev-auth.js
@@ -8,7 +8,8 @@ module.exports = {
   },
   handler: async (request, h) => {
     try {
-      await auth.authenticate(undefined, request.cookieAuth);
+      const { userId } = request.query;
+      await auth.authenticate(userId, request.cookieAuth);
       return h.redirect("/");
     } catch (err) {
       request.logger.setBindings({ err });
diff --git a/app/routes/login.js b/app/routes/login.js
index 9fe1189a..a90396e1 100644
--- a/app/routes/login.js
+++ b/app/routes/login.js
@@ -8,7 +8,9 @@ module.exports = {
   },
   handler: async (request, h) => {
     try {
-      const authUrl = await auth.getAuthenticationUrl();
+      // This is only needed for the dev login, and wont affect real logins
+      const { userId } = request.query;
+      const authUrl = await auth.getAuthenticationUrl(userId);
       return h.redirect(authUrl);
     } catch (err) {
       request.logger.setBindings({ err });
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 841baa23..e2468581 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -27,5 +27,3 @@ services:
       ENDEMICS_ENABLED: ${ENDEMICS_ENABLED:-true}
       MULTI_SPECIES_ENABLED: ${MULTI_SPECIES_ENABLED:-false}
       SUPER_ADMINS: ${SUPER_ADMINS}
-      DEVELOPER_NAME: ${DEVELOPER_NAME}
-      DEVELOPER_USERNAME: ${DEVELOPER_USERNAME}
diff --git a/package-lock.json b/package-lock.json
index ea1c2afe..aed5d990 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "ffc-ahwr-backoffice",
-  "version": "2.2.2",
+  "version": "2.2.3",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "ffc-ahwr-backoffice",
-      "version": "2.2.2",
+      "version": "2.2.3",
       "license": "OGL-UK-3.0",
       "dependencies": {
         "@azure/msal-node": "1.14.5",
diff --git a/package.json b/package.json
index dad92324..785e40e2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ffc-ahwr-backoffice",
-  "version": "2.2.2",
+  "version": "2.2.3",
   "description": "Back office of the health and welfare of your livestock",
   "homepage": "https://github.com/DEFRA/ffc-ahwr-backoffice",
   "main": "app/index.js",
diff --git a/test/unit/auth/dev-auth.test.js b/test/unit/auth/dev-auth.test.js
index ad2e706f..0847f66d 100644
--- a/test/unit/auth/dev-auth.test.js
+++ b/test/unit/auth/dev-auth.test.js
@@ -1,4 +1,9 @@
-const devAuth = require("../../../app/auth/dev-auth");
+const {
+  getAuthenticationUrl,
+  authenticate,
+  refresh,
+  logout,
+} = require("../../../app/auth/dev-auth");
 const {
   administrator,
   processor,
@@ -13,22 +18,62 @@ describe("Dev auth test", () => {
     jest.clearAllMocks();
   });
 
-  test("getAuthenticationUrl test", () => {
-    expect(devAuth.getAuthenticationUrl()).toBe("/dev-auth");
+  test("getAuthenticationUrl without any arguments", () => {
+    expect(getAuthenticationUrl()).toBe("/dev-auth");
+  });
+
+  test("getAuthenticationUrl with a userId", () => {
+    expect(getAuthenticationUrl("abc123")).toBe("/dev-auth?userId=abc123");
   });
-  test("authenticate test", async () => {
-    await devAuth.authenticate(expect.anything(), {
+
+  test("authenticate test with no userId argument", async () => {
+    await authenticate(undefined, {
       set: MOCK_COOKIE_AUTH_SET,
     });
     expect(MOCK_COOKIE_AUTH_SET).toHaveBeenCalledTimes(1);
+    expect(MOCK_COOKIE_AUTH_SET).toHaveBeenCalledWith({
+      account: {
+        name: "Developer",
+        username: "developer@defra.gov.uk",
+      },
+      scope: [
+        "administrator",
+        "processor",
+        "user",
+        "recommender",
+        "authoriser",
+      ],
+    });
   });
+
+  test("authenticate test with userId provided", async () => {
+    await authenticate("abc123", {
+      set: MOCK_COOKIE_AUTH_SET,
+    });
+    expect(MOCK_COOKIE_AUTH_SET).toHaveBeenCalledTimes(1);
+    expect(MOCK_COOKIE_AUTH_SET).toHaveBeenCalledWith({
+      account: {
+        name: "Developer-abc123",
+        username: "developer+abc123@defra.gov.uk",
+      },
+      scope: [
+        "administrator",
+        "processor",
+        "user",
+        "recommender",
+        "authoriser",
+      ],
+    });
+  });
+
   test("refresh test", async () => {
     expect(
-      await devAuth.refresh(expect.anything(), { set: MOCK_COOKIE_AUTH_SET }),
+      await refresh(expect.anything(), { set: MOCK_COOKIE_AUTH_SET }),
     ).toEqual([administrator, processor, user, recommender, authoriser]);
     expect(MOCK_COOKIE_AUTH_SET).toHaveBeenCalledTimes(1);
   });
+
   test("logout test", () => {
-    expect(devAuth.logout).toBeDefined();
+    expect(logout).toBeDefined();
   });
 });
diff --git a/test/unit/plugins/headers.test.js b/test/unit/plugins/headers.test.js
index c9e6242c..2e3ad828 100644
--- a/test/unit/plugins/headers.test.js
+++ b/test/unit/plugins/headers.test.js
@@ -14,7 +14,7 @@ describe("headers plugin tests", () => {
       value:
         "default-src 'self';object-src 'none';script-src 'self' www.google-analytics.com *.googletagmanager.com ajax.googleapis.com *.googletagmanager.com/gtm.js 'unsafe-inline' 'unsafe-eval' 'unsafe-hashes';form-action 'self';base-uri 'self';connect-src 'self' *.google-analytics.com *.analytics.google.com *.googletagmanager.comstyle-src 'self' 'unsafe-inline' tagmanager.google.com *.googleapis.com;img-src 'self' *.google-analytics.com *.googletagmanager.com;",
     },
-  ])("returns IT healcheck", async ({ key, value }) => {
+  ])("header key '$key' contains value '$value'", async ({ key, value }) => {
     const url = "/";
     const options = {
       method: "GET",


Reviews:
user: login: aaroncarroll82
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:06:41Z
user: login: RobCatton
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T10:02:57Z
user: login: aaroncarroll82
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-26T13:14:14Z

Review comments:
diff_hunk: @@ -7,26 +7,39 @@ const {
 } = require("./permissions");
 const { developerName, developerUsername } = require("../config");
 
-const devAccount = {
-  name: developerName || "Developer",
-  username: developerUsername || "developer@defra.gov.uk",
+const getDevAccount = (userId) => {
+  if (userId) {
+    return {
+      name: `Developer-${userId}`,
+      username: `developer+${userId}@defra.gov.uk`,
+    };
+  }
+
+  return {
+    name: developerName || "Developer",
+    username: developerUsername || "developer@defra.gov.uk",
+  };
 };
 
-const getAuthenticationUrl = () => {
+const getAuthenticationUrl = (userId) => {
+  if (userId) {
+    return `/dev-auth?userId=${userId}`;
+  }
+
   return "/dev-auth";
 };
 
-const authenticate = async (_, cookieAuth) => {
+const authenticate = async (userId, cookieAuth) => {
   cookieAuth.set({
     scope: [administrator, processor, user, recommender, authoriser],
-    account: devAccount,
+    account: getDevAccount(userId),
   });
 };
 
 const refresh = async (_account, cookieAuth) => {
   cookieAuth.set({
     scope: [administrator, processor, user, recommender, authoriser],
-    account: devAccount,
+    account: getDevAccount(),
path: app/auth/dev-auth.js
user: login: aaroncarroll82
body: Is this OK to call through with no param? If this is ever called it will change authentication underneath you from the specific dev account you formerly chose to the default one, which would be a bit odd. Maybe save the last one used in a variable somewhere to use in this case.

If it's never called (maybe the dev one never times out...?) then maybe it's better jus to make this function a no-op instead.
created_at: 2025-03-25T21:05:31Z
author_association: COLLABORATOR
diff_hunk: @@ -7,26 +7,39 @@ const {
 } = require("./permissions");
 const { developerName, developerUsername } = require("../config");
 
-const devAccount = {
-  name: developerName || "Developer",
-  username: developerUsername || "developer@defra.gov.uk",
+const getDevAccount = (userId) => {
+  if (userId) {
+    return {
+      name: `Developer-${userId}`,
+      username: `developer+${userId}@defra.gov.uk`,
+    };
+  }
+
+  return {
+    name: developerName || "Developer",
+    username: developerUsername || "developer@defra.gov.uk",
+  };
 };
 
-const getAuthenticationUrl = () => {
+const getAuthenticationUrl = (userId) => {
+  if (userId) {
+    return `/dev-auth?userId=${userId}`;
+  }
+
   return "/dev-auth";
 };
 
-const authenticate = async (_, cookieAuth) => {
+const authenticate = async (userId, cookieAuth) => {
   cookieAuth.set({
     scope: [administrator, processor, user, recommender, authoriser],
-    account: devAccount,
+    account: getDevAccount(userId),
   });
 };
 
 const refresh = async (_account, cookieAuth) => {
   cookieAuth.set({
     scope: [administrator, processor, user, recommender, authoriser],
-    account: devAccount,
+    account: getDevAccount(),
path: app/auth/dev-auth.js
user: login: RobCatton
body: yeah good point - I knew it wasnt ideal but figured it would just log you in as the base Developer. I'll add a variable like you suggest.
created_at: 2025-03-26T10:02:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-26T10:22:24Z
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=ffc-ahwr-backoffice&pullRequest=274) **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=ffc-ahwr-backoffice&pullRequest=274&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=ffc-ahwr-backoffice&pullRequest=274&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=ffc-ahwr-backoffice&pullRequest=274&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ffc-ahwr-backoffice&pullRequest=274&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=ffc-ahwr-backoffice&pullRequest=274&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ffc-ahwr-backoffice&pullRequest=274)


author_association: NONE