diff --git a/UI/src/app/app.component.spec.ts b/UI/src/app/app.component.spec.ts index 9151efa813..c66e88082b 100644 --- a/UI/src/app/app.component.spec.ts +++ b/UI/src/app/app.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { SharedService } from './services/shared.service'; import { GetAuthService } from './services/getauth.service'; @@ -9,8 +9,7 @@ import { Router, ActivatedRoute, NavigationEnd, RouteConfigLoadEnd, RouteConfigL import { PrimeNGConfig } from 'primeng/api'; import { HelperService } from './services/helper.service'; import { Location } from '@angular/common'; -import { of, Subject } from 'rxjs'; -import { CommonModule, DatePipe } from '@angular/common'; +import { of, Subject, throwError } from 'rxjs'; describe('AppComponent', () => { let component: AppComponent; @@ -40,7 +39,7 @@ describe('AppComponent', () => { ]); getAuthServiceMock = jasmine.createSpyObj('GetAuthService', ['checkAuth']); - const httpServiceMock = jasmine.createSpyObj('HttpService', [], { currentVersion: '1.0.0' }); + const httpServiceMock = jasmine.createSpyObj('HttpService', ['handleRestoreUrl'], { currentVersion: '1.0.0' }); const googleAnalyticsServiceMock = jasmine.createSpyObj('GoogleAnalyticsService', ['setPageLoad']); const getAuthorizationServiceMock = jasmine.createSpyObj('GetAuthorizationService', ['getRole']); const helperServiceMock = jasmine.createSpyObj('HelperService', ['setBackupOfUrlFilters']); @@ -159,11 +158,6 @@ describe('AppComponent', () => { }); }); - xit('should decode and set state filters from URL hash', () => { - component.ngOnInit(); - expect(sharedService.setBackupOfUrlFilters).toHaveBeenCalledWith('SomeEncodedData'); - }); - it('should navigate to dashboard if no shared link exists', () => { localStorage.removeItem('shared_link'); @@ -172,50 +166,6 @@ describe('AppComponent', () => { expect(router.navigate).toHaveBeenCalledWith(['./dashboard/']); }); - xit('should navigate to error page if user lacks project access', () => { - const validStateFilters = btoa(JSON.stringify({ primary_level: [{ basicProjectConfigId: '123' }] })); - localStorage.setItem('shared_link', `http://example.com?stateFilters=${validStateFilters}`); - localStorage.setItem( - 'currentUserDetails', - JSON.stringify({ - projectsAccess: [ - { - projects: [{ projectId: '456' }], - }, - ], - authorities: ['ROLE_SUPERADMIN'] - }) - ); - - component.ngOnInit(); - - expect(router.navigate).toHaveBeenCalledWith(['/dashboard/Error']); - /* expect(sharedService.raiseError).toHaveBeenCalledWith({ - status: 901, - message: 'No project access.', - }); */ - }); - - xit('should navigate to shared link if user has access to all projects', () => { - const validStateFilters = btoa(JSON.stringify({ primary_level: [{ basicProjectConfigId: '123' }] })); - localStorage.setItem('shared_link', `http://example.com?stateFilters=${validStateFilters}`); - localStorage.setItem( - 'currentUserDetails', - JSON.stringify({ - projectsAccess: [ - { - projects: [{ basicProjectConfigId: '123' }], - }, - ], - authorities: ['ROLE_SUPERADMIN'] - }) - ); - - component.ngOnInit(); - - expect(router.navigate).toHaveBeenCalledWith([`http://example.com?stateFilters=${validStateFilters}`]); - }); - it('should initialize component correctly and call ngOnInit', () => { // const routerSpy = spyOn(router, 'navigate'); // const getAuthSpy = spyOn(getAuthService, 'checkAuth').and.returnValue(true); @@ -242,36 +192,98 @@ describe('AppComponent', () => { expect(header.classList.contains('scrolled')).toBeFalse(); }); - xit('should decode stateFilters and set backup filter state', () => { - const mockParam = btoa(JSON.stringify({ primary_level: [{ labelName: 'Test', nodeId: 'node-1' }] })); - localStorage.setItem('shared_link', `http://example.com/?stateFilters=${mockParam}`); - // const serviceSpy = spyOn(sharedService, 'setBackupOfFilterSelectionState'); + it('should navigate to default dashboard if no shared link is found', () => { + localStorage.removeItem('shared_link'); + // const routerSpy = spyOn(router, 'navigate'); component.ngOnInit(); - expect(sharedServiceMock.setBackupOfFilterSelectionState).toHaveBeenCalledWith({ primary_level: [{ labelName: 'Test', nodeId: 'node-1' }] }); + expect(routerMock.navigate).toHaveBeenCalledWith(['./dashboard/']); }); - xit('should handle invalid URL and redirect to error page', () => { - const invalidParam = '12asdasd213131'; - localStorage.setItem('shared_link', `http://example.com/?stateFilters=${invalidParam}`); + it('should navigate to the provided URL if the user has access to all projects', fakeAsync(() => { + const decodedStateFilters = JSON.stringify({ + parent_level: { basicProjectConfigId: 'project1', labelName: 'Project' }, + primary_level: [] + }); + const currentUserProjectAccess = [{ projectId: 'project1' }]; + const url = 'http://example.com'; + + spyOn(component, 'urlRedirection').and.callThrough(); - component.ngOnInit(); + component.urlRedirection(decodedStateFilters, currentUserProjectAccess, url, true); + + tick(); + expect(component.urlRedirection).toHaveBeenCalledWith(decodedStateFilters, currentUserProjectAccess, url, true); + expect(router.navigate).toHaveBeenCalledWith([url]); + })); - expect(routerMock.navigate).toHaveBeenCalledWith(['/dashboard/Error']); + it('should navigate to the error page if the user does not have access to the project', fakeAsync(() => { + const decodedStateFilters = JSON.stringify({ + parent_level: { basicProjectConfigId: 'project1', labelName: 'Project' }, + primary_level: [] + }); + const currentUserProjectAccess = [{ projectId: 'project2' }]; + const url = 'http://example.com'; + + spyOn(component, 'urlRedirection').and.callThrough(); + + component.urlRedirection(decodedStateFilters, currentUserProjectAccess, url, false); + + tick(); + expect(component.urlRedirection).toHaveBeenCalledWith(decodedStateFilters, currentUserProjectAccess, url, false); + expect(router.navigate).toHaveBeenCalledWith(['/dashboard/Error']); expect(sharedServiceMock.raiseError).toHaveBeenCalledWith({ - status: 900, - message: 'Invalid URL.', + status: 901, + message: 'No project access.' }); - }); + })); - it('should navigate to default dashboard if no shared link is found', () => { - localStorage.removeItem('shared_link'); - // const routerSpy = spyOn(router, 'navigate'); + // Test cases for scroll behavior + describe('scroll behavior', () => { + let header: HTMLElement; - component.ngOnInit(); + beforeEach(() => { + header = document.createElement('div'); + header.classList.add('header'); + document.body.appendChild(header); + }); - expect(routerMock.navigate).toHaveBeenCalledWith(['./dashboard/']); + afterEach(() => { + document.body.removeChild(header); + }); + + it('should add scrolled class when window is scrolled beyond 200px', () => { + header.classList.add('scrolled'); + // Set the scroll position + window.scrollTo(0, 201); + + // Dispatch a scroll event + window.dispatchEvent(new Event('scroll')); + + expect(header.classList.contains('scrolled')).toBeTrue(); + }); + + it('should remove scrolled class when window is scrolled less than 200px', () => { + window.scrollTo(0, 199); + window.dispatchEvent(new Event('scroll')); + expect(header.classList.contains('scrolled')).toBeFalse(); + }); + }); + + // Test cases for authorization + describe('authorization', () => { + it('should set authorized flag based on auth service response', () => { + getAuthService.checkAuth.and.returnValue(false); + component.ngOnInit(); + expect(component.authorized).toBeFalse(); + }); + + it('should clear newUI from localStorage on init', () => { + localStorage.setItem('newUI', 'some-value'); + component.ngOnInit(); + expect(localStorage.getItem('newUI')).toBeNull(); + }); }); }); diff --git a/UI/src/app/app.component.ts b/UI/src/app/app.component.ts index b3ba0a01bd..1b27e3711c 100644 --- a/UI/src/app/app.component.ts +++ b/UI/src/app/app.component.ts @@ -24,8 +24,9 @@ import { GoogleAnalyticsService } from './services/google-analytics.service'; import { GetAuthorizationService } from './services/get-authorization.service'; import { Router, RouteConfigLoadStart, RouteConfigLoadEnd, NavigationEnd, ActivatedRoute } from '@angular/router'; import { PrimeNGConfig } from 'primeng/api'; -import { HelperService } from './services/helper.service'; import { Location } from '@angular/common'; +import { catchError } from 'rxjs/operators'; +import { throwError } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -40,6 +41,7 @@ export class AppComponent implements OnInit { refreshCounter: number = 0; self: any = this; selectedTab: string = ''; + @HostListener('window:scroll', ['$event']) onScroll(event) { const header = document.querySelector('.header'); @@ -51,7 +53,7 @@ export class AppComponent implements OnInit { } constructor(public router: Router, private service: SharedService, private getAuth: GetAuthService, private httpService: HttpService, private primengConfig: PrimeNGConfig, - public ga: GoogleAnalyticsService, private authorisation: GetAuthorizationService, private route: ActivatedRoute, private helperService: HelperService, private location: Location) { + public ga: GoogleAnalyticsService, private authorisation: GetAuthorizationService, private route: ActivatedRoute, private location: Location) { this.authorized = this.getAuth.checkAuth(); } @@ -62,36 +64,69 @@ export class AppComponent implements OnInit { this.route.queryParams .subscribe(params => { if (!this.refreshCounter) { - let param = params['stateFilters']; - if (param?.length) { - try { - let selectedTab = this.location.path(); - selectedTab = selectedTab?.split('/')[2] ? selectedTab?.split('/')[2] : 'iteration'; - selectedTab = selectedTab?.split(' ').join('-').toLowerCase(); - this.selectedTab = selectedTab.split('?statefilters=')[0]; - this.service.setSelectedBoard(this.selectedTab); - - param = atob(param); - console.log('param', param); - // param = param.replace(/###/gi, '___'); - - const kpiFilterParam = params['kpiFilters']; - if (kpiFilterParam) { - const kpiFilterParamDecoded = atob(kpiFilterParam); - const kpiFilterValFromUrl = (kpiFilterParamDecoded && JSON.parse(kpiFilterParamDecoded)) ? JSON.parse(kpiFilterParamDecoded) : this.service.getKpiSubFilterObj(); - this.service.setKpiSubFilterObj(kpiFilterValFromUrl); - } - - this.service.setBackupOfFilterSelectionState(JSON.parse(param)); - this.refreshCounter++; - } catch (error) { - this.router.navigate(['/dashboard/Error']); // Redirect to the error page - setTimeout(() => { - this.service.raiseError({ - status: 900, - message: 'Invalid URL.' + let stateFiltersParam = params['stateFilters']; + let kpiFiltersParam = params['kpiFilters']; + + if (stateFiltersParam?.length) { + let selectedTab = decodeURIComponent(this.location.path()); + selectedTab = selectedTab?.split('/')[2] ? selectedTab?.split('/')[2] : 'iteration'; + selectedTab = selectedTab?.split(' ').join('-').toLowerCase(); + this.selectedTab = selectedTab.split('?statefilters=')[0]; + this.service.setSelectedBoard(this.selectedTab); + + if (stateFiltersParam?.length <= 8 && kpiFiltersParam?.length <= 8) { + this.httpService.handleRestoreUrl(stateFiltersParam, kpiFiltersParam) + .pipe( + catchError((error) => { + this.router.navigate(['/dashboard/Error']); // Redirect to the error page + setTimeout(() => { + this.service.raiseError({ + status: 900, + message: error.message || 'Invalid URL.' + }); + }); + + return throwError(error); // Re-throw the error so it can be caught by a global error handler if needed + }) + ) + .subscribe((response: any) => { + if (response.success) { + const longKPIFiltersString = response.data['longKPIFiltersString']; + const longStateFiltersString = response.data['longStateFiltersString']; + stateFiltersParam = atob(longStateFiltersString); + // stateFiltersParam = stateFiltersParam.replace(/###/gi, '___'); + + // const kpiFiltersParam = params['kpiFilters']; + if (longKPIFiltersString) { + const kpiFilterParamDecoded = atob(longKPIFiltersString); + + const kpiFilterValFromUrl = (kpiFilterParamDecoded && JSON.parse(kpiFilterParamDecoded)) ? JSON.parse(kpiFilterParamDecoded) : this.service.getKpiSubFilterObj(); + this.service.setKpiSubFilterObj(kpiFilterValFromUrl); + } + + this.service.setBackupOfFilterSelectionState(JSON.parse(stateFiltersParam)); + this.refreshCounter++; + } }); - }, 100); + } else { + try { + stateFiltersParam = atob(stateFiltersParam); + if (kpiFiltersParam) { + const kpiFilterParamDecoded = atob(kpiFiltersParam); + const kpiFilterValFromUrl = (kpiFilterParamDecoded && JSON.parse(kpiFilterParamDecoded)) ? JSON.parse(kpiFilterParamDecoded) : this.service.getKpiSubFilterObj(); + this.service.setKpiSubFilterObj(kpiFilterValFromUrl); + } + this.service.setBackupOfFilterSelectionState(JSON.parse(stateFiltersParam)); + this.refreshCounter++; + } catch (error) { + this.router.navigate(['/dashboard/Error']); // Redirect to the error page + setTimeout(() => { + this.service.raiseError({ + status: 900, + message: 'Invalid URL.' + }); + }, 100); + } } } } @@ -127,44 +162,76 @@ export class AppComponent implements OnInit { // Extract query parameters const queryParams = new URLSearchParams(url.split('?')[1]); const stateFilters = queryParams.get('stateFilters'); + const kpiFilters = queryParams.get('kpiFilters'); if (stateFilters && stateFilters.length > 0) { - const decodedStateFilters = atob(stateFilters); - const stateFiltersObj = JSON.parse(decodedStateFilters); - - // console.log('Decoded State Filters Object:', stateFiltersObj); - let stateFilterObj = []; - let projectLevelSelected = false; - if (typeof stateFiltersObj['parent_level'] === 'object' && Object.keys(stateFiltersObj['parent_level']).length > 0) { - stateFilterObj = [stateFiltersObj['parent_level']]; + let decodedStateFilters: string = ''; + // let stateFiltersObj: Object = {}; + + if (stateFilters?.length <= 8) { + this.httpService.handleRestoreUrl(stateFilters, kpiFilters) + .pipe( + catchError((error) => { + this.router.navigate(['/dashboard/Error']); + setTimeout(() => { + this.service.raiseError({ + status: 900, + message: error.message || 'Invalid URL.', + }); + }, 100); + return throwError(error); // Re-throw the error so it can be caught by a global error handler if needed + }) + ) + .subscribe((response: any) => { + if (response.success) { + const longStateFiltersString = response.data['longStateFiltersString']; + decodedStateFilters = atob(longStateFiltersString); + this.urlRedirection(decodedStateFilters, currentUserProjectAccess, url, ifSuperAdmin); + } + }); } else { - stateFilterObj = stateFiltersObj['primary_level']; - } - - projectLevelSelected = stateFilterObj?.length && stateFilterObj[0]?.labelName?.toLowerCase() === 'project'; - - // Check if user has access to all project in stateFiltersObj['primary_level'] - const hasAccessToAll = ifSuperAdmin || stateFilterObj.every(filter => - currentUserProjectAccess?.some(project => project.projectId === filter.basicProjectConfigId) - ); - - if (projectLevelSelected) { - if (hasAccessToAll) { - this.router.navigate([JSON.parse(JSON.stringify(url))]); - } else { - this.router.navigate(['/dashboard/Error']); - setTimeout(() => { - this.service.raiseError({ - status: 901, - message: 'No project access.', - }); - }, 100); - } + decodedStateFilters = atob(stateFilters); + this.urlRedirection(decodedStateFilters, currentUserProjectAccess, url, ifSuperAdmin); } } + } else { this.router.navigate(['./dashboard/']); } } + urlRedirection(decodedStateFilters, currentUserProjectAccess, url, ifSuperAdmin) { + const stateFiltersObjLocal = JSON.parse(decodedStateFilters); + + let stateFilterObj = []; + let projectLevelSelected = false; + if (typeof stateFiltersObjLocal['parent_level'] === 'object' && Object.keys(stateFiltersObjLocal['parent_level']).length > 0) { + stateFilterObj = [stateFiltersObjLocal['parent_level']]; + } else { + stateFilterObj = stateFiltersObjLocal['primary_level']; + } + + projectLevelSelected = stateFilterObj?.length && stateFilterObj[0]?.labelName?.toLowerCase() === 'project'; + + // Check if user has access to all project in stateFiltersObjLocal['primary_level'] + const hasAllProjectAccess = stateFilterObj.every(filter => + currentUserProjectAccess?.some(project => project.projectId === filter.basicProjectConfigId) + ); + + // Superadmin have all project access hence no need to check project for superadmin + const hasAccessToAll = ifSuperAdmin || hasAllProjectAccess; + + if (projectLevelSelected) { + if (hasAccessToAll) { + this.router.navigate([url]); + } else { + this.router.navigate(['/dashboard/Error']); + this.service.raiseError({ + status: 901, + message: 'No project access.', + }); + } + } + } + } diff --git a/UI/src/app/authentication/login/login.component.spec.ts b/UI/src/app/authentication/login/login.component.spec.ts index b11b43f020..2ad7d25a13 100644 --- a/UI/src/app/authentication/login/login.component.spec.ts +++ b/UI/src/app/authentication/login/login.component.spec.ts @@ -23,6 +23,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { of, throwError } from 'rxjs'; import { LoginComponent } from './login.component'; import { HttpService } from '../../services/http.service'; +import { HelperService } from 'src/app/services/helper.service'; import { SharedService } from '../../services/shared.service'; import { GoogleAnalyticsService } from 'src/app/services/google-analytics.service'; @@ -32,6 +33,8 @@ describe('LoginComponent', () => { let router: Router; let httpService: HttpService; let sharedService: SharedService; + let helperService: HelperService; + let ga: GoogleAnalyticsService; const mockRouter = { navigate: jasmine.createSpy('navigate'), @@ -44,13 +47,15 @@ describe('LoginComponent', () => { queryParams: of({ sessionExpire: 'Session expired' }), }; - const mockHttpService = jasmine.createSpyObj('HttpService', ['login']); + const mockHttpService = jasmine.createSpyObj('HttpService', ['login', 'handleRestoreUrl']); const mockSharedService = { getCurrentUserDetails: jasmine.createSpy('getCurrentUserDetails'), raiseError: jasmine.createSpy('raiseError'), }; + const mockHelperService = jasmine.createSpyObj('HelperService', ['urlShorteningRedirection']); + const mockGoogleAnalyticsService = { setLoginMethod: jasmine.createSpy('setLoginMethod'), }; @@ -64,6 +69,7 @@ describe('LoginComponent', () => { { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: HttpService, useValue: mockHttpService }, { provide: SharedService, useValue: mockSharedService }, + { provide: HelperService, useValue: mockHelperService }, { provide: GoogleAnalyticsService, useValue: mockGoogleAnalyticsService }, ], }).compileComponents(); @@ -157,31 +163,28 @@ describe('LoginComponent', () => { expect(mockRouter.navigate).toHaveBeenCalledWith(['./dashboard/Config/Profile']); }); - it('should redirect to dashboard on successful login if user has access', () => { - // Mock return values for sharedService methods - mockSharedService.getCurrentUserDetails.and.callFake((key) => { - const mockData = { - user_email: 'test@example.com', - authorities: ['ROLE_SUPERADMIN'], - projectsAccess: [{}], // Non-empty projectsAccess - }; - return mockData[key]; - }); - - // Mock successful login response - const mockResponse = { status: 200, body: {} }; - mockHttpService.login.and.returnValue(of(mockResponse)); - - // Call onSubmit - component.loginForm.controls['username'].setValue('testUser'); - component.loginForm.controls['password'].setValue('testPass'); - component.onSubmit(); + it('should handle 401 status code', () => { + const data = { status: 401, error: { message: 'Unauthorized' } }; + component.performLogin(data, 'username', 'password'); + expect(component.error).toBe('Unauthorized'); + expect(component.f.password.value).toBe(''); + expect(component.submitted).toBe(false); + }); - // Verify navigation - expect(mockRouter.navigate).toHaveBeenCalledWith(['./dashboard/']); + it('should handle 0 status code (Internal Server Error)', () => { + const data = { status: 0 }; + component.performLogin(data, 'username', 'password'); + expect(component.error).toBe('Internal Server Error'); }); - afterEach(() => { - localStorage.removeItem('shared_link'); + it('should handle 200 status code with redirectToProfile() returning true', () => { + spyOn(component, 'redirectToProfile').and.returnValue(true); + const data = { status: 200, body: {} }; + component.performLogin(data, 'username', 'password'); + expect(router.navigate).toHaveBeenCalledWith(['./dashboard/Config/Profile']); }); + + // afterEach(() => { + // localStorage.removeItem('shared_link'); + // }); }); diff --git a/UI/src/app/authentication/login/login.component.ts b/UI/src/app/authentication/login/login.component.ts index 45100bf147..ea25e87321 100644 --- a/UI/src/app/authentication/login/login.component.ts +++ b/UI/src/app/authentication/login/login.component.ts @@ -23,6 +23,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { first } from 'rxjs/operators'; import { SharedService } from '../../services/shared.service'; import { GoogleAnalyticsService } from 'src/app/services/google-analytics.service'; +import { HelperService } from 'src/app/services/helper.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', @@ -40,7 +41,7 @@ export class LoginComponent implements OnInit { - constructor(private formBuilder: UntypedFormBuilder, private route: ActivatedRoute, private router: Router, private httpService: HttpService, private sharedService: SharedService, private ga: GoogleAnalyticsService) { + constructor(private formBuilder: UntypedFormBuilder, private route: ActivatedRoute, private router: Router, private httpService: HttpService, private sharedService: SharedService, private ga: GoogleAnalyticsService, private helperService: HelperService) { } ngOnInit() { @@ -119,49 +120,7 @@ export class LoginComponent implements OnInit { if (this.redirectToProfile()) { this.router.navigate(['./dashboard/Config/Profile']); } else { - const url = localStorage.getItem('shared_link'); - const currentUserProjectAccess = JSON.parse(localStorage.getItem('currentUserDetails'))?.projectsAccess?.length ? JSON.parse(localStorage.getItem('currentUserDetails'))?.projectsAccess[0]?.projects : []; - if (url) { - // Extract query parameters - const queryParams = new URLSearchParams(url.split('?')[1]); - const stateFilters = queryParams.get('stateFilters'); - - if (stateFilters) { - const decodedStateFilters = atob(stateFilters); - const stateFiltersObj = JSON.parse(decodedStateFilters); - - // console.log('Decoded State Filters Object:', stateFiltersObj); - let stateFilterObj = []; - - if (typeof stateFiltersObj['parent_level'] === 'object' && Object.keys(stateFiltersObj['parent_level']).length > 0) { - stateFilterObj = [stateFiltersObj['parent_level']]; - } else { - stateFilterObj = stateFiltersObj['primary_level']; - } - - // Check if user has access to all project in stateFiltersObj['primary_level'] - // SUperadmin have all project access hence no need to check project for superadmin - const hasAccessToAll = this.sharedService.getCurrentUserDetails('authorities')?.includes('ROLE_SUPERADMIN') || stateFilterObj.every(filter => - currentUserProjectAccess?.some(project => project.projectId === filter.basicProjectConfigId) - ) - - if (hasAccessToAll) { - localStorage.removeItem('shared_link'); - this.router.navigate([JSON.parse(JSON.stringify(url))]); - } else { - localStorage.removeItem('shared_link'); - this.router.navigate(['/dashboard/Error']); - setTimeout(() => { - this.sharedService.raiseError({ - status: 901, - message: 'No project access.', - }); - }, 100); - } - } - } else { - this.router.navigate(['./dashboard/']); - } + this.helperService.urlShorteningRedirection(); } } } diff --git a/UI/src/app/dashboardv2/filter-v2/filter-new.component.ts b/UI/src/app/dashboardv2/filter-v2/filter-new.component.ts index 2608a35643..88505ca745 100644 --- a/UI/src/app/dashboardv2/filter-v2/filter-new.component.ts +++ b/UI/src/app/dashboardv2/filter-v2/filter-new.component.ts @@ -14,6 +14,7 @@ import { FeatureFlagsService } from 'src/app/services/feature-toggle.service'; templateUrl: './filter-new.component.html', styleUrls: ['./filter-new.component.css'] }) + export class FilterNewComponent implements OnInit, OnDestroy { filterDataArr = {}; masterData = {}; @@ -1387,11 +1388,30 @@ export class FilterNewComponent implements OnInit, OnDestroy { copyUrlToClipboard(event: Event) { event.stopPropagation(); const url = window.location.href; // Get the current URL - navigator.clipboard.writeText(url).then(() => { - this.showSuccess(); - }).catch(err => { - console.error('Failed to copy URL: ', err); + const queryParams = new URLSearchParams(url.split('?')[1]); + const stateFilters = queryParams.get('stateFilters'); + const kpiFilters = queryParams.get('kpiFilters'); + const payload = { + "longStateFiltersString": stateFilters, + "longKPIFiltersString": kpiFilters + }; + this.httpService.handleUrlShortener(payload).subscribe((response: any) => { + console.log(response); + const shortStateFilterString = response.data.shortStateFiltersString; + const shortKPIFilterString = response.data.shortKPIFilterString; + const shortUrl = `${url.split('?')[0]}?stateFilters=${shortStateFilterString}&kpiFilters=${shortKPIFilterString}`; + navigator.clipboard.writeText(shortUrl).then(() => { + this.showSuccess(); + }).catch(err => { + console.error('Failed to copy URL: ', err); + }); }); + + // navigator.clipboard.writeText(url).then(() => { + // this.showSuccess(); + // }).catch(err => { + // console.error('Failed to copy URL: ', err); + // }); } showSuccess() { diff --git a/UI/src/app/services/app-initializer.service.ts b/UI/src/app/services/app-initializer.service.ts index 2c941e58ed..035577e2bc 100644 --- a/UI/src/app/services/app-initializer.service.ts +++ b/UI/src/app/services/app-initializer.service.ts @@ -20,178 +20,175 @@ import { PageNotFoundComponent } from '../page-not-found/page-not-found.componen import { DashboardV2Component } from '../dashboardv2/dashboard-v2/dashboard-v2.component'; import { ExecutiveV2Component } from '../dashboardv2/executive-v2/executive-v2.component'; import { DecodeUrlGuard } from './decodeURL.guard'; +import { HelperService } from './helper.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class AppInitializerService { - constructor(private sharedService: SharedService, private httpService: HttpService, private router: Router, private featureToggleService: FeatureFlagsService, private http: HttpClient, private route: ActivatedRoute, private ga: GoogleAnalyticsService) { + constructor(private sharedService: SharedService, private httpService: HttpService, private router: Router, private featureToggleService: FeatureFlagsService, private http: HttpClient, private route: ActivatedRoute, private ga: GoogleAnalyticsService, private helperService: HelperService) { + } + commonRoutes: Routes = [ + { path: '', redirectTo: 'iteration', pathMatch: 'full' }, + { path: 'Error', component: ErrorComponent, pathMatch: 'full' }, + // { + // // path: 'iteration', component: IterationComponent, pathMatch: 'full', canActivate: [AccessGuard], + // // data: { + // // feature: "Iteration" + // // } + // }, + { + path: 'kpi-maturity', component: MaturityComponent, pathMatch: 'full', canActivate: [AccessGuard], + data: { + feature: "Maturity" + } } - commonRoutes: Routes = [ - { path: '', redirectTo: 'iteration', pathMatch: 'full' }, - { path: 'Error', component: ErrorComponent, pathMatch: 'full' }, - // { - // // path: 'iteration', component: IterationComponent, pathMatch: 'full', canActivate: [AccessGuard], - // // data: { - // // feature: "Iteration" - // // } - // }, - { - path: 'kpi-maturity', component: MaturityComponent, pathMatch: 'full', canActivate: [AccessGuard], - data: { - feature: "Maturity" - } - } - ]; - routes: Routes = [ - { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + ]; + routes: Routes = [ + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { + path: 'authentication', + loadChildren: () => import('../../app/authentication/authentication.module').then(m => m.AuthenticationModule), + resolve: [Logged], + canActivate: [SSOGuard] + }, + { + path: 'dashboard', component: DashboardV2Component, + canActivateChild: [FeatureGuard], + children: [ + ...this.commonRoutes, { - path: 'authentication', - loadChildren: () => import('../../app/authentication/authentication.module').then(m => m.AuthenticationModule), - resolve: [Logged], - canActivate: [SSOGuard] + path: 'Config', + loadChildren: () => import('../../app/config/config.module').then(m => m.ConfigModule), + data: { + feature: "Config" + } }, { - path: 'dashboard', component: DashboardV2Component, - canActivateChild: [FeatureGuard], - children: [ - ...this.commonRoutes, - { - path: 'Config', - loadChildren: () => import('../../app/config/config.module').then(m => m.ConfigModule), - data: { - feature: "Config" - } - }, - { - path: 'Report', - loadChildren: () => import('../../app/dashboardv2/reports-module/reports-module.module').then(m => m.ReportsModuleModule), - data: { - feature: "Report" - } - }, - { path: ':boardName', component: ExecutiveV2Component, pathMatch: 'full', canActivate: [DecodeUrlGuard] }, - { path: 'Error', component: ErrorComponent, pathMatch: 'full' }, - { path: 'unauthorized-access', component: UnauthorisedAccessComponent, pathMatch: 'full' }, - - ], canActivate: [AuthGuard], + path: 'Report', + loadChildren: () => import('../../app/dashboardv2/reports-module/reports-module.module').then(m => m.ReportsModuleModule), + data: { + feature: "Report" + } }, - { path: 'authentication-fail', component: SsoAuthFailureComponent }, - { path: '**', redirectTo: 'authentication' } - ]; - - routesAuth: Routes = [ - { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { path: ':boardName', component: ExecutiveV2Component, pathMatch: 'full', canActivate: [DecodeUrlGuard] }, + { path: 'Error', component: ErrorComponent, pathMatch: 'full' }, + { path: 'unauthorized-access', component: UnauthorisedAccessComponent, pathMatch: 'full' }, + + ], canActivate: [AuthGuard], + }, + { path: 'authentication-fail', component: SsoAuthFailureComponent }, + { path: '**', redirectTo: 'authentication' } + ]; + + routesAuth: Routes = [ + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { + path: 'dashboard', component: DashboardV2Component, + children: [ + ...this.commonRoutes, + { path: 'Error', component: ErrorComponent, pathMatch: 'full' }, + { path: 'unauthorized-access', component: UnauthorisedAccessComponent, pathMatch: 'full' }, { - path: 'dashboard', component: DashboardV2Component, - children: [ - ...this.commonRoutes, - { path: 'Error', component: ErrorComponent, pathMatch: 'full' }, - { path: 'unauthorized-access', component: UnauthorisedAccessComponent, pathMatch: 'full' }, - { - path: 'Config', - loadChildren: () => import('../../app/config/config.module').then(m => m.ConfigModule), - data: { - feature: "Config" - } - }, - { path: ':boardName', component: ExecutiveV2Component, pathMatch: 'full' }, - - ], canActivate: [AuthGuard], + path: 'Config', + loadChildren: () => import('../../app/config/config.module').then(m => m.ConfigModule), + data: { + feature: "Config" + } }, - { path: 'pageNotFound', component: PageNotFoundComponent }, - { path: '**', redirectTo: 'pageNotFound' } - ]; - - async checkFeatureFlag() { - let loc = window.location.hash ? JSON.parse(JSON.stringify(window.location.hash?.split('#')[1])) : ''; - if (loc && loc.indexOf('authentication') === -1 && loc.indexOf('Error') === -1 && loc.indexOf('Config') === -1) { - localStorage.setItem('shared_link', loc) - } - return new Promise(async (resolve, reject) => { - if (!environment['production']) { - this.featureToggleService.config = this.featureToggleService.loadConfig().then((res) => res); - this.validateToken(loc); - } else { - const env$ = this.http.get('assets/env.json').pipe( - tap(env => { - environment['baseUrl'] = env['baseUrl'] || ''; - environment['SSO_LOGIN'] = env['SSO_LOGIN'] === 'true' ? true : false; - environment['AUTHENTICATION_SERVICE'] = env['AUTHENTICATION_SERVICE'] === 'true' ? true : false; - environment['CENTRAL_LOGIN_URL'] = env['CENTRAL_LOGIN_URL'] || ''; - environment['CENTRAL_API_URL'] = env['CENTRAL_API_URL'] || ''; - environment['MAP_URL'] = env['MAP_URL'] || ''; - environment['RETROS_URL'] = env['RETROS_URL'] || ''; - environment['SPEED_SUITE'] = env['SPEED_SUITE'] === 'true' ? true : false; - if (loc && loc.indexOf('authentication') === -1 && loc.indexOf('Error') === -1 && loc.indexOf('Config') === -1) { - localStorage.setItem('shared_link', loc) - } - this.validateToken(loc); - })); - env$.toPromise().then(async res => { - this.featureToggleService.config = this.featureToggleService.loadConfig().then((res) => res); - }); + { path: ':boardName', component: ExecutiveV2Component, pathMatch: 'full', canActivate: [DecodeUrlGuard] }, + + ], canActivate: [AuthGuard], + }, + { path: 'pageNotFound', component: PageNotFoundComponent }, + { path: '**', redirectTo: 'pageNotFound' } + ]; + + async checkFeatureFlag() { + let loc = window.location.hash ? JSON.parse(JSON.stringify(window.location.hash?.split('#')[1])) : ''; + loc = decodeURIComponent(loc); + if (loc && loc.indexOf('authentication') === -1 && loc.indexOf('Error') === -1 && loc.indexOf('Config') === -1) { + localStorage.setItem('shared_link', (loc)) + } + return new Promise(async (resolve, reject) => { + if (!environment['production']) { + this.featureToggleService.config = this.featureToggleService.loadConfig().then((res) => res); + this.validateToken(loc); + } else { + const env$ = this.http.get('assets/env.json').pipe( + tap(env => { + environment['baseUrl'] = env['baseUrl'] || ''; + environment['SSO_LOGIN'] = env['SSO_LOGIN'] === 'true' ? true : false; + environment['AUTHENTICATION_SERVICE'] = env['AUTHENTICATION_SERVICE'] === 'true' ? true : false; + environment['CENTRAL_LOGIN_URL'] = env['CENTRAL_LOGIN_URL'] || ''; + environment['CENTRAL_API_URL'] = env['CENTRAL_API_URL'] || ''; + environment['MAP_URL'] = env['MAP_URL'] || ''; + environment['RETROS_URL'] = env['RETROS_URL'] || ''; + environment['SPEED_SUITE'] = env['SPEED_SUITE'] === 'true' ? true : false; + if (loc && loc.indexOf('authentication') === -1 && loc.indexOf('Error') === -1 && loc.indexOf('Config') === -1) { + localStorage.setItem('shared_link', loc) } - - - - // load google Analytics script on all instances except local and if customAPI property is true - let addGAScript = await this.featureToggleService.isFeatureEnabled('GOOGLE_ANALYTICS'); - if (addGAScript) { - if (window.location.origin.indexOf('localhost') === -1) { - this.ga.load('gaTagManager').then(data => { - console.log('script loaded ', data); - }) - } + this.validateToken(loc); + })); + env$.toPromise().then(async res => { + this.featureToggleService.config = this.featureToggleService.loadConfig().then((res) => res); + }); + } + + + + // load google Analytics script on all instances except local and if customAPI property is true + let addGAScript = await this.featureToggleService.isFeatureEnabled('GOOGLE_ANALYTICS'); + if (addGAScript) { + if (window.location.origin.indexOf('localhost') === -1) { + this.ga.load('gaTagManager').then(data => { + console.log('script loaded ', data); + }) + } + } + resolve(); + }) + } + + validateToken(location) { + return new Promise((resolve, reject) => { + if (!environment['AUTHENTICATION_SERVICE']) { + this.router.resetConfig([...this.routes]); + this.router.navigate([location]); + } else { + // Make API call or initialization logic here... + this.httpService.getUserDetailsForCentral().subscribe((response) => { + if (response?.['success']) { + this.httpService.setCurrentUserDetails(response?.['data']); + this.router.resetConfig([...this.routesAuth]); + localStorage.setItem("user_name", response?.['data']?.user_name); + localStorage.setItem("user_email", response?.['data']?.user_email); + this.ga.setLoginMethod(response?.['data'], response?.['data']?.authType); + } + + if (location) { + let redirect_uri = JSON.parse(localStorage.getItem('redirect_uri')); + if (redirect_uri) { + localStorage.removeItem('redirect_uri'); } - resolve(); - }) - } - - validateToken(location) { - return new Promise((resolve, reject) => { - if (!environment['AUTHENTICATION_SERVICE']) { - this.router.resetConfig([...this.routes]); - this.router.navigate([location]); + this.router.navigateByUrl(location); + } else { + if (localStorage.getItem('shared_link')) { + this.helperService.urlShorteningRedirection(); } else { - // Make API call or initialization logic here... - this.httpService.getUserDetailsForCentral().subscribe((response) => { - if (response?.['success']) { - this.httpService.setCurrentUserDetails(response?.['data']); - this.router.resetConfig([...this.routesAuth]); - localStorage.setItem("user_name", response?.['data']?.user_name); - localStorage.setItem("user_email", response?.['data']?.user_email); - this.ga.setLoginMethod(response?.['data'], response?.['data']?.authType); - } - - if (location) { - let redirect_uri = JSON.parse(localStorage.getItem('redirect_uri')); - if (redirect_uri) { - localStorage.removeItem('redirect_uri'); - } - this.router.navigateByUrl(location); - } else { - if (localStorage.getItem('shared_link')) { - const shared_link = localStorage.getItem('shared_link') - if (shared_link) { - localStorage.removeItem('shared_link'); - } - this.router.navigateByUrl(shared_link); - } else { - this.router.navigate(['/dashboard/iteration']); - } - - } - }, error => { - console.log(error); - }); + this.router.navigate(['/dashboard/iteration']); + } + } + }, error => { + console.log(error); + }); - } - resolve(); + } + resolve(); - }) + }) - } + } } diff --git a/UI/src/app/services/decodeURL.guard.ts b/UI/src/app/services/decodeURL.guard.ts index 0d24b10a9e..7215a1d8de 100644 --- a/UI/src/app/services/decodeURL.guard.ts +++ b/UI/src/app/services/decodeURL.guard.ts @@ -15,7 +15,7 @@ export class DecodeUrlGuard implements CanActivate { const decodedUrl = decodeURIComponent(state.url); // Compare decoded URL with the current state URL - if (state.url.length > 50 && state.url.indexOf('?stateFilters=') == -1) { + if (state.url.indexOf('stateFilters') != -1 && state.url.indexOf('?stateFilters=') == -1) { // If the decoded URL differs, navigate to the decoded URL this.router.navigateByUrl(decodedUrl, { replaceUrl: true }); return false; // Prevent further navigation until the URL is corrected diff --git a/UI/src/app/services/helper.service.ts b/UI/src/app/services/helper.service.ts index 63471e364f..a5a7a83043 100644 --- a/UI/src/app/services/helper.service.ts +++ b/UI/src/app/services/helper.service.ts @@ -27,6 +27,8 @@ import { SharedService } from './shared.service'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { environment } from 'src/environments/environment'; import { ActivatedRoute, Router } from '@angular/router'; +import { catchError } from 'rxjs/operators'; +import { throwError } from 'rxjs'; @Injectable() export class HelperService { isKanban = false; @@ -57,7 +59,7 @@ export class HelperService { downloadJson.selectedMap[filterData[0].label].push(filterData[0].filterData[i].nodeId); } } - if(isKanban === true){ + if (isKanban === true) { downloadJson['selectedMap']['sprint'] = []; } return this.httpService.downloadExcel(downloadJson, kpiId); @@ -486,7 +488,7 @@ export class HelperService { value: item.value.map(x => ({ ...x, value: (typeof x.value === 'object') ? {} : [], - allHoverValue : [], + allHoverValue: [], lineValue: x?.hasOwnProperty('lineValue') ? (typeof x.lineValue === 'object') ? {} : [] : null })) })); @@ -601,12 +603,12 @@ export class HelperService { aggregateHoverValues(objects: any[]): any { return objects.reduce((acc, obj) => { - Object.keys(obj).forEach((key) => { - acc[key] = (acc[key] || 0) + obj[key]; - }); - return acc; + Object.keys(obj).forEach((key) => { + acc[key] = (acc[key] || 0) + obj[key]; + }); + return acc; }, {}); -} + } getKpiCommentsHttp(data) { @@ -741,7 +743,7 @@ export class HelperService { // this.sharedService.setAddtionalFilterBackup(savedDetails); // } - // old UI Method, removing + // old UI Method, removing // setFilterValueIfAlreadyHaveBackup(kpiId, kpiSelectedFilterObj, tab, refreshValue, initialValue, subFilter, filters?) { // let haveBackup = {} @@ -985,4 +987,85 @@ export class HelperService { return aggregatedResponse; } + + + // url shortening redirection logic + urlShorteningRedirection() { + const shared_link = localStorage.getItem('shared_link'); + const currentUserProjectAccess = JSON.parse(localStorage.getItem('currentUserDetails'))?.projectsAccess?.length ? JSON.parse(localStorage.getItem('currentUserDetails'))?.projectsAccess[0]?.projects : []; + if (shared_link) { + // Extract query parameters + const queryParams = new URLSearchParams(shared_link.split('?')[1]); + const stateFilters = queryParams.get('stateFilters'); + const kpiFilters = queryParams.get('kpiFilters'); + + if (stateFilters) { + let decodedStateFilters: string = ''; + // let stateFiltersObj: Object = {}; + + if (stateFilters?.length <= 8) { + this.httpService.handleRestoreUrl(stateFilters, kpiFilters) + .pipe( + catchError((error) => { + this.router.navigate(['/dashboard/Error']); // Redirect to the error page + setTimeout(() => { + this.sharedService.raiseError({ + status: 900, + message: error.message || 'Invalid URL.' + }); + }); + return throwError(error); // Re-throw the error so it can be caught by a global error handler if needed + }) + ) + .subscribe((response: any) => { + if (response.success) { + const longStateFiltersString = response.data['longStateFiltersString']; + decodedStateFilters = atob(longStateFiltersString); + this.urlRedirection(decodedStateFilters, currentUserProjectAccess, shared_link); + } + }); + } else { + decodedStateFilters = atob(stateFilters); + this.urlRedirection(decodedStateFilters, currentUserProjectAccess, shared_link); + } + } + } else { + this.router.navigate(['./dashboard/']); + } + } + + urlRedirection(decodedStateFilters, currentUserProjectAccess, url) { + url = decodeURIComponent(url); + const stateFiltersObjLocal = JSON.parse(decodedStateFilters); + + let stateFilterObj = []; + + if (typeof stateFiltersObjLocal['parent_level'] === 'object' && Object.keys(stateFiltersObjLocal['parent_level']).length > 0) { + stateFilterObj = [stateFiltersObjLocal['parent_level']]; + } else { + stateFilterObj = stateFiltersObjLocal['primary_level']; + } + + // Check if user has access to all project in stateFiltersObjLocal['primary_level'] + const hasAllProjectAccess = stateFilterObj.every(filter => + currentUserProjectAccess?.some(project => project.projectId === filter.basicProjectConfigId) + ); + + // Superadmin have all project access hence no need to check project for superadmin + const getAuthorities = this.sharedService.getCurrentUserDetails('authorities'); + const hasAccessToAll = Array.isArray(getAuthorities) && getAuthorities?.includes('ROLE_SUPERADMIN') || hasAllProjectAccess; + + localStorage.removeItem('shared_link'); + if (hasAccessToAll) { + this.router.navigate([url]); + } else { + this.router.navigate(['/dashboard/Error']); + setTimeout(() => { + this.sharedService.raiseError({ + status: 901, + message: 'No project access.', + }); + }, 100); + } + } } diff --git a/UI/src/app/services/http.service.ts b/UI/src/app/services/http.service.ts index cdd61a04e7..ca505bb272 100644 --- a/UI/src/app/services/http.service.ts +++ b/UI/src/app/services/http.service.ts @@ -176,6 +176,8 @@ export class HttpService { private getShowHideKpiUrl = this.baseUrl + '/api/user-board-config'; private getShowHideKpiNewUIUrl = this.baseUrl + '/api/user-board-config/getBoardConfig'; private recommendationsUrl = this.baseUrl + '/api/kpiRecommendation'; + private urlShortener = this.baseUrl + '/api/stringShortener/shorten'; + private urlRestore = this.baseUrl + '/api/stringShortener/longString'; currentUserDetails = null; private saveMetaDataStepURL = this.baseUrl + '/api/processor/metadata/step/'; @@ -1188,4 +1190,12 @@ export class HttpService { fetchJiramappingBE(basicConfigID){ return this.http.post(this.saveMetaDataStepURL + basicConfigID, {}); } + + handleUrlShortener(payload: any): Observable { + return this.http.post(this.urlShortener, payload); + } + + handleRestoreUrl(stateFilterData, kpiFilterData) { + return this.http.get(`${this.urlRestore}?stateFilters=${stateFilterData}&kpiFilters=${kpiFilterData}`); + } } diff --git a/UI/src/app/services/shared.service.ts b/UI/src/app/services/shared.service.ts index 5e93817629..38b2b4746f 100644 --- a/UI/src/app/services/shared.service.ts +++ b/UI/src/app/services/shared.service.ts @@ -372,6 +372,11 @@ export class SharedService { private tempStateFilters = null; setBackupOfFilterSelectionState(selectedFilterObj) { + const routerUrl = decodeURIComponent(this.router.url).split('?')[0]; + const segments = typeof routerUrl === 'string' && routerUrl?.split('/'); + const hasConfig = segments && segments.includes('Config'); + const hasHelp = segments && segments.includes('Help'); + const hasError = segments && segments.includes('Error'); if (selectedFilterObj && Object.keys(selectedFilterObj).length === 1 && Object.keys(selectedFilterObj)[0] === 'selected_type') { this.selectedFilters = { ...selectedFilterObj }; } else if (selectedFilterObj) { @@ -389,7 +394,7 @@ export class SharedService { this.setBackupOfUrlFilters(JSON.stringify(this.selectedFilters || {})); // NOTE: Do not navigate if the state filters are same as previous, this is to reduce the number of navigation calls, hence refactoring the code - if (this.tempStateFilters !== stateFilterEnc) { + if ((this.tempStateFilters !== stateFilterEnc) && (!hasConfig && !hasError && !hasHelp)) { this.router.navigate([], { queryParams: { 'stateFilters': stateFilterEnc }, relativeTo: this.route @@ -426,6 +431,11 @@ export class SharedService { } setKpiSubFilterObj(value: any) { + const routerUrl = decodeURIComponent(this.router.url).split('?')[0]; + const segments = routerUrl?.split('/'); + const hasConfig = segments.includes('Config'); + const hasHelp = segments.includes('Help'); + const hasError = segments.includes('Error'); if (!value) { this.selectedKPIFilterObj = {}; } else if (Object.keys(value)?.length && Object.keys(value)[0].indexOf('kpi') !== -1) { @@ -435,12 +445,12 @@ export class SharedService { } const kpiFilterParamStr = btoa(Object.keys(this.selectedKPIFilterObj).length ? JSON.stringify(this.selectedKPIFilterObj) : ''); - this.router.navigate([], { - queryParams: { 'stateFilters': this.tempStateFilters, 'kpiFilters': kpiFilterParamStr }, // Pass the object here - relativeTo: this.route, - queryParamsHandling: 'merge' - }); - + if (!hasConfig && !hasError && !hasHelp) { + this.router.navigate([], { + queryParams: { 'stateFilters': this.tempStateFilters, 'kpiFilters': kpiFilterParamStr }, // Pass the object here + relativeTo: this.route, + }); + } this.selectedFilterOption.next(value); } diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/config/WebSecurityConfig.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/config/WebSecurityConfig.java index f9a5c8e14d..97036e0ee9 100644 --- a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/config/WebSecurityConfig.java +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/config/WebSecurityConfig.java @@ -58,13 +58,13 @@ import lombok.AllArgsConstructor; /** - * Extension of {WebSecurityConfigurerAdapter} to provide configuration - * for web security. + * Extension of {WebSecurityConfigurerAdapter} to provide configuration for web + * security. * * @author anisingh4 * - * @author pawkandp - * Removed the depricate WebSecurityConfigurerAdapter with new spring version 6+ + * @author pawkandp Removed the depricate WebSecurityConfigurerAdapter with new + * spring version 6+ */ @Configuration @EnableWebSecurity @@ -73,129 +73,128 @@ @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig implements WebMvcConfigurer { - private JwtAuthenticationFilter jwtAuthenticationFilter; + private JwtAuthenticationFilter jwtAuthenticationFilter; - private AuthenticationResultHandler authenticationResultHandler; + private AuthenticationResultHandler authenticationResultHandler; - private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; + private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; - private AuthenticationProvider standardAuthenticationProvider; + private AuthenticationProvider standardAuthenticationProvider; - private AuthProperties authProperties; + private AuthProperties authProperties; - private CustomApiConfig customApiConfig; + private CustomApiConfig customApiConfig; - private StandardAuthenticationManager authenticationManager; + private StandardAuthenticationManager authenticationManager; - public static Properties getProps() throws IOException { - Properties prop = new Properties(); - try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("crowd.properties")) { - prop.load(in); - } - return prop; - } + public static Properties getProps() throws IOException { + Properties prop = new Properties(); + try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("crowd.properties")) { + prop.load(in); + } + return prop; + } - /** - * Added below fixes for security scan: - commented the headers in the response - * - added CorsFilter in filter chain for endpoints mentioned in the method - * - * @param http - reference to HttpSecurity - */ - @Bean - protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // Configure AuthenticationManagerBuilder - AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); - setAuthenticationProvider(authenticationManagerBuilder); - http.headers(headers -> headers.cacheControl(HeadersConfigurer.CacheControlConfig::disable) + /** + * Added below fixes for security scan: - commented the headers in the response + * - added CorsFilter in filter chain for endpoints mentioned in the method + * + * @param http + * - reference to HttpSecurity + */ + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // Configure AuthenticationManagerBuilder + AuthenticationManagerBuilder authenticationManagerBuilder = http + .getSharedObject(AuthenticationManagerBuilder.class); + setAuthenticationProvider(authenticationManagerBuilder); + http.headers(headers -> headers.cacheControl(HeadersConfigurer.CacheControlConfig::disable) .httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(customApiConfig.isIncludeSubDomains()) .maxAgeInSeconds(customApiConfig.getMaxAgeInSeconds()))); - http.csrf(AbstractHttpConfigurer::disable); - http.authorizeHttpRequests(authz -> authz - .requestMatchers("/appinfo").permitAll().requestMatchers("/registerUser") - .permitAll().requestMatchers("/changePassword").permitAll().requestMatchers("/login/captcha").permitAll() - .requestMatchers("/login/captchavalidate").permitAll().requestMatchers("/login**").permitAll() - .requestMatchers("/error").permitAll().requestMatchers("/authenticationProviders").permitAll() - .requestMatchers("/auth-types-status").permitAll().requestMatchers("/pushData/*").permitAll() - .requestMatchers("/getversionmetadata").permitAll() - .requestMatchers("/kpiIntegrationValues").permitAll() - .requestMatchers("/processor/saveRepoToolsStatus").permitAll() - .requestMatchers("/v1/kpi/{kpiID}").permitAll() - .requestMatchers("/basicconfigs/hierarchyResponses").permitAll() - - // management metrics - .requestMatchers("/info").permitAll().requestMatchers("/health").permitAll().requestMatchers("/env").permitAll() - .requestMatchers("/metrics").permitAll() - .requestMatchers("/actuator/togglz**").permitAll() - .requestMatchers("/togglz-console**").permitAll() - .requestMatchers("hierarchy/migrate/**").permitAll() - .requestMatchers("/actuator**").permitAll() - .requestMatchers("/forgotPassword").permitAll() - .requestMatchers("/validateEmailToken**").permitAll() - .requestMatchers("/resetPassword").permitAll() - .requestMatchers("/cache/clearAllCache").permitAll().requestMatchers(HttpMethod.GET, "/cache/clearCache/**") - .permitAll().requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .requestMatchers(HttpMethod.GET, "/analytics/switch").permitAll().anyRequest().authenticated()) - .addFilterBefore(standardLoginRequestFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterAfter(corsFilter(), ChannelProcessingFilter.class) - .httpBasic(basic -> basic.authenticationEntryPoint(customAuthenticationEntryPoint())) - .exceptionHandling(Customizer.withDefaults()); - return http.build(); - } - - @Bean - protected CorsFilter corsFilter() { - return new CorsFilter(); - } - - protected void setAuthenticationProvider(AuthenticationManagerBuilder auth) { - List authenticationProviders = authProperties.getAuthenticationProviders(); - - if (authenticationProviders.contains(AuthType.STANDARD)) { - auth.authenticationProvider(standardAuthenticationProvider); - } - } - - @Bean - protected StandardLoginRequestFilter standardLoginRequestFilter(AuthenticationManager authenticationManager){ - return new StandardLoginRequestFilter("/login", authenticationManager, authenticationResultHandler, - customAuthenticationFailureHandler, customApiConfig); - } - - @Bean - CorsConfigurationSource apiConfigurationSource() { - final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - final CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); - config.setAllowedOrigins(customApiConfig.getCorsFilterValidOrigin()); - config.addAllowedHeader("*"); - config.addAllowedMethod("OPTIONS"); - config.addAllowedMethod("HEAD"); - config.addAllowedMethod("GET"); - config.addAllowedMethod("PUT"); - config.addAllowedMethod("POST"); - config.addAllowedMethod("DELETE"); - config.addAllowedMethod("PATCH"); - source.registerCorsConfiguration("/**", config); - return source; - } - - @Bean - public CustomAuthenticationEntryPoint customAuthenticationEntryPoint() { - return new CustomAuthenticationEntryPoint(); - } - - @Bean - public WebSecurityCustomizer webSecurityCustomizer() { - return web -> web.ignoring().requestMatchers("/v3/api-docs.yaml", "/v3/api-docs/**", "/swagger-ui/**"); - } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/swagger.yaml") - .addResourceLocations("classpath:/static/swagger.yaml"); - registry.addResourceHandler("/swagger-ui/**") - .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); - } + http.csrf(AbstractHttpConfigurer::disable); + http.authorizeHttpRequests(authz -> authz.requestMatchers("/appinfo").permitAll() + .requestMatchers("/registerUser").permitAll().requestMatchers("/changePassword").permitAll() + .requestMatchers("/login/captcha").permitAll().requestMatchers("/login/captchavalidate").permitAll() + .requestMatchers("/login**").permitAll().requestMatchers("/error").permitAll() + .requestMatchers("/authenticationProviders").permitAll().requestMatchers("/auth-types-status") + .permitAll().requestMatchers("/pushData/*").permitAll().requestMatchers("/getversionmetadata") + .permitAll().requestMatchers("/kpiIntegrationValues").permitAll() + .requestMatchers("/processor/saveRepoToolsStatus").permitAll().requestMatchers("/v1/kpi/{kpiID}") + .permitAll().requestMatchers("/basicconfigs/hierarchyResponses").permitAll() + + // management metrics + .requestMatchers("/info").permitAll().requestMatchers("/health").permitAll().requestMatchers("/env") + .permitAll().requestMatchers("/metrics").permitAll().requestMatchers("/actuator/togglz**").permitAll() + .requestMatchers("/togglz-console**").permitAll().requestMatchers("hierarchy/migrate/**").permitAll() + .requestMatchers("/actuator**").permitAll().requestMatchers("/forgotPassword").permitAll() + .requestMatchers("/validateEmailToken**").permitAll().requestMatchers("/resetPassword").permitAll() + .requestMatchers("/cache/clearAllCache").permitAll() + .requestMatchers(HttpMethod.GET, "/cache/clearCache/**").permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers(HttpMethod.GET, "/analytics/switch").permitAll() + .requestMatchers("/stringShortener/shorten").permitAll() + .requestMatchers("/stringShortener/longString").permitAll().anyRequest().authenticated()) + .addFilterBefore(standardLoginRequestFilter(authenticationManager), + UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterAfter(corsFilter(), ChannelProcessingFilter.class) + .httpBasic(basic -> basic.authenticationEntryPoint(customAuthenticationEntryPoint())) + .exceptionHandling(Customizer.withDefaults()); + return http.build(); + } + + @Bean + protected CorsFilter corsFilter() { + return new CorsFilter(); + } + + protected void setAuthenticationProvider(AuthenticationManagerBuilder auth) { + List authenticationProviders = authProperties.getAuthenticationProviders(); + + if (authenticationProviders.contains(AuthType.STANDARD)) { + auth.authenticationProvider(standardAuthenticationProvider); + } + } + + @Bean + protected StandardLoginRequestFilter standardLoginRequestFilter(AuthenticationManager authenticationManager) { + return new StandardLoginRequestFilter("/login", authenticationManager, authenticationResultHandler, + customAuthenticationFailureHandler, customApiConfig); + } + + @Bean + CorsConfigurationSource apiConfigurationSource() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.setAllowedOrigins(customApiConfig.getCorsFilterValidOrigin()); + config.addAllowedHeader("*"); + config.addAllowedMethod("OPTIONS"); + config.addAllowedMethod("HEAD"); + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return source; + } + + @Bean + public CustomAuthenticationEntryPoint customAuthenticationEntryPoint() { + return new CustomAuthenticationEntryPoint(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring().requestMatchers("/v3/api-docs.yaml", "/v3/api-docs/**", "/swagger-ui/**"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/swagger.yaml").addResourceLocations("classpath:/static/swagger.yaml"); + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); + } } \ No newline at end of file diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/controller/StringShortenerController.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/controller/StringShortenerController.java new file mode 100644 index 0000000000..d27f11ef0e --- /dev/null +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/controller/StringShortenerController.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.controller; + +import com.publicissapient.kpidashboard.apis.model.ServiceResponse; +import com.publicissapient.kpidashboard.apis.stringshortener.dto.StringShortenerDTO; +import com.publicissapient.kpidashboard.apis.stringshortener.model.StringShortener; +import com.publicissapient.kpidashboard.apis.stringshortener.service.StringShortenerService; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Optional; + +@RestController +@RequestMapping("/stringShortener") +public class StringShortenerController { + + private final StringShortenerService stringShortenerService; + private static final String SHORT_STRING_RESPONSE_MESSAGE = "Successfully Created Short String"; + private static final String FAILURE_RESPONSE_MESSAGE = "Invalid URL."; + private static final String FETCH_SUCCESS_MESSAGE = "Successfully fetched"; + + + @Autowired + private StringShortenerController(StringShortenerService stringShortenerService) { + this.stringShortenerService=stringShortenerService; + } + + @PostMapping("/shorten") + public ResponseEntity createShortString(@RequestBody StringShortenerDTO stringShortenerDTO) { + ServiceResponse response = null; + StringShortener stringShortener = stringShortenerService.createShortString(stringShortenerDTO); + final ModelMapper modelMapper = new ModelMapper(); + final StringShortenerDTO responseDTO = modelMapper.map(stringShortener, StringShortenerDTO.class); + if (responseDTO != null && !responseDTO.toString().isEmpty()) { + response = new ServiceResponse(true, SHORT_STRING_RESPONSE_MESSAGE, responseDTO); + return ResponseEntity.status(HttpStatus.OK).body(response); + } else { + response = new ServiceResponse(false, FAILURE_RESPONSE_MESSAGE, null); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + } + + @GetMapping("/longString") + public ResponseEntity getLongString(@RequestParam String kpiFilters, @RequestParam String stateFilters) { + ServiceResponse response = null; + Optional stringShortener = stringShortenerService.getLongString(kpiFilters, stateFilters); + if (stringShortener.isPresent()) { + final ModelMapper modelMapper = new ModelMapper(); + final StringShortenerDTO responseDTO = modelMapper.map(stringShortener.get(), StringShortenerDTO.class); + response = new ServiceResponse(true, FETCH_SUCCESS_MESSAGE, responseDTO); + return ResponseEntity.status(HttpStatus.OK).body(response); + } else { + response = new ServiceResponse(false, FAILURE_RESPONSE_MESSAGE, null); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + } +} \ No newline at end of file diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/dto/StringShortenerDTO.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/dto/StringShortenerDTO.java new file mode 100644 index 0000000000..0f60820ec4 --- /dev/null +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/dto/StringShortenerDTO.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.dto; + +import lombok.Data; + +@Data +public class StringShortenerDTO { + private String longStateFiltersString; + private String shortStateFiltersString; + private String longKPIFiltersString; + private String shortKPIFilterString; + +} \ No newline at end of file diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/model/StringShortener.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/model/StringShortener.java new file mode 100644 index 0000000000..0b609b5790 --- /dev/null +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/model/StringShortener.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.model; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + + @Data + @Document(collection = "string_shorteners") + public class StringShortener { + @Id + private String id; + private String longStateFiltersString; + private String shortStateFiltersString; + private String longKPIFiltersString; + private String shortKPIFilterString; + } \ No newline at end of file diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/repository/StringShortenerRepository.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/repository/StringShortenerRepository.java new file mode 100644 index 0000000000..42bff73eac --- /dev/null +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/repository/StringShortenerRepository.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.repository; + +import com.publicissapient.kpidashboard.apis.stringshortener.model.StringShortener; +import org.springframework.data.mongodb.repository.MongoRepository; +import java.util.Optional; + +public interface StringShortenerRepository extends MongoRepository { + Optional findByShortKPIFilterStringAndShortStateFiltersString(String shortKPIFilterString, String shortStateFiltersString); + Optional findByLongKPIFiltersStringAndLongStateFiltersString(String longKPIFiltersString, String longStateFiltersString); + +} \ No newline at end of file diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/service/StringShortenerService.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/service/StringShortenerService.java new file mode 100644 index 0000000000..780822e5c2 --- /dev/null +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/service/StringShortenerService.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.service; + +import static com.publicissapient.kpidashboard.apis.stringshortener.util.UniqueShortKeyGenerator.generateShortKey; + +import java.util.Optional; + +import com.publicissapient.kpidashboard.apis.stringshortener.dto.StringShortenerDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.publicissapient.kpidashboard.apis.stringshortener.model.StringShortener; +import com.publicissapient.kpidashboard.apis.stringshortener.repository.StringShortenerRepository; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class StringShortenerService { + + private final StringShortenerRepository stringShortenerRepository; + + @Autowired + public StringShortenerService(StringShortenerRepository stringMappingRepository) { + this.stringShortenerRepository = stringMappingRepository; + } + + public StringShortener createShortString(StringShortenerDTO stringShortenerDTO){ + if (stringShortenerDTO == null) { + log.warn("Provided stringShortenerDTO is null"); + throw new IllegalArgumentException("Please provide a valid stringShortenerDTO"); + } + Optional stringShortenerOptional = stringShortenerRepository.findByLongKPIFiltersStringAndLongStateFiltersString(stringShortenerDTO.getLongKPIFiltersString(),stringShortenerDTO.getLongStateFiltersString()); + if (stringShortenerOptional.isPresent()) { + log.info("Existing mapping found for long strings: {},{}", stringShortenerDTO.getLongKPIFiltersString().replaceAll("[^a-zA-Z0-9-_]", ""),stringShortenerDTO.getLongStateFiltersString().replaceAll("[^a-zA-Z0-9-_]", "")); + return stringShortenerOptional.get(); + } + String shortKPIFiltersString = generateShortKey(stringShortenerDTO.getLongKPIFiltersString()); + String shortStateFiltersString = generateShortKey(stringShortenerDTO.getLongStateFiltersString()); + StringShortener stringMapping = new StringShortener(); + stringMapping.setLongKPIFiltersString(stringShortenerDTO.getLongKPIFiltersString()); + stringMapping.setShortKPIFilterString(shortKPIFiltersString); + stringMapping.setLongStateFiltersString(stringShortenerDTO.getLongStateFiltersString()); + stringMapping.setShortStateFiltersString(shortStateFiltersString); + + return stringShortenerRepository.save(stringMapping); + } + + public Optional getLongString(String kpiFilters, String stateFilters) { + return stringShortenerRepository.findByShortKPIFilterStringAndShortStateFiltersString(kpiFilters, stateFilters); + } +} \ No newline at end of file diff --git a/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/util/UniqueShortKeyGenerator.java b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/util/UniqueShortKeyGenerator.java new file mode 100644 index 0000000000..04d728b19e --- /dev/null +++ b/customapi/src/main/java/com/publicissapient/kpidashboard/apis/stringshortener/util/UniqueShortKeyGenerator.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.util; + +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +@Slf4j +public class UniqueShortKeyGenerator { + + private UniqueShortKeyGenerator() { + throw new IllegalStateException("Utility class"); + } + + public static String generateShortKey(String input) { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null"); + } + log.info("Generating short key for input: {}", input.replaceAll("[^a-zA-Z0-9-_]", "")); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + String shortKey = java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + // Return first 8 characters for a short key + return shortKey.substring(0, 8); + } catch (NoSuchAlgorithmException e) { + log.error("Error generating short key for input: {}", input.replaceAll("[^a-zA-Z0-9-_]", ""), e); + throw new IllegalStateException("Error generating short key", e); + } + } + +} \ No newline at end of file diff --git a/customapi/src/test/java/com/publicissapient/kpidashboard/apis/stringshortener/service/StringShortenerServiceTest.java b/customapi/src/test/java/com/publicissapient/kpidashboard/apis/stringshortener/service/StringShortenerServiceTest.java new file mode 100644 index 0000000000..d1fae514df --- /dev/null +++ b/customapi/src/test/java/com/publicissapient/kpidashboard/apis/stringshortener/service/StringShortenerServiceTest.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package com.publicissapient.kpidashboard.apis.stringshortener.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.publicissapient.kpidashboard.apis.stringshortener.dto.StringShortenerDTO; +import com.publicissapient.kpidashboard.apis.stringshortener.model.StringShortener; +import com.publicissapient.kpidashboard.apis.stringshortener.repository.StringShortenerRepository; + +@RunWith(MockitoJUnitRunner.class) +public class StringShortenerServiceTest { + + @Mock + private StringShortenerRepository stringShortenerRepository; + + @InjectMocks + private StringShortenerService stringShortenerService; + + private StringShortenerDTO stringShortenerDTO; + private StringShortener stringShortener; + + @Before + public void setUp() { + stringShortenerDTO = new StringShortenerDTO(); + stringShortenerDTO.setLongKPIFiltersString("longKPI"); + stringShortenerDTO.setLongStateFiltersString("longState"); + + stringShortener = new StringShortener(); + stringShortener.setLongKPIFiltersString("longKPI"); + stringShortener.setShortKPIFilterString("shortKPI"); + stringShortener.setLongStateFiltersString("longState"); + stringShortener.setShortStateFiltersString("shortState"); + } + + @Test + public void testCreateShortString_NullInput() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + stringShortenerService.createShortString(null); + }); + assertEquals("Please provide a valid stringShortenerDTO", exception.getMessage()); + } + + @Test + public void testCreateShortString_ExistingMapping() { + when(stringShortenerRepository.findByLongKPIFiltersStringAndLongStateFiltersString("longKPI", "longState")) + .thenReturn(Optional.of(stringShortener)); + + StringShortener result = stringShortenerService.createShortString(stringShortenerDTO); + assertEquals(stringShortener, result); + } + + @Test + public void testGetLongString_Found() { + when(stringShortenerRepository.findByShortKPIFilterStringAndShortStateFiltersString("shortKPI", "shortState")) + .thenReturn(Optional.of(stringShortener)); + + Optional result = stringShortenerService.getLongString("shortKPI", "shortState"); + assertEquals(stringShortener, result.get()); + } + + @Test + public void testGetLongString_NotFound() { + when(stringShortenerRepository.findByShortKPIFilterStringAndShortStateFiltersString("shortKPI", "shortState")) + .thenReturn(Optional.empty()); + + Optional result = stringShortenerService.getLongString("shortKPI", "shortState"); + assertEquals(Optional.empty(), result); + } + + @Test + public void testCreateShortString_NewMapping() { + // Arrange + when(stringShortenerRepository.findByLongKPIFiltersStringAndLongStateFiltersString("longKPI", "longState")) + .thenReturn(Optional.empty()); + + when(stringShortenerRepository.save(any(StringShortener.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // Act + StringShortener result = stringShortenerService.createShortString(stringShortenerDTO); + + // Assert + assertEquals("longKPI", result.getLongKPIFiltersString()); + assertEquals("longState", result.getLongStateFiltersString()); + assertEquals(8, result.getShortKPIFilterString().length()); + assertEquals(8, result.getShortStateFiltersString().length()); + } +} \ No newline at end of file diff --git a/customapi/src/test/java/com/publicissapient/kpidashboard/apis/stringshortener/utils/UniqueShortKeyGeneratorTest.java b/customapi/src/test/java/com/publicissapient/kpidashboard/apis/stringshortener/utils/UniqueShortKeyGeneratorTest.java new file mode 100644 index 0000000000..12faeb843f --- /dev/null +++ b/customapi/src/test/java/com/publicissapient/kpidashboard/apis/stringshortener/utils/UniqueShortKeyGeneratorTest.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright 2014 CapitalOne, LLC. + * Further development Copyright 2022 Sapient Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ +package com.publicissapient.kpidashboard.apis.stringshortener.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +import com.publicissapient.kpidashboard.apis.stringshortener.util.UniqueShortKeyGenerator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class UniqueShortKeyGeneratorTest { + + @Test + public void testGenerateShortKey() { + String input = "exampleString"; + String expectedShortKey = "Wat88jds"; + String actualShortKey = UniqueShortKeyGenerator.generateShortKey(input); + assertEquals(expectedShortKey, actualShortKey); + } + + @Test + public void testGenerateShortKeyWithEmptyInput() { + String input = ""; + String expectedShortKey = "47DEQpj8"; + String actualShortKey = UniqueShortKeyGenerator.generateShortKey(input); + assertEquals(expectedShortKey, actualShortKey); + } + + @Test + public void testGenerateShortKeyWithNullInput() { + assertThrows(IllegalArgumentException.class, () -> { + UniqueShortKeyGenerator.generateShortKey(null); + }); + } +} \ No newline at end of file