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

Implement nav bar for internal users #478

Merged
merged 10 commits into from
Oct 25, 2023
3 changes: 2 additions & 1 deletion app/plugins/views.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ function context (request) {
authenticated: request.auth.isAuthenticated,
authorized: request.auth.isAuthorized,
user: request.auth.credentials?.user,
scope: request.auth.credentials?.scope
scope: request.auth.credentials?.scope,
permission: request.auth.credentials?.permission
}
}
}
Expand Down
59 changes: 58 additions & 1 deletion app/services/plugins/auth.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const FetchUserRolesAndGroupsService = require('../idm/fetch-user-roles-and-grou
* of the roles the user has. This is used by hapi when authorising the user against a route; if the route has a scope
* array of strings then the user's scope array must contain at least one of the strings.
*
* Finally, we return a 'permission' object. This is used to determine which nav bar menu items a user sees.
*
* @param {Number} userId The user id to be authenticated
*
* @returns {Object} response
Expand All @@ -27,6 +29,8 @@ const FetchUserRolesAndGroupsService = require('../idm/fetch-user-roles-and-grou
* @returns {RoleModel[]} response.credentials.roles Objects representing the roles the user has
* @returns {GroupModel[]} response.credentials.groups Objects representing the groups the user is assigned to
* @returns {String[]} response.credentials.scope The names of the roles the user has, for route authorisation purposes
* @returns {Object} response.credentials.permission Object with each top level permission as a key and true or false
* whether the user has authorisation to access the area
*/
async function go (userId) {
const { user, roles, groups } = await FetchUserRolesAndGroupsService.go(userId)
Expand All @@ -36,7 +40,60 @@ async function go (userId) {
return role.role
})

return { isValid: !!user, credentials: { user, roles, groups, scope } }
const permission = _permission(scope)

return { isValid: !!user, credentials: { user, roles, groups, scope, permission } }
}

/**
* Determine top level permissions
*
* The legacy service uses 'roles' to determine what a user can or cannot do. These roles are added to routes as 'scope'
* and used to determine if a users is authorized to access a particular page.
*
* These 'roles' are very granular, for example, in the Manage page what links you see is dependent on the roles you
* have. Because of the current way the UI is designed we need to know whether you can access one of the top level areas
* of the site; Bill runs, Digitise! and Manage.
*
* For simplicity we call these 'permissions'. We determine them in this function and add them to the credentials this
* service returns.
*
* They are used by the nav bar to determine which menu items should be visible.
*
* @param {String[]} scope All the scopes (roles) a user has access to
*
* @returns {Object} Each top level permissions is a key. The value is true or false as to whether the user has
* permission to access that area of the service
*/
function _permission (scope = []) {
const billRuns = scope.includes('billing')

const manageRoles = [
'ar_approver',
'billing',
'bulk_return_notifications',
'hof_notifications',
'manage_accounts',
'renewal_notifications',
'returns'
]
const manage = scope.some((role) => {
return manageRoles.includes(role)
})

const abstractionReformRoles = [
'ar_user',
'ar_approver'
]
const abstractionReform = scope.some((role) => {
return abstractionReformRoles.includes(role)
})

return {
abstractionReform,
billRuns,
manage
}
}

module.exports = {
Expand Down
33 changes: 33 additions & 0 deletions app/views/includes/nav-bar.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!-- navigation pattern -->
<div class="nav-container navbarWhite">
<nav id="nav" class="navbar govuk-width-container ">
<ul class="navbar__list">
<li class="navbar__item">
<a class="navbar__link govuk-link--no-visited-state {{ 'navbar__link--active' if activeNavBar === 'search' }}" href="/licences" id="nav-search">
Search
</a>
</li>
{% if auth.permission.billRuns %}
<li class="navbar__item">
<a class="navbar__link govuk-link--no-visited-state {{ 'navbar__link--active' if activeNavBar === 'bill-runs' }}" href="/billing/batch/list" id="nav-bill-runs">
Bill runs
</a>
</li>
{% endif %}
{% if auth.permission.abstractionReform %}
<li class="navbar__item">
<a class="navbar__link govuk-link--no-visited-state {{ 'navbar__link--active' if activeNavBar === 'abstraction-reform' }}" href="/digitise" id="nav-abstraction-reform">
Digitise!
</a>
</li>
{% endif %}
{% if auth.permission.manage %}
<li class="navbar__item">
<a class="navbar__link govuk-link--no-visited-state {{ 'navbar__link--active' if activeNavBar === 'manage' }}" href="/manage" id="nav-manage">
Manage
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
6 changes: 6 additions & 0 deletions app/views/layout.njk
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
}) }}
{% endblock %}

{% block beforeContent %}
{% if auth.authenticated %}
{% include "includes/nav-bar.njk" %}
{% endif %}
{% endblock %}

{% block content %}
<h1 class="govuk-heading-xl">Default page template</h1>
{% endblock %}
Expand Down
60 changes: 60 additions & 0 deletions client/sass/application.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,63 @@
$govuk-global-styles: true;

@import "node_modules/govuk-frontend/govuk/all";

Cruikshanks marked this conversation as resolved.
Show resolved Hide resolved
// ========== ========== ========== ========== ==========
// Nav bar
// ========== ========== ========== ========== ==========

// nav bar container
.navbar {
border-bottom: 1px solid $govuk-border-colour;
}

.navbar__list {
margin: 0;
padding: 0;
list-style: none;
margin-top: 15px 0px;

@media (min-width: 768px) {
font-size: 19px;
margin-top: 0;
position:relative;
left:-15px;
}
}

// nav bar list
.navbar__item {
font-weight: 700;
margin-bottom: 5px;
display: block;
@media (min-width: 768px) {
margin-bottom: 0px;
display: inline-block;
font-size: 19px;
}
}

.navbar__link {
display: inline-block;
text-decoration: none;
padding: 10px 5px 5px 0px;
font-size: 16px;
@media (min-width: 768px) {
padding: 15px;
font-size: 19px;
}
}

// active link for nav bar
.navbar__link--active {
border-left: 4px solid $govuk-link-colour;
margin-left: -10px;
padding-left: 5px;
@media (min-width: 768px) {
border-left: none;
border-bottom: 4px solid $govuk-link-colour;
margin-bottom: -1px;
margin-left:0;
padding-left:15px;
}
}
61 changes: 61 additions & 0 deletions test/services/plugins/auth.service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,67 @@ describe('Auth service', () => {

expect(result.credentials.scope).to.equal(['Role'])
})

it('returns the top level permissions in credentials.permission', async () => {
const result = await AuthService.go(12345)

expect(result.credentials.permission).to.equal({ abstractionReform: false, billRuns: false, manage: false })
})
})

describe('when the user has a top-level permission role', () => {
describe("such as 'ar_user'", () => {
beforeEach(() => {
Sinon.stub(FetchUserRolesAndGroupsService, 'go')
.resolves({
user: { name: 'User' },
roles: [{ role: 'ar_user' }],
groups: [{ group: 'Group' }]
})
})

it('returns the matching top level permission as true', async () => {
const result = await AuthService.go(12345)

expect(result.credentials.permission).to.equal({ abstractionReform: true, billRuns: false, manage: false })
})
})

describe("such as 'billing'", () => {
beforeEach(() => {
Sinon.stub(FetchUserRolesAndGroupsService, 'go')
.resolves({
user: { name: 'User' },
roles: [{ role: 'billing' }],
groups: [{ group: 'Group' }]
})
})

it('returns the matching top level permission as true', async () => {
const result = await AuthService.go(12345)

// NOTE: Access to bill runs is granted for users with the 'billing' role. They also get access to the manage
// page. So, there currently isn't a scenario where a user would see the 'Bill runs' option but not 'Manage'.
expect(result.credentials.permission).to.equal({ abstractionReform: false, billRuns: true, manage: true })
})
})

describe("such as 'returns'", () => {
beforeEach(() => {
Sinon.stub(FetchUserRolesAndGroupsService, 'go')
.resolves({
user: { name: 'User' },
roles: [{ role: 'returns' }],
groups: [{ group: 'Group' }]
})
})

it('returns the matching top level permission as true', async () => {
const result = await AuthService.go(12345)

expect(result.credentials.permission).to.equal({ abstractionReform: false, billRuns: false, manage: true })
})
})
})

describe('when the user id is not found', () => {
Expand Down
Loading