diff --git a/backend/migrations/20250106091714-addLastViewedSLVMessage.js b/backend/migrations/20250106091714-addLastViewedSLVMessage.js index 4ea46c874d..0cf11f7536 100644 --- a/backend/migrations/20250106091714-addLastViewedSLVMessage.js +++ b/backend/migrations/20250106091714-addLastViewedSLVMessage.js @@ -1,17 +1,17 @@ 'use strict'; -const loginTable = { tableName: 'Login', schema: 'cqc' }; +const userTable = { tableName: 'User', schema: 'cqc' }; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - return queryInterface.addColumn(loginTable, 'LastViewedSLVMessage', { + return queryInterface.addColumn(userTable, 'LastViewedSLVMessage', { type: Sequelize.DataTypes.DATE, allowNull: true, }); }, async down(queryInterface) { - return queryInterface.removeColumn(loginTable, 'LastViewedSLVMessage'); + return queryInterface.removeColumn(userTable, 'LastViewedSLVMessage'); }, }; diff --git a/backend/server/models/login.js b/backend/server/models/login.js index 0d0fc370e0..560d4f5a12 100644 --- a/backend/server/models/login.js +++ b/backend/server/models/login.js @@ -81,11 +81,6 @@ module.exports = function (sequelize, DataTypes) { allowNull: false, field: '"AgreedUpdatedTerms"', }, - lastViewedSLVMessage: { - type: DataTypes.DATE, - allowNull: true, - field: 'LastViewedSLVMessage', - }, }, { tableName: '"Login"', diff --git a/backend/server/models/user.js b/backend/server/models/user.js index 4b321e1fd4..8515e6ede0 100644 --- a/backend/server/models/user.js +++ b/backend/server/models/user.js @@ -274,6 +274,11 @@ module.exports = function (sequelize, DataTypes) { allowNull: true, field: '"CanManageWdfClaimsChangedBy"', }, + lastViewedSLVMessage: { + type: DataTypes.DATE, + allowNull: true, + field: 'LastViewedSLVMessage', + }, }, { tableName: '"User"', @@ -542,5 +547,16 @@ module.exports = function (sequelize, DataTypes) { return userFound; }; + User.setDateForLastViewedSLVMessage = async function (userUid) { + return await this.update( + { lastViewedSLVMessage: new Date() }, + { + where: { + uid: userUid, + }, + }, + ); + }; + return User; }; diff --git a/backend/server/routes/accounts/user.js b/backend/server/routes/accounts/user.js index df9db3b72d..dacc876b23 100644 --- a/backend/server/routes/accounts/user.js +++ b/backend/server/routes/accounts/user.js @@ -809,7 +809,7 @@ router.route('/swap/establishment/notification/:nmsdId').get(async (req, res) => let notificationArr = []; const establishmentNotifications = await notifications.selectNotificationByEstablishment(req.establishmentUid); - if(establishmentNotifications) notificationArr.push(establishmentNotifications); + if (establishmentNotifications) notificationArr.push(establishmentNotifications); return res.status(200).send(notificationArr); } } catch (e) { @@ -911,6 +911,27 @@ const swapEstablishment = async (req, res) => { .json(response); }; +const updateLastViewedSLVMessage = async (req, res) => { + try { + const userUid = req.params?.userUid; + + if (!isCorrectlyFormattedUid(userUid)) { + return res.status(400).send('User UID invalid'); + } + + await models.user.setDateForLastViewedSLVMessage(userUid); + + return res.status(200); + } catch (error) { + return res.status(500).send('Failed to update last viewed date'); + } +}; + +const isCorrectlyFormattedUid = (userUid) => { + const uuidRegex = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/; + return uuidRegex.test(userUid.toUpperCase()); +}; + router.route('/').get(return200); router.route('/admin').get(Authorization.isAdmin, listAdminUsers); router.route('/admin/:userId').get(Authorization.isAdmin, getUser); @@ -945,6 +966,7 @@ router.route('/my/establishments').get(Authorization.isAuthorised, listEstablish router.route('/admin/:userId').get(Authorization.isAuthorised, getUser); router.use('/my/notifications', Authorization.isAuthorised); router.route('/my/notifications').get(listNotifications); +router.route('/update-last-viewed-slv-message/:userUid').post(Authorization.isAuthorised, updateLastViewedSLVMessage); router.use('/swap/establishment/:id', authLimiter); router.route('/swap/establishment/:id').post(Authorization.isAdmin, swapEstablishment); @@ -955,3 +977,4 @@ module.exports.partAddUser = partAddUser; module.exports.listAdminUsers = listAdminUsers; module.exports.updateUser = updateUser; +module.exports.updateLastViewedSLVMessage = updateLastViewedSLVMessage; diff --git a/backend/server/routes/login.js b/backend/server/routes/login.js index 77d2216189..1248f688f1 100644 --- a/backend/server/routes/login.js +++ b/backend/server/routes/login.js @@ -65,7 +65,6 @@ router.post('/', async (req, res) => { 'tribalSalt', 'agreedUpdatedTerms', 'status', - 'lastViewedSLVMessage', ], include: [ { @@ -80,6 +79,7 @@ router.post('/', async (req, res) => { 'UserRoleValue', 'registrationSurveyCompleted', 'tribalId', + 'lastViewedSLVMessage', ], include: [ { @@ -286,7 +286,7 @@ router.post('/', async (req, res) => { migratedUser, }, establishmentUser.user.registrationSurveyCompleted, - establishmentUser.lastViewedSLVMessage, + establishmentUser.user.lastViewedSLVMessage, ); await models.sequelize.transaction(async (t) => { diff --git a/backend/server/test/unit/routes/accounts/user.spec.js b/backend/server/test/unit/routes/accounts/user.spec.js index bea679977e..bfe38bf710 100644 --- a/backend/server/test/unit/routes/accounts/user.spec.js +++ b/backend/server/test/unit/routes/accounts/user.spec.js @@ -3,8 +3,15 @@ const expect = chai.expect; const sinon = require('sinon'); const httpMocks = require('node-mocks-http'); -const { meetsMaxUserLimit, partAddUser, listAdminUsers, updateUser } = require('../../../../routes/accounts/user'); +const { + meetsMaxUserLimit, + partAddUser, + listAdminUsers, + updateUser, + updateLastViewedSLVMessage, +} = require('../../../../routes/accounts/user'); const User = require('../../../../models/classes/user').User; +const models = require('../../../../models'); describe('user.js', () => { let req; @@ -345,4 +352,43 @@ describe('user.js', () => { }); }); }); + + describe('updateLastViewedSLVMessage', () => { + let req; + let res; + + beforeEach(() => { + req = httpMocks.createRequest(); + res = httpMocks.createResponse(); + }); + + it('should return 200 response if userUid in params is valid and database call successful', async () => { + req.params = { userUid: '6b6885fa-340d-4d59-8720-c03d8845e603' }; + sinon.stub(models.user, 'setDateForLastViewedSLVMessage').returns(null); + + await updateLastViewedSLVMessage(req, res); + + expect(res.statusCode).to.equal(200); + }); + + it('should return 400 response if userUid in params invalid', async () => { + req.params = { userUid: 'invalid-uid' }; + sinon.stub(models.user, 'setDateForLastViewedSLVMessage').returns(null); + + await updateLastViewedSLVMessage(req, res); + + expect(res.statusCode).to.equal(400); + expect(res._getData()).to.deep.equal('User UID invalid'); + }); + + it('should return 500 response if unexpected error', async () => { + req.params = { userUid: '6b6885fa-340d-4d59-8720-c03d8845e603' }; + sinon.stub(models.user, 'setDateForLastViewedSLVMessage').throws(); + + await updateLastViewedSLVMessage(req, res); + + expect(res.statusCode).to.equal(500); + expect(res._getData()).to.deep.equal('Failed to update last viewed date'); + }); + }); }); diff --git a/frontend/src/app/core/model/userDetails.model.ts b/frontend/src/app/core/model/userDetails.model.ts index ce5f07518d..7b7dc39535 100644 --- a/frontend/src/app/core/model/userDetails.model.ts +++ b/frontend/src/app/core/model/userDetails.model.ts @@ -25,6 +25,7 @@ export interface UserDetails { updatedBy?: string; username?: string; canManageWdfClaims?: boolean; + lastViewedSLVMessage?: Date; } export enum UserStatus { diff --git a/frontend/src/app/core/services/user.service.ts b/frontend/src/app/core/services/user.service.ts index bb51025e52..fd1acc904f 100644 --- a/frontend/src/app/core/services/user.service.ts +++ b/frontend/src/app/core/services/user.service.ts @@ -3,7 +3,7 @@ import { Injectable, isDevMode } from '@angular/core'; import { GetWorkplacesResponse } from '@core/model/my-workplaces.model'; import { URLStructure } from '@core/model/url.model'; import { UserDetails } from '@core/model/userDetails.model'; -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; @@ -184,7 +184,10 @@ export class UserService { .pipe(map((response) => response.users)); } - public updateSLVMessage(userUid: string) { - return of(null); + public updateSLVMessage() { + return this.http.post( + `${environment.appRunnerEndpoint}/api/user/update-last-viewed-slv-message/${this.loggedInUser.uid}`, + {}, + ); } } diff --git a/frontend/src/app/features/login/login.component.ts b/frontend/src/app/features/login/login.component.ts index 2f24e63ab1..798e79fab9 100644 --- a/frontend/src/app/features/login/login.component.ts +++ b/frontend/src/app/features/login/login.component.ts @@ -158,6 +158,9 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { this.subscriptions.add( this.authService.authenticate(username, password).subscribe( (response) => { + const isPreviousUser = this.authService.isPreviousUser(username); + this.authService.clearPreviousUser(); + if (response.body.establishment?.uid) { this.establishmentService.establishmentId = response.body.establishment.uid; } @@ -177,8 +180,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { return this.router.navigate(['/registration-survey']); } - if (this.authService.isPreviousUser(username) && this.authService.redirectLocation) { - this.authService.clearPreviousUser(); + if (isPreviousUser && this.authService.redirectLocation) { return this.router.navigateByUrl(this.authService.redirectLocation); } diff --git a/frontend/src/app/features/login/starters-leavers-vacancies-login-message/starters-leavers-vacancies-login-message.component.ts b/frontend/src/app/features/login/starters-leavers-vacancies-login-message/starters-leavers-vacancies-login-message.component.ts index 5684dd95a9..47513f64cf 100644 --- a/frontend/src/app/features/login/starters-leavers-vacancies-login-message/starters-leavers-vacancies-login-message.component.ts +++ b/frontend/src/app/features/login/starters-leavers-vacancies-login-message/starters-leavers-vacancies-login-message.component.ts @@ -10,10 +10,8 @@ export class StartersLeaversVacanciesLoginMessageComponent implements OnDestroy private subscriptions: Subscription = new Subscription(); constructor(private userService: UserService) { - const userUid = this.userService.loggedInUser.uid; - this.subscriptions.add( - this.userService.updateSLVMessage(userUid).subscribe((res) => { + this.userService.updateSLVMessage().subscribe((res) => { () => {}; }), );