From 7ccefd81b12ea95f13562a170f83c03845822e0e Mon Sep 17 00:00:00 2001 From: Lucas Andrade <63932475+nYCSTs@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:24:33 -0300 Subject: [PATCH] 59 admin crud (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Inicio do crud de admin Co-authored-by: cansancaojennifer Co-authored-by: nYCSTs * melhorar o visual da tabela Co-authored-by: nYCSTs Co-authored-by: cansancaojennifer * implementa restrição de rotas Co-authored-by: nYCSTs Co-authored-by: cansancaojennifer * ajuste na tabela de usuários Co-authored-by: nYCSTs Co-authored-by: cansancaojennifer * Melhorias gerais na tabela de usuários * Filtragem por nome e email. Paginacao na listagem de usuarios * Altera como e feita a atualizacao de role * Filtragem por vinculo * Ajuste bug reportado pelo sonar * Implementa Guard para rotas de admin davimarinho * Atualiza testes * Atualiza url do serviço de usuarios * Atualiza url de testes de usuario * Atualiza testes * Atualiza valores de vinculo no filtro. Melhorias gerais * Aumenta valor de erro do budget * Altera configuração do ts --------- Co-authored-by: RaisSabeAndrade Co-authored-by: cansancaojennifer Co-authored-by: João Victor --- angular.json | 2 +- package.json | 2 + src/app/app-routing.module.ts | 3 + src/app/app.module.ts | 9 +- src/app/guard/auth.guard.ts | 2 +- .../user-token-interceptor.service.spec.ts | 2 +- src/app/pages/profile/profile.component.ts | 1 + .../update-role/update-role.component.css | 39 +++++ .../update-role/update-role.component.html | 79 +++++++++++ .../update-role/update-role.component.spec.ts | 134 ++++++++++++++++++ .../update-role/update-role.component.ts | 79 +++++++++++ src/app/services/admin.guard.spec.ts | 40 ++++++ src/app/services/admin.guard.ts | 24 ++++ src/app/services/user.service.spec.ts | 21 ++- src/app/services/user.service.ts | 39 ++++- src/index.html | 3 + 16 files changed, 469 insertions(+), 10 deletions(-) create mode 100644 src/app/pages/update-role/update-role.component.css create mode 100644 src/app/pages/update-role/update-role.component.html create mode 100644 src/app/pages/update-role/update-role.component.spec.ts create mode 100644 src/app/pages/update-role/update-role.component.ts create mode 100644 src/app/services/admin.guard.spec.ts create mode 100644 src/app/services/admin.guard.ts diff --git a/angular.json b/angular.json index 3f2fbd7d..28e6f364 100644 --- a/angular.json +++ b/angular.json @@ -37,7 +37,7 @@ { "type": "initial", "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumError": "2mb" }, { "type": "anyComponentStyle", diff --git a/package.json b/package.json index 6db2f4d3..26f7eb3f 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,12 @@ "private": true, "dependencies": { "@angular/animations": "^15.2.0", + "@angular/cdk": "^15.2.9", "@angular/common": "^15.2.0", "@angular/compiler": "^15.2.0", "@angular/core": "^15.2.0", "@angular/forms": "^15.2.0", + "@angular/material": "^15.2.9", "@angular/platform-browser": "^15.2.0", "@angular/platform-browser-dynamic": "^15.2.0", "@angular/router": "^15.2.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e78cb473..f30e734b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -11,6 +11,8 @@ import { CheckCodeRestPasswordComponent } from './pages/check-code-rest-password import { ResetPasswordComponent } from './pages/reset-password/reset-password.component'; import { AuthGuard } from './guard/auth.guard'; import { EditUserComponent } from './pages/edit-user/edit-user.component'; +import { UpdateRoleComponent } from './pages/update-role/update-role.component'; +import { AdminGuard } from './services/admin.guard'; import { SuggestAgendaComponent } from './pages/suggest-agenda/suggest-agenda.component'; import { ParticipateComponent } from './pages/participate/participate.component'; import { WithTokenGuard } from './guard/with-token.guard'; @@ -29,6 +31,7 @@ const routes: Routes = [ { path: 'participate', component: ParticipateComponent, canActivate: [AuthGuard], }, { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard], }, { path: 'editUser/:id', component: EditUserComponent, canActivate: [AuthGuard], }, + { path: 'update-role', component: UpdateRoleComponent, canActivate: [AdminGuard], }, ]; @NgModule({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ec322a40..cff38be0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,8 +1,8 @@ // Import import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { ReactiveFormsModule } from '@angular/forms'; import { ToastModule } from 'primeng/toast'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { ConfirmationService } from 'primeng/api'; @@ -30,6 +30,8 @@ import { CheckCodeRestPasswordComponent } from './pages/check-code-rest-password import { AuthGuard } from './guard/auth.guard'; import { AuthService } from './services/auth.service'; import { EditUserComponent } from './pages/edit-user/edit-user.component'; +import { UpdateRoleComponent } from './pages/update-role/update-role.component'; +import {MatPaginatorModule} from '@angular/material/paginator'; import { MenuModule } from 'primeng/menu'; import { VideoCommentComponent } from './components/video-comment/video-comment.component'; import { SuggestAgendaComponent } from './pages/suggest-agenda/suggest-agenda.component'; @@ -44,12 +46,14 @@ import { MessageService } from 'primeng/api'; ReactiveFormsModule, ToastModule, ConfirmDialogModule, - BrowserAnimationsModule, OAuthModule.forRoot(), InputTextModule, DropdownModule, ButtonModule, MenuModule, + FormsModule, + BrowserAnimationsModule, + MatPaginatorModule ], declarations: [ AppComponent, @@ -65,6 +69,7 @@ import { MessageService } from 'primeng/api'; ResetPasswordComponent, CheckCodeRestPasswordComponent, EditUserComponent, + UpdateRoleComponent, SuggestAgendaComponent, ParticipateComponent, VideoCommentComponent, diff --git a/src/app/guard/auth.guard.ts b/src/app/guard/auth.guard.ts index cf049b73..f98f0c30 100644 --- a/src/app/guard/auth.guard.ts +++ b/src/app/guard/auth.guard.ts @@ -17,4 +17,4 @@ export class AuthGuard implements CanActivate { return false; } } -} +} \ No newline at end of file diff --git a/src/app/interceptor/user-token-interceptor.service.spec.ts b/src/app/interceptor/user-token-interceptor.service.spec.ts index 8ca74733..8b17bcdc 100644 --- a/src/app/interceptor/user-token-interceptor.service.spec.ts +++ b/src/app/interceptor/user-token-interceptor.service.spec.ts @@ -30,7 +30,7 @@ describe('TokenInterceptorService', () => { it('should add header Authorization to request', () => { localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJqb2FvMTV2aWN0b3IwOEBnbWFpbC5jb20iLCJleHAiOjE2OTkzMTI5MzV9.1B9qBJt8rErwBKyD5JCdsPozsw86oQ38tdfDuMM2HFI'); - userService.getAllUsers().subscribe((res) => { + userService.getAllUsers({}).subscribe((res) => { expect(res).toBeTruthy(); }); const req = httpMock.expectOne(`${userService.usersAPIURL}/users`); diff --git a/src/app/pages/profile/profile.component.ts b/src/app/pages/profile/profile.component.ts index 99b07ee2..a39bf757 100644 --- a/src/app/pages/profile/profile.component.ts +++ b/src/app/pages/profile/profile.component.ts @@ -10,6 +10,7 @@ import { HttpErrorResponse } from '@angular/common/http'; type ErrorResponseType = HttpErrorResponse; + @Component({ selector: 'app-profile', templateUrl: './profile.component.html', diff --git a/src/app/pages/update-role/update-role.component.css b/src/app/pages/update-role/update-role.component.css new file mode 100644 index 00000000..e85b9d2c --- /dev/null +++ b/src/app/pages/update-role/update-role.component.css @@ -0,0 +1,39 @@ +table { + border-collapse: collapse; + width: 100%; + border: 1px solid #ccc; +} + + +th { + background-color: #f2f2f2; + border: 1px solid #ccc; + padding: 8px; + text-align: left; +} + + +tr:nth-child(even) { + background-color: #f2f2f2; +} + +tr:nth-child(odd) { + background-color: #ffffff; +} + + +td { + border: 1px solid #ccc; + padding: 8px; +} + + +tr:hover { + background-color: #e0e0e0; +} + +.filter-container { + display: flex; + align-items: flex-start; + column-gap: 6px; +} \ No newline at end of file diff --git a/src/app/pages/update-role/update-role.component.html b/src/app/pages/update-role/update-role.component.html new file mode 100644 index 00000000..0c04f8ba --- /dev/null +++ b/src/app/pages/update-role/update-role.component.html @@ -0,0 +1,79 @@ +
+
+
+ + +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + +
NomeEmailVínculoCargoAtivo
{{ user.name }}{{ user.email }}{{ user.connection }} + + Administrador + {{ user.is_active ? "Sim" : "Não" }}
+ +
+ + +

Nenhum usuário encontrado

+
+
diff --git a/src/app/pages/update-role/update-role.component.spec.ts b/src/app/pages/update-role/update-role.component.spec.ts new file mode 100644 index 00000000..e6e4a514 --- /dev/null +++ b/src/app/pages/update-role/update-role.component.spec.ts @@ -0,0 +1,134 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { UserService } from 'src/app/services/user.service'; +import { of, throwError } from 'rxjs'; +import { FormsModule } from '@angular/forms'; + +import { UpdateRoleComponent } from './update-role.component'; +import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +const mockData = [ + { + "connection": "PROFESSOR", + "id": 1, + "name": "Luisa", + "email": "email1@email.com", + "role": "USER", + "is_active": null, + }, + { + "connection": "ESTUDANTE", + "id": 2, + "name": "Renato", + "email": "email2@email.com", + "role": "ADMIN", + "is_active": true, + }, + { + "connection": "SERVIDOR", + "id": 3, + "name": "Paolla", + "email": "email3@email.com", + "role": "USER", + "is_active": true, + } +] + +class UserServiceMock { + constructor() { } + getAllUsers() { + return of(mockData); + } + updateUserRole() { + return of({}); + } +} + +describe('UpdateRoleComponent', () => { + let component: UpdateRoleComponent; + let fixture: ComponentFixture; + let userService: UserService; + let httpMock: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UpdateRoleComponent], + imports: [HttpClientTestingModule, MatPaginatorModule, FormsModule, BrowserAnimationsModule], + providers: [{ provide: UserService, useValue: new UserServiceMock() }] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UpdateRoleComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserService); + httpMock = TestBed.inject(HttpTestingController); + }); + + it('should create', () => { + let adminToken: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWNhc2NhbmRyYWRleDFAZ21haWwuY29tIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzAwMTAwMDQxfQ.aDhw1xkK55bhUQCm6tSxX4LYxq8hP_b3T8gYUS449F8" + localStorage.setItem("token", adminToken) + + component.users = mockData; + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should get all users', () => { + const mySpy = spyOn(userService, 'getAllUsers').and.callThrough(); + component.getAllUsers(); + expect(mySpy).toHaveBeenCalled(); + }); + + + it('should get all users and data headers is null', () => { + const mySpy = spyOn(userService, 'getAllUsers').and.returnValue(of({ headers: null, body: [] })); + component.getAllUsers(); + expect(mySpy).toHaveBeenCalled(); + expect(component.users).toEqual([]); + }); + + + + it('should get all users and return error', () => { + const mySpy = spyOn(userService, 'getAllUsers').and.returnValue(throwError(() => new Error('Erro'))); + component.getAllUsers(); + expect(mySpy).toHaveBeenCalled(); + }); + + it('should update pageSize and pageIndex onPaginateChange', () => { + const event: PageEvent = { + length: 50, + pageIndex: 1, + pageSize: 10, + previousPageIndex: 0 + }; + + component.onPaginateChange(event); + + expect(component.pageSize).toEqual(event.pageSize); + expect(component.pageIndex).toEqual(event.pageIndex); + }); + + it('should filter users from get users', () => { + const mySpy = spyOn(userService, 'getAllUsers').and.callThrough(); + component.filterUser(); + expect(mySpy).toHaveBeenCalled(); + }); + + it('should update user role', () => { + const mySpy = spyOn(userService, 'updateUserRole').and.callThrough(); + component.updateUserRole(1); + expect(mySpy).toHaveBeenCalled(); + }); + + it('should update user role and return error', () => { + const mySpy = spyOn(userService, 'updateUserRole').and.returnValue(throwError(() => new Error('Erro'))); + component.updateUserRole(1); + expect(mySpy).toHaveBeenCalled(); + }); + + +}); diff --git a/src/app/pages/update-role/update-role.component.ts b/src/app/pages/update-role/update-role.component.ts new file mode 100644 index 00000000..4848c0cb --- /dev/null +++ b/src/app/pages/update-role/update-role.component.ts @@ -0,0 +1,79 @@ +import { UserService } from './../../services/user.service'; +import { Component, OnInit } from '@angular/core'; +import { PageEvent } from '@angular/material/paginator'; +import jwt_decode from 'jwt-decode'; + + +@Component({ + selector: 'app-update-role', + templateUrl: './update-role.component.html', + styleUrls: ['./update-role.component.css'], +}) +export class UpdateRoleComponent implements OnInit { + users: any = []; + userId: number = 0; + + filterInputValue: string = ""; + filterConnectionValue: string = "" + + total: number = 0; + pageSize: number = 10; + pageIndex: number = 0; + + constructor(private userService: UserService) {} + + ngOnInit(): void { + this.getUserid(); + this.getAllUsers(); + }; + + updateUserRole(id: number) { + this.userService.updateUserRole(id).subscribe({ + next: (data) => { + console.log(data); + }, + error: (erro) => { + console.error('Erro', erro); + }, + }); + }; + + getUserid() { + const decodedToken: any = jwt_decode( + localStorage.getItem('token') as string + ); + this.userId = decodedToken.id; + }; + + filterUser() { + this.pageIndex = 0; + this.getAllUsers(); + }; + + onPaginateChange(event: PageEvent) { + this.pageSize = event.pageSize; + this.pageIndex = event.pageIndex; + + this.getAllUsers(); + }; + + getAllUsers() { + this.userService.getAllUsers({ + nameEmail: this.filterInputValue, + connection: this.filterConnectionValue, + limit: this.pageSize, + offset: (this.pageIndex * this.pageSize) + }).subscribe({ + next: (data: Response) => { + this.users = data.body; + if (data && data.headers && data.headers.has("x-total-count")) { + const totalCountHeader = data.headers.get("x-total-count"); + this.total = Number.parseInt(totalCountHeader || '0', 10); + } + }, + error: (erro) => { + console.error('Erro', erro); + }, + }); + } +} diff --git a/src/app/services/admin.guard.spec.ts b/src/app/services/admin.guard.spec.ts new file mode 100644 index 00000000..e7ae5656 --- /dev/null +++ b/src/app/services/admin.guard.spec.ts @@ -0,0 +1,40 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { LoginComponent } from '../pages/login/login.component'; + +import { AdminGuard } from './admin.guard'; + +describe('AdminGuard', () => { + let guard: AdminGuard; + let adminToken: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWNhc2NhbmRyYWRleDFAZ21haWwuY29tIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzAwMTAwMDQxfQ.aDhw1xkK55bhUQCm6tSxX4LYxq8hP_b3T8gYUS449F8" + let userToken: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWNhc2NhbmRyYWRleDFAZ21haWwuY29tIiwicm9sZSI6IlVTRVIiLCJleHAiOjE3NjAwMTIxNTB9.dtKlfCqAuwaIUygAZnylw1nc1IXuJAnY8R_H1pGPlv0"; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes( + [{ path: 'login', component: LoginComponent }] + )], + providers: [AdminGuard], + declarations: [LoginComponent] + }); + guard = TestBed.inject(AdminGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + it('should return false for a user with role ADMIN', () => { + localStorage.setItem('token', adminToken); + expect(guard.canActivate()).toBe(true); + } + ); + + it('should return false for a user with role USER', () => { + localStorage.setItem('token', userToken); + expect(guard.canActivate()).toBe(false); + } + ); + +}); diff --git a/src/app/services/admin.guard.ts b/src/app/services/admin.guard.ts new file mode 100644 index 00000000..8dd944ef --- /dev/null +++ b/src/app/services/admin.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { UserService } from './user.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AdminGuard implements CanActivate { + constructor( + private router: Router, + private userService: UserService + ) {} + + canActivate(): boolean { + const roles = this.userService.getRoles(); + if (roles !== "ADMIN") { + this.router.navigate(["/"]); + return false; + } + return true; + } + +} diff --git a/src/app/services/user.service.spec.ts b/src/app/services/user.service.spec.ts index 9950ac52..362561e8 100644 --- a/src/app/services/user.service.spec.ts +++ b/src/app/services/user.service.spec.ts @@ -53,8 +53,8 @@ describe('UserService', () => { "role": "USER", "is_active": true }]; - service.getAllUsers().subscribe(res => { - expect(res).toEqual(userResponse); + service.getAllUsers({}).subscribe(res => { + expect(res.body).toEqual(userResponse); }); const req = httpMock.expectOne(`${service.usersAPIURL}/users`); expect(req.request.method).toBe('GET'); @@ -98,6 +98,23 @@ describe('UserService', () => { } ); + it('should update user role', () => { + const userResponse: any = { + "id": 1, + "name": "Mario", + "connection": "ALUNO", + "email": "mario@gmail.com", + "role": "USER", + "is_active": true + } + service.updateUserRole(1).subscribe(res => { + expect(res).toEqual(userResponse); + }); + const req = httpMock.expectOne(`${service.usersAPIURL}/users/role/1`); + expect(req.request.method).toBe('PATCH'); + req.flush(userResponse); + }) + afterEach(() => { httpMock.verify(); } diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index e95a63c7..bafbb1cd 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -2,6 +2,16 @@ import { Injectable } from '@angular/core'; import { environment } from '../environment/environment'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; +import jwt_decode from 'jwt-decode'; + +interface IGetAllUsers { + name?: string; + email?: string; + nameEmail?: string; + connection?: string; + offset?: number; + limit?: number; +} @Injectable({ providedIn: 'root' @@ -15,14 +25,37 @@ export class UserService { return this.http.get(`${this.usersAPIURL}/users/${id}`); } - getAllUsers(): Observable { - return this.http.get(`${this.usersAPIURL}/users`); + getAllUsers({ name, email, nameEmail, connection, offset, limit }: IGetAllUsers): Observable { + const params = { + ...(name && { name__like: name }), + ...(email && { email__like: email }), + ...(nameEmail && { name_or_email: nameEmail }), + ...(connection && { connection }), + ...(offset && { offset: offset.toString() }), + ...(limit && { limit: limit.toString() }), + }; + + const searchParams = new URLSearchParams(params); + const queryString = searchParams.toString(); + + return this.http.get(`${this.usersAPIURL}/users${queryString && '?' + queryString}`, {observe: 'response'}); + } + + getRoles() { + const access_token = localStorage.getItem("token"); + const payload: any = jwt_decode(access_token as string); + const userRole: string = payload.role; + return userRole; } updateUser(id: any, body: any): Observable { return this.http.patch(`${this.usersAPIURL}/users/${id}`, body); } + updateUserRole(id: any): Observable { + return this.http.patch(`${this.usersAPIURL}/users/role/${id}`, {}); + } + deleteUser(id: any): Observable { return this.http.delete(`${this.usersAPIURL}/users/${id}`); } @@ -30,4 +63,4 @@ export class UserService { getVinculo(): Observable { return this.http.get(`${this.usersAPIURL}/auth/vinculo`); } -} +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 29761c0e..04d7175f 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,9 @@ + + +