diff --git a/server/controllers/baseClientController.test.ts b/server/controllers/baseClientController.test.ts index e35586ef..663a8e44 100644 --- a/server/controllers/baseClientController.test.ts +++ b/server/controllers/baseClientController.test.ts @@ -9,6 +9,7 @@ import listBaseClientsPresenter from '../views/presenters/listBaseClientsPresent import createUserToken from '../testutils/createUserToken' import viewBaseClientPresenter from '../views/presenters/viewBaseClientPresenter' import nunjucksUtils from '../views/helpers/nunjucksUtils' +import editBaseClientPresenter from '../views/presenters/editBaseClientPresenter' describe('BaseClientController', () => { const token = createUserToken(['ADMIN']) @@ -109,7 +110,7 @@ describe('BaseClientController', () => { }) it('if grant is specified with authorization-code renders the details screen', async () => { - // GIVEN a request with grant="client-credentials" parameter + // GIVEN a request with grant="authorization-code" parameter request = createMock({ query: { grant: 'authorization-code' } }) // WHEN the create base client page is requested @@ -184,4 +185,85 @@ describe('BaseClientController', () => { }) }) }) + + describe('update base client details', () => { + it('displays update base client details screen', async () => { + // GIVEN a request to edit a base client + const baseClient = baseClientFactory.build() + baseClientService.getBaseClient.mockResolvedValue(baseClient) + request = createMock({ params: { baseClientId: baseClient.baseClientId } }) + + // WHEN the edit base client details page is requested + await baseClientController.displayEditBaseClient()(request, response, next) + + // THEN the base client is retrieved from the base client service + expect(baseClientService.getBaseClient).toHaveBeenCalledWith(token, baseClient.baseClientId) + + // AND the page is rendered + const presenter = editBaseClientPresenter(baseClient) + expect(response.render).toHaveBeenCalledWith('pages/edit-base-client-details.njk', { + baseClient, + presenter, + ...nunjucksUtils, + }) + }) + + it('updates and redirects to view base client screen', async () => { + // GIVEN the service will return without an error + const baseClient = baseClientFactory.build() + request = createMock({ + params: { baseClientId: baseClient.baseClientId }, + body: { baseClientId: baseClient.baseClientId }, + }) + baseClientService.updateBaseClient.mockResolvedValue(new Response()) + + // WHEN it is posted + await baseClientController.updateBaseClientDetails()(request, response, next) + + // THEN the base client service is updated + expect(baseClientService.updateBaseClient).toHaveBeenCalled() + + // AND the user is redirected to the view base client page + expect(response.redirect).toHaveBeenCalledWith(`/base-clients/${baseClient.baseClientId}`) + }) + }) + + describe('update base client deployment', () => { + it('displays update base client deployment screen', async () => { + // GIVEN a request to edit base client deployment + const baseClient = baseClientFactory.build() + baseClientService.getBaseClient.mockResolvedValue(baseClient) + request = createMock({ params: { baseClientId: baseClient.baseClientId } }) + + // WHEN the edit base client details page is requested + await baseClientController.displayEditBaseClientDeployment()(request, response, next) + + // THEN the base client is retrieved from the base client service + expect(baseClientService.getBaseClient).toHaveBeenCalledWith(token, baseClient.baseClientId) + + // AND the page is rendered + expect(response.render).toHaveBeenCalledWith('pages/edit-base-client-deployment.njk', { + baseClient, + }) + }) + + it('updates and redirects to view base client screen', async () => { + // GIVEN the service will return without an error + const baseClient = baseClientFactory.build() + request = createMock({ + params: { baseClientId: baseClient.baseClientId }, + body: { baseClientId: baseClient.baseClientId }, + }) + baseClientService.updateBaseClientDeployment.mockResolvedValue(new Response()) + + // WHEN it is posted + await baseClientController.updateBaseClientDeployment()(request, response, next) + + // THEN the base client service is updated + expect(baseClientService.updateBaseClientDeployment).toHaveBeenCalled() + + // AND the user is redirected to the view base client page + expect(response.redirect).toHaveBeenCalledWith(`/base-clients/${baseClient.baseClientId}`) + }) + }) }) diff --git a/server/controllers/baseClientController.ts b/server/controllers/baseClientController.ts index b75b4ae6..ff732d4b 100644 --- a/server/controllers/baseClientController.ts +++ b/server/controllers/baseClientController.ts @@ -3,7 +3,7 @@ import { BaseClientService } from '../services' import listBaseClientsPresenter from '../views/presenters/listBaseClientsPresenter' import viewBaseClientPresenter from '../views/presenters/viewBaseClientPresenter' import nunjucksUtils from '../views/helpers/nunjucksUtils' -import { mapCreateBaseClientForm } from '../mappers' +import { mapCreateBaseClientForm, mapEditBaseClientDeploymentForm, mapEditBaseClientDetailsForm } from '../mappers' import { BaseClient } from '../interfaces/baseClientApi/baseClient' import editBaseClientPresenter from '../views/presenters/editBaseClientPresenter' @@ -94,4 +94,69 @@ export default class BaseClientController { return '' } } + + public displayEditBaseClient(): RequestHandler { + return async (req, res) => { + const userToken = res.locals.user.token + const { baseClientId } = req.params + const baseClient = await this.baseClientService.getBaseClient(userToken, baseClientId) + + const presenter = editBaseClientPresenter(baseClient) + res.render('pages/edit-base-client-details.njk', { + baseClient, + presenter, + ...nunjucksUtils, + }) + } + } + + public updateBaseClientDetails(): RequestHandler { + return async (req, res, next) => { + const userToken = res.locals.user.token + const { baseClientId } = req.params + + // get current values + const baseClient = await this.baseClientService.getBaseClient(userToken, baseClientId) + + // map form values to updated base client + const updatedClient = mapEditBaseClientDetailsForm(baseClient, req) + + // update base client + await this.baseClientService.updateBaseClient(userToken, updatedClient) + + // return to view base client page + res.redirect(`/base-clients/${baseClientId}`) + } + } + + public displayEditBaseClientDeployment(): RequestHandler { + return async (req, res) => { + const userToken = res.locals.user.token + const { baseClientId } = req.params + const baseClient = await this.baseClientService.getBaseClient(userToken, baseClientId) + + res.render('pages/edit-base-client-deployment.njk', { + baseClient, + }) + } + } + + public updateBaseClientDeployment(): RequestHandler { + return async (req, res, next) => { + const userToken = res.locals.user.token + const { baseClientId } = req.params + + // get current values + const baseClient = await this.baseClientService.getBaseClient(userToken, baseClientId) + + // map form values to updated base client + const updatedClient = mapEditBaseClientDeploymentForm(baseClient, req) + + // update base client + await this.baseClientService.updateBaseClientDeployment(userToken, updatedClient) + + // return to view base client page + res.redirect(`/base-clients/${baseClientId}`) + } + } } diff --git a/server/data/localMockData/baseClientsResponseMock.ts b/server/data/localMockData/baseClientsResponseMock.ts index c92b48ea..0d9623cf 100644 --- a/server/data/localMockData/baseClientsResponseMock.ts +++ b/server/data/localMockData/baseClientsResponseMock.ts @@ -35,4 +35,16 @@ export const getBaseClientResponseMock: GetBaseClientResponse = { databaseUserName: 'databaseUserName', validDays: 1, accessTokenValidityMinutes: 60, + deployment: { + team: 'deployment team', + teamContact: 'deployment team contact', + teamSlack: 'deployment team slack', + hosting: 'deployment hosting', + namespace: 'deployment namespace', + deployment: 'deployment deployment', + secretName: 'deployment secret name', + clientIdKey: 'deployment client id key', + secretKey: 'deployment secret key', + deploymentInfo: 'deployment deployment info', + }, } diff --git a/server/interfaces/baseClientApi/baseClientResponse.ts b/server/interfaces/baseClientApi/baseClientResponse.ts index c3e982fd..dcc90aa5 100644 --- a/server/interfaces/baseClientApi/baseClientResponse.ts +++ b/server/interfaces/baseClientApi/baseClientResponse.ts @@ -22,6 +22,18 @@ export interface GetBaseClientResponse { databaseUserName?: string validDays?: number accessTokenValidityMinutes?: number + deployment: { + team: string + teamContact: string + teamSlack: string + hosting: string + namespace: string + deployment: string + secretName: string + clientIdKey: string + secretKey: string + deploymentInfo: string + } } export interface ClientSecretsResponse { diff --git a/server/mappers/baseClientApi/getBaseClient.ts b/server/mappers/baseClientApi/getBaseClient.ts index 8effb796..7aa7f714 100644 --- a/server/mappers/baseClientApi/getBaseClient.ts +++ b/server/mappers/baseClientApi/getBaseClient.ts @@ -27,16 +27,7 @@ export default (response: GetBaseClientResponse): BaseClient => { contact: '', status: '', }, - deployment: { - team: '', - teamContact: '', - teamSlack: '', - hosting: '', - namespace: '', - deployment: '', - secretName: '', - clientIdKey: '', - }, + deployment: response.deployment, config: { allowedIPs: response.ips ? response.ips : [], expiryDate: response.validDays diff --git a/server/mappers/baseClientApi/updateBaseClient.ts b/server/mappers/baseClientApi/updateBaseClient.ts index 9a6d7ff9..1a84b95a 100644 --- a/server/mappers/baseClientApi/updateBaseClient.ts +++ b/server/mappers/baseClientApi/updateBaseClient.ts @@ -4,9 +4,9 @@ import { daysRemaining } from '../../utils/utils' export default (baseClient: BaseClient): UpdateBaseClientRequest => { return { - scopes: ['read', 'write'], - authorities: ['ROLE_CLIENT_CREDENTIALS'], - ips: [], + scopes: baseClient.scopes, + authorities: baseClient.clientCredentials.authorities, + ips: baseClient.config.allowedIPs, jiraNumber: baseClient.audit, databaseUserName: baseClient.clientCredentials.databaseUserName, validDays: baseClient.config.expiryDate ? daysRemaining(baseClient.config.expiryDate) : null, diff --git a/server/mappers/forms/mapCreateBaseClientForm.ts b/server/mappers/forms/mapCreateBaseClientForm.ts index 3b89a42f..148071b2 100644 --- a/server/mappers/forms/mapCreateBaseClientForm.ts +++ b/server/mappers/forms/mapCreateBaseClientForm.ts @@ -1,24 +1,6 @@ import type { Request } from 'express' import { BaseClient } from '../../interfaces/baseClientApi/baseClient' -import { multiSeparatorSplit } from '../../utils/utils' - -function getDayOfExpiry(daysRemaining: string) { - const daysRemainingInt = parseIntWithDefault(daysRemaining, 0) - const timeOfExpiry: Date = new Date(Date.now() + daysRemainingInt * 24 * 60 * 60 * 1000) - return timeOfExpiry.toISOString().split('T')[0] -} - -function getAccessTokenValiditySeconds(accessTokenValidity: string, customAccessTokenValidity?: string) { - if (accessTokenValidity === 'custom' && customAccessTokenValidity) { - return parseIntWithDefault(customAccessTokenValidity, 0) - } - return parseIntWithDefault(accessTokenValidity, 0) -} - -function parseIntWithDefault(value: string, defaultValue: number) { - const parsed = parseInt(value, 10) - return Number.isNaN(parsed) ? defaultValue : parsed -} +import { getAccessTokenValiditySeconds, getDayOfExpiry, multiSeparatorSplit } from '../../utils/utils' export default (request: Request): BaseClient => { // valid days is calculated from expiry date diff --git a/server/mappers/forms/mapEditBaseClientDeploymentForm.test.ts b/server/mappers/forms/mapEditBaseClientDeploymentForm.test.ts new file mode 100644 index 00000000..f119d32d --- /dev/null +++ b/server/mappers/forms/mapEditBaseClientDeploymentForm.test.ts @@ -0,0 +1,58 @@ +import type { Request } from 'express' +import { createMock } from '@golevelup/ts-jest' +import { baseClientFactory } from '../../testutils/factories' +import { mapEditBaseClientDeploymentForm } from '../index' + +const formRequest = (form: Record) => { + return createMock({ body: form }) +} + +describe('mapEditBaseClientDeploymentForm', () => { + describe('updates deployment details only', () => { + it('updates details only', () => { + // Given a base client with fields populated + const detailedBaseClient = baseClientFactory.build({ + accessTokenValidity: 3600, + deployment: { + team: 'deployment team', + }, + service: { + serviceName: 'service serviceName', + }, + }) + + // and given an edit deployment request with all fields populated + const request = formRequest({ + team: 'team', + teamContact: 'contact', + teamSlack: 'slack', + hosting: 'CLOUDPLATFORM', + namespace: 'b', + deployment: 'c', + secretName: 'd', + clientIdKey: 'e', + secretKey: 'f', + deploymentInfo: 'g', + }) + + // when the form is mapped + const update = mapEditBaseClientDeploymentForm(detailedBaseClient, request) + + // then the deployment details are updated + expect(update.deployment.team).toEqual('team') + expect(update.deployment.teamContact).toEqual('contact') + expect(update.deployment.teamSlack).toEqual('slack') + expect(update.deployment.hosting).toEqual('CLOUDPLATFORM') + expect(update.deployment.namespace).toEqual('b') + expect(update.deployment.deployment).toEqual('c') + expect(update.deployment.secretName).toEqual('d') + expect(update.deployment.clientIdKey).toEqual('e') + expect(update.deployment.secretKey).toEqual('f') + expect(update.deployment.deploymentInfo).toEqual('g') + + // but regular client details and service details are not updated + expect(update.accessTokenValidity).toEqual(3600) + expect(update.service.serviceName).toEqual(detailedBaseClient.service.serviceName) + }) + }) +}) diff --git a/server/mappers/forms/mapEditBaseClientDeploymentForm.ts b/server/mappers/forms/mapEditBaseClientDeploymentForm.ts new file mode 100644 index 00000000..d52baa0e --- /dev/null +++ b/server/mappers/forms/mapEditBaseClientDeploymentForm.ts @@ -0,0 +1,21 @@ +import type { Request } from 'express' +import { BaseClient } from '../../interfaces/baseClientApi/baseClient' + +export default (baseClient: BaseClient, request: Request): BaseClient => { + const data = request.body + return { + ...baseClient, + deployment: { + team: data.team, + teamContact: data.teamContact, + teamSlack: data.teamSlack, + hosting: data.hosting, + namespace: data.namespace, + deployment: data.deployment, + secretName: data.secretName, + clientIdKey: data.clientIdKey, + secretKey: data.secretKey, + deploymentInfo: data.deploymentInfo, + }, + } +} diff --git a/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts b/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts new file mode 100644 index 00000000..24ac0644 --- /dev/null +++ b/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts @@ -0,0 +1,127 @@ +import type { Request } from 'express' +import { createMock } from '@golevelup/ts-jest' +import { baseClientFactory } from '../../testutils/factories' +import { dateISOString, offsetNow } from '../../utils/utils' +import { mapEditBaseClientDetailsForm } from '../index' + +const formRequest = (form: Record) => { + return createMock({ body: form }) +} + +describe('mapEditBaseClientDetailsForm', () => { + describe('accessTokenValidity', () => { + it.each([ + ['1200', '1200', '', 1200], + ['3600', '3600', '', 3600], + ['43200', '43200', '', 43200], + ['Null selection', null, '', 0], + ['Custom with value', 'custom', '25', 25], + ['Custom with string value', 'custom', 'blah', 0], + ])( + 'sets accessTokenValidity based on combination of dropdown selection %s and text box contents %s', + (_: string, selection: string, text: string, expected: number) => { + const baseClient = baseClientFactory.build({ accessTokenValidity: null }) + const request = formRequest({ accessTokenValidity: selection, customAccessTokenValidity: text }) + + const updated = mapEditBaseClientDetailsForm(baseClient, request) + + expect(updated.accessTokenValidity).toEqual(expected) + }, + ) + }) + + describe('expiry', () => { + it('defaults to null if expiry is null', () => { + const expiry: boolean = null + const expiryDays: string = null + const baseClient = baseClientFactory.build({ accessTokenValidity: null }) + const request = formRequest({ expiry, expiryDays }) + + const presenter = mapEditBaseClientDetailsForm(baseClient, request) + + expect(presenter.config.expiryDate).toBeNull() + }) + + it('defaults to already expired for non-numeric day count', () => { + const expiry: boolean = true + const expiryDays: string = 'blah' + const baseClient = baseClientFactory.build({ accessTokenValidity: null }) + const request = formRequest({ expiry, expiryDays }) + + const presenter = mapEditBaseClientDetailsForm(baseClient, request) + + const expected = dateISOString(offsetNow(0)) + expect(presenter.config.expiryDate).toEqual(expected) + }) + + it('can be in the past (negative expiry days)', () => { + const expiry: boolean = true + const expiryDays: string = '-1' + const baseClient = baseClientFactory.build({ accessTokenValidity: null }) + const request = formRequest({ expiry, expiryDays }) + + const presenter = mapEditBaseClientDetailsForm(baseClient, request) + + const expected = dateISOString(offsetNow(-1)) + expect(presenter.config.expiryDate).toEqual(expected) + }) + + it('can be in the future (positive expiry days)', () => { + const expiry: boolean = true + const expiryDays: string = '1' + const baseClient = baseClientFactory.build({ accessTokenValidity: null }) + const request = formRequest({ expiry, expiryDays }) + + const presenter = mapEditBaseClientDetailsForm(baseClient, request) + + const expected = dateISOString(offsetNow(1)) + expect(presenter.config.expiryDate).toEqual(expected) + }) + }) + + describe('updates details only', () => { + it('updates details only', () => { + // Given a base client with fields populated + const detailedBaseClient = baseClientFactory.build({ + accessTokenValidity: 3600, + deployment: { + team: 'deployment team', + }, + service: { + serviceName: 'service serviceName', + }, + }) + + // and given an edit request with all fields populated + const request = formRequest({ + baseClientId: detailedBaseClient.baseClientId, + clientType: 'request clientType', + approvedScopes: 'requestscope1,requestscope2', + audit: 'request audit', + grant: 'request grant', + authorities: 'requestauthority1\r\nrequestauthority2', + databaseUsername: 'request databaseUsername', + allowedIPs: 'requestallowedIP1\r\nrequestallowedIP2', + expiry: true, + expiryDays: '1', + }) + + // when the form is mapped + const update = mapEditBaseClientDetailsForm(detailedBaseClient, request) + + // then the client details are updated + expect(update.baseClientId).toEqual(detailedBaseClient.baseClientId) + expect(update.clientType).toEqual('request clientType') + expect(update.scopes).toEqual(['requestscope1', 'requestscope2']) + expect(update.audit).toEqual('request audit') + expect(update.grantType).toEqual('request grant') + expect(update.clientCredentials.authorities).toEqual(['requestauthority1', 'requestauthority2']) + expect(update.clientCredentials.databaseUserName).toEqual('request databaseUsername') + expect(update.config.allowedIPs).toEqual(['requestallowedIP1', 'requestallowedIP2']) + + // but deployment and service details are not updated + expect(update.deployment.team).toEqual(detailedBaseClient.deployment.team) + expect(update.service.serviceName).toEqual(detailedBaseClient.service.serviceName) + }) + }) +}) diff --git a/server/mappers/forms/mapEditBaseClientDetailsForm.ts b/server/mappers/forms/mapEditBaseClientDetailsForm.ts new file mode 100644 index 00000000..2a4491ec --- /dev/null +++ b/server/mappers/forms/mapEditBaseClientDetailsForm.ts @@ -0,0 +1,28 @@ +import type { Request } from 'express' +import { BaseClient } from '../../interfaces/baseClientApi/baseClient' +import { getAccessTokenValiditySeconds, getDayOfExpiry, multiSeparatorSplit } from '../../utils/utils' + +export default (baseClient: BaseClient, request: Request): BaseClient => { + const data = request.body + + const { accessTokenValidity, customAccessTokenValidity } = data + const accessTokenValiditySeconds = getAccessTokenValiditySeconds(accessTokenValidity, customAccessTokenValidity) + const dayOfExpiry = data.expiry ? getDayOfExpiry(data.expiryDays) : null + + return { + ...baseClient, + clientType: data.clientType, + accessTokenValidity: accessTokenValiditySeconds, + scopes: multiSeparatorSplit(data.approvedScopes, [',', '\r\n', '\n']), + audit: data.audit, + grantType: data.grant, + clientCredentials: { + authorities: multiSeparatorSplit(data.authorities, [',', '\r\n', '\n']), + databaseUserName: data.databaseUsername, + }, + config: { + allowedIPs: multiSeparatorSplit(data.allowedIPs, [',', '\r\n', '\n']), + expiryDate: dayOfExpiry, + }, + } +} diff --git a/server/mappers/index.ts b/server/mappers/index.ts index ef05159f..96004377 100644 --- a/server/mappers/index.ts +++ b/server/mappers/index.ts @@ -8,6 +8,8 @@ import mapUpdateBaseClientRequest from './baseClientApi/updateBaseClient' import mapUpdateBaseClientDeploymentRequest from './baseClientApi/updateBaseClientDeployment' import mapListClientInstancesResponse from './baseClientApi/listClientInstances' import mapCreateBaseClientForm from './forms/mapCreateBaseClientForm' +import mapEditBaseClientDetailsForm from './forms/mapEditBaseClientDetailsForm' +import mapEditBaseClientDeploymentForm from './forms/mapEditBaseClientDeploymentForm' export { mapGetBaseClientResponse, @@ -18,4 +20,6 @@ export { mapUpdateBaseClientDeploymentRequest, mapListClientInstancesResponse, mapCreateBaseClientForm, + mapEditBaseClientDetailsForm, + mapEditBaseClientDeploymentForm, } diff --git a/server/routes/baseClientRouter.ts b/server/routes/baseClientRouter.ts index 52407d23..e0b2477f 100644 --- a/server/routes/baseClientRouter.ts +++ b/server/routes/baseClientRouter.ts @@ -22,7 +22,11 @@ export default function baseClientRouter(services: Services): Router { get('/', baseClientController.displayBaseClients()) get('/base-clients/new', baseClientController.displayNewBaseClient()) + get('/base-clients/:baseClientId/deployment', baseClientController.displayEditBaseClientDeployment()) + get('/base-clients/:baseClientId/edit', baseClientController.displayEditBaseClient()) get('/base-clients/:baseClientId', baseClientController.displayBaseClient()) post('/base-clients/new', baseClientController.createBaseClient()) + post('/base-clients/:baseClientId/deployment', baseClientController.updateBaseClientDeployment()) + post('/base-clients/:baseClientId/edit', baseClientController.updateBaseClientDetails()) return router } diff --git a/server/testutils/factories/responses/getBaseClientResponse.ts b/server/testutils/factories/responses/getBaseClientResponse.ts index eaf0b87c..f2b7449b 100644 --- a/server/testutils/factories/responses/getBaseClientResponse.ts +++ b/server/testutils/factories/responses/getBaseClientResponse.ts @@ -11,4 +11,16 @@ export default Factory.define(() => ({ databaseUserName: 'databaseUserName', validDays: 1, accessTokenValidityMinutes: 60, + deployment: { + team: 'deployment team', + teamContact: 'deployment team contact', + teamSlack: 'deployment team slack', + hosting: 'deployment hosting', + namespace: 'deployment namespace', + deployment: 'deployment deployment', + secretName: 'deployment secret name', + clientIdKey: 'deployment client id key', + secretKey: 'deployment secret key', + deploymentInfo: 'deployment deployment info', + }, })) diff --git a/server/utils/utils.ts b/server/utils/utils.ts index 8d1af370..cb18e838 100644 --- a/server/utils/utils.ts +++ b/server/utils/utils.ts @@ -57,6 +57,27 @@ export const offsetNow = (days: number) => { export const offsetDate = (date: Date, days: number) => { const newDate = new Date(date) - newDate.setDate(newDate.getDate() + days) + newDate.setDate(date.getDate() + days) return newDate } + +export const parseIntWithDefault = (value: string, defaultValue: number) => { + const parsed = parseInt(value, 10) + return Number.isNaN(parsed) ? defaultValue : parsed +} + +export const getDayOfExpiry = (daysLeft: string) => { + const daysRemainingInt = parseIntWithDefault(daysLeft, 0) + return offsetNow(daysRemainingInt).toISOString().split('T')[0] +} + +export const dateISOString = (date: Date) => { + return date.toISOString().split('T')[0] +} + +export const getAccessTokenValiditySeconds = (accessTokenValidity: string, customAccessTokenValidity?: string) => { + if (accessTokenValidity === 'custom' && customAccessTokenValidity) { + return parseIntWithDefault(customAccessTokenValidity, 0) + } + return parseIntWithDefault(accessTokenValidity, 0) +} diff --git a/server/views/pages/base-client.njk b/server/views/pages/base-client.njk index 7aea5c86..83d27b8b 100644 --- a/server/views/pages/base-client.njk +++ b/server/views/pages/base-client.njk @@ -63,7 +63,7 @@

Base client details

@@ -211,7 +211,7 @@ { text: "Allowed IPs" },{ - text: toLinesHtml(baseClient.config.allowedIPs) + html: toLinesHtml(baseClient.config.allowedIPs) } ] ] @@ -284,7 +284,7 @@

Deployment details

diff --git a/server/views/pages/edit-base-client-deployment.njk b/server/views/pages/edit-base-client-deployment.njk new file mode 100644 index 00000000..06fb71ff --- /dev/null +++ b/server/views/pages/edit-base-client-deployment.njk @@ -0,0 +1,187 @@ +{% extends "../partials/layout.njk" %} + +{% set pageTitle = applicationName + " - Add base client" %} +{% set mainClasses = "app-container govuk-body" %} + +{% set pageName="Home" %} +{% set bodyClasses = "extra-wide" %} + +{% block header %} + {% include "partials/header.njk" %} +{% endblock %} + +{% from "govuk/components/input/macro.njk" import govukInput %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/label/macro.njk" import govukLabel %} +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/textarea/macro.njk" import govukTextarea %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} + + +{% block content %} + {{ govukBackLink({ + text: "Back", + href: "/base-clients/" + baseClient.baseClientId + }) }} + +
+
+ + + +

+ Edit deployment details +

+ + {{ govukSummaryList({ + rows: [ + { + key: { + text: "Base client id" + }, + value: { + text: baseClient.baseClientId + } + } + ] + }) }} + + +

Contact

+ +
+
+ + {{ govukInput({ + label: { + text: "Team" + }, + id: "team", + name: "team", + value: baseClient.deployment.team + }) }} + + {{ govukInput({ + label: { + text: "Team contact" + }, + id: "team-contact", + name: "teamContact", + value: baseClient.deployment.teamContact + }) }} + + {{ govukInput({ + label: { + text: "Team slack channel" + }, + id: "team-slack", + name: "teamSlack", + value: baseClient.deployment.teamSlack + }) }} +
+
+ + +

Platform

+ + {{ govukRadios({ + classes: "govuk-radios--inline", + name: "hosting", + fieldset: { + legend: { + text: "Hosting", + isPageHeading: false + } + }, + items: [ + { + value: "CLOUDPLATFORM", + text: "Cloud Platform" + }, + { + value: "OTHER", + text: "Other" + } + ], + value: baseClient.deployment.hosting + }) }} + +
+
+ {{ govukInput({ + label: { + text: "Namespace" + }, + id: "namespace", + name: "namespace", + value: baseClient.deployment.namespace + }) }} + + {{ govukInput({ + label: { + text: "Deployment" + }, + id: "deployment", + name: "deployment", + value: baseClient.deployment.deployment + }) }} + + {{ govukInput({ + label: { + text: "Secret name" + }, + id: "secret-name", + name: "secretName", + value: baseClient.deployment.secretName + }) }} + + {{ govukInput({ + label: { + text: "Client id key" + }, + id: "client-id-key", + name: "clientIdKey", + value: baseClient.deployment.clientIdKey + }) }} + + {{ govukInput({ + label: { + text: "Secret key" + }, + id: "secret-key", + name: "secretKey", + value: baseClient.deployment.secretKey + }) }} + +
+
+ + {{ govukTextarea({ + name: "deploymentInfo", + id: "deployment-info", + label: { + text: "Deployment info", + isPageHeading: false + }, + value: baseClient.deployment.deploymentInfo + }) }} + +
+ {{ govukButton({ + text: "Save", + type: "submit", + preventDoubleClick: true + }) }} + Cancel +
+ +
+ +
+ +{% endblock %} + +{% block pageScripts %} + +{% endblock %} diff --git a/server/views/pages/edit-base-client-details.njk b/server/views/pages/edit-base-client-details.njk new file mode 100644 index 00000000..acec658f --- /dev/null +++ b/server/views/pages/edit-base-client-details.njk @@ -0,0 +1,279 @@ +{% extends "../partials/layout.njk" %} + +{% set pageTitle = applicationName + " - Add base client" %} +{% set mainClasses = "app-container govuk-body" %} + +{% set pageName="Home" %} +{% set bodyClasses = "extra-wide" %} + +{% block header %} + {% include "partials/header.njk" %} +{% endblock %} + +{% from "govuk/components/fieldset/macro.njk" import govukFieldset %} +{% from "govuk/components/input/macro.njk" import govukInput %} +{% from "govuk/components/select/macro.njk" import govukSelect %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/label/macro.njk" import govukLabel %} +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} +{% from "govuk/components/textarea/macro.njk" import govukTextarea %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} + + +{% block content %} + {{ govukBackLink({ + text: "Back", + href: "/base-clients/" + baseClient.baseClientId + }) }} + +
+
+ + + +

+ Edit base client details +

+ +

Client details

+ {{ govukInput({ + label: { + text: "Base client ID" + }, + classes: "govuk-!-width-two-thirds", + id: "base-client-id", + value: baseClient.baseClientId, + disabled: true + }) }} + + {{ govukRadios({ + classes: "govuk-radios--inline", + name: "clientType", + fieldset: { + legend: { + text: "Client type", + isPageHeading: false + } + }, + items: [ + { + value: "SERVICE", + text: "Service" + }, + { + value: "PERSONAL", + text: "Personal" + } + ], + value: baseClient.clientType + }) }} + + {{ govukSelect({ + id: "access-token-validity", + name: "accessTokenValidity", + label: { + text: "Access token validity" + }, + hint: { + text: "Token expiration time in seconds" + }, + classes: "govuk-!-width-one-third", + items: [ + { + value: "1200", + text: "1200 - 20 minutes" + }, + { + value: "3600", + text: "3600 - 1 hour", + selected: true + }, + { + value: "43200", + text: "43200 - 12 hours" + }, + { + value: "custom", + text: "Custom" + } + ], + value: presenter.accessTokenValidityDropdown + }) }} + +
+ {{ govukInput({ + label: { + text: "Custom access token validity in seconds" + }, + classes: "govuk-!-width-one-half", + id: "custom-access-token-validity", + name: "customAccessTokenValidity", + value: presenter.accessTokenValidityText + }) }} +
+ + {{ govukTextarea({ + id: "approved-scopes", + name: "approvedScopes", + label: { + text: "Approved scopes", + isPageHeading: false + }, + hint: { + text: "read, write ..." + }, + value: toLines(baseClient.scopes) + }) }} + +

Audit trail

+ {{ govukTextarea({ + name: "audit", + id: "audit", + label: { + text: "Details", + isPageHeading: false + }, + hint: { + text: "jira tickets, slack messages ..." + }, + value: baseClient.audit + }) }} + +

Grant details

+ + {% if baseClient.grantType == "client_credentials" %} + {{ govukInput({ + label: { + text: "Grant type" + }, + classes: "govuk-!-width-two-thirds", + id: "grant-type", + name: "grantType", + value: "Client credentials", + disabled: true + }) }} + + {{ govukTextarea({ + name: "authorities", + id: "authorities", + label: { + text: "Authorities", + isPageHeading: false + }, + value: toLines(baseClient.clientCredentials.authorities) + }) }} + + {{ govukInput({ + label: { + text: "Database username" + }, + classes: "govuk-!-width-two-thirds", + id: "database-username", + name: "databaseUsername", + value: baseClient.clientCredentials.databaseUsername + }) }} + {% endif %} + + {% if baseClient.grantType == "authorization_code" %} + {{ govukInput({ + label: { + text: "Grant type" + }, + classes: "govuk-!-width-two-thirds", + id: "grant-type", + name: "grantType", + value: "Authorization code", + disabled: true + }) }} + + {{ govukTextarea({ + name: "redirectUris", + id: "redirect-uris", + label: { + text: "Registered redirect URIs", + isPageHeading: false + }, + value: toLines(baseClient.authorizationCode.redirectUris) + }) }} + {{ govukInput({ + label: { + text: "JWT Fields Configuration" + }, + hint: { + text: "Comma separated list of fields, prefixed with + to add, - to remove e.g. +name,-user_id" + }, + classes: "govuk-!-width-one-third", + id: "jwt-fields", + name: "jwtFields", + value: baseClient.authorizationCode.jwtFields + }) }} + + + {{ govukCheckboxes({ + name: "azureAdLoginFlow", + fieldset: { + legend: { + text: "Azure Ad Login Flow", + isPageHeading: false + }, + hint: { + text: "Customize behaviour of Azure AD login" + } + }, + items: [ + { + value: "redirect", + text: "Auto redirect", + checked: baseClient.authorisationCode.azureAdLoginFlow + } + ] + }) }} + {% endif %} + +

Config

+ {{ govukCheckboxes({ + name: "expiry", + items: [ + { + value: "expire", + text: "allow client to expire", + checked: presenter.expiry, + conditional: { + html: "" + } + } + ] + }) }} + + {{ govukTextarea({ + name: "allowedIPs", + id: "allowed-ips", + label: { + text: "Allowed IPs", + isPageHeading: false + }, + hint: { + html: "One IP address/CIDR notation per line
81.134.202.29/32 - mojvpn
35.176.93.186/32 - global-protect
35.178.209.113/32 - cloudplatform-1
3.8.51.207/32 - cloudplatform-2
35.177.252.54/32 - cloudplatform-3" + }, + value: toLines(baseClient.config.allowedIPs) + }) }} + +
+ {{ govukButton({ + text: "Save", + type: "submit", + preventDoubleClick: true + }) }} + Cancel +
+ +
+ +
+ +{% endblock %} + +{% block pageScripts %} + +{% endblock %}