diff --git a/.github/workflows/continous-integration.yml b/.github/workflows/continous-integration.yml index ad605e39..fb86d97a 100755 --- a/.github/workflows/continous-integration.yml +++ b/.github/workflows/continous-integration.yml @@ -28,18 +28,6 @@ jobs: - name: Test 📋 run: npm run test:prod - - - name: Generate JUnit report - run: | - cp test-report-example.xml test-report.xml - - - name: Test Printing Test Report - run: cat test-report.xml - - - name: Remove src/app/app.constant.ts from lcov.info - run: | - grep -v "SF:src/app/app.constant.ts" coverage/unb-tv-frontend/lcov.info > coverage/unb-tv-frontend/lcov_temp.info - mv coverage/unb-tv-frontend/lcov_temp.info coverage/unb-tv-frontend/lcov.info - name: Executa SonarCloud Scan if: ${{ always() }} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bc778ff5..038d38fc 100755 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -23,14 +23,16 @@ import { PrivacyPolicyComponent } from './pages/privacy-policy/privacy-policy.co import { HomeAdminComponent } from './pages/home-admin/home-admin.component'; import { AdminActivateComponent } from './pages/admin-activate/admin-activate.component'; +import { SuperAdminActivateComponent } from './pages/super-admin-activate/super-admin-activate.component'; import { CategoryTableComponent } from './pages/category-table/category-table.component'; import { VideoViewsComponent } from './pages/video-views/video-views.component'; import { RecordComponent } from './pages/record/record.component'; import { DashboardCategoryComponent } from './pages/dashboard-category/dashboard-category.component'; +import { ControleSuperAdminComponent } from './pages/controle-super-admin/controle-super-admin.component'; import { WithTokenGuard } from './guard/with-token.guard'; import { TokenAdminGuard } from './guard/admin.guard'; - +import { TokenSuperAdminGuard } from './guard/super-admin.guard'; const routes: Routes = [ { path: '', component: LoginComponent, canActivate: [WithTokenGuard] }, @@ -93,22 +95,26 @@ const routes: Routes = [ canActivate: [AdminGuard], }, { path: 'privacy', component: PrivacyPolicyComponent }, - - { path: 'homeAdmin', + { + path: 'homeAdmin', component: HomeAdminComponent, canActivate: [TokenAdminGuard], }, { path: 'adminActivate', - component: AdminActivateComponent + component: AdminActivateComponent, + }, + { + path: 'superAdminActivate', + component: SuperAdminActivateComponent, }, - { + { path: 'category-views', component: CategoryTableComponent, canActivate: [TokenAdminGuard], }, - { - path: 'video-views', + { + path: 'video-views', component: VideoViewsComponent, canActivate: [TokenAdminGuard], }, @@ -118,9 +124,14 @@ const routes: Routes = [ canActivate: [TokenAdminGuard], }, { - path: 'record', - component: RecordComponent, - canActivate: [AuthGuard] + path: 'record', + component: RecordComponent, + canActivate: [AuthGuard], + }, + { + path: 'controleSuperAdmin', + component: ControleSuperAdminComponent, + canActivate: [TokenSuperAdminGuard], }, ]; @@ -128,4 +139,4 @@ const routes: Routes = [ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ffade5ad..e77d0785 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -15,6 +15,7 @@ import { SocialLoginModule, SocialAuthServiceConfig, GoogleLoginProvider, Facebo // Declaration import { NgModule, isDevMode } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { AppComponent } from './app.component'; import { LoginComponent } from './pages/login/login.component'; import { RegisterComponent } from './pages/register/register.component'; @@ -46,6 +47,8 @@ import { PrivacyPolicyComponent } from './pages/privacy-policy/privacy-policy.co import { ServiceWorkerModule } from '@angular/service-worker'; import { NgChartsModule } from 'ng2-charts'; import { HomeAdminComponent } from './pages/home-admin/home-admin.component'; +import {ControleSuperAdminComponent} from './pages/controle-super-admin/controle-super-admin.component' + import { CategoryTableComponent } from './pages/category-table/category-table.component'; import { VideoViewsComponent } from './pages/video-views/video-views.component'; @@ -79,6 +82,7 @@ import { RecordComponent } from './pages/record/record.component'; registrationStrategy: 'registerWhenStable:30000' }), NgChartsModule, + CommonModule, ], declarations: [ @@ -108,6 +112,7 @@ import { RecordComponent } from './pages/record/record.component'; VideoViewsComponent, DashboardCategoryComponent, RecordComponent, + ControleSuperAdminComponent, ], providers: [ diff --git a/src/app/guard/admin.guard.ts b/src/app/guard/admin.guard.ts index ba64eab5..e0379df8 100644 --- a/src/app/guard/admin.guard.ts +++ b/src/app/guard/admin.guard.ts @@ -13,7 +13,7 @@ export class TokenAdminGuard implements CanActivate { if (this.authService.isAuthenticated()) { const userRole = this.userService.getRoles(); - if (userRole === 'ADMIN') { + if (userRole === 'ADMIN' || userRole === 'COADMIN') { return true; } else { this.router.navigate(['/loginsocial']); diff --git a/src/app/guard/super-admin.guard.spec.ts b/src/app/guard/super-admin.guard.spec.ts new file mode 100644 index 00000000..d5b949a3 --- /dev/null +++ b/src/app/guard/super-admin.guard.spec.ts @@ -0,0 +1,70 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { TokenSuperAdminGuard } from './super-admin.guard'; +import { AuthService } from '../services/auth.service'; +import { UserService } from '../services/user.service'; +import { AlertService } from '../services/alert.service'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('TokenSuperAdminGuard', () => { + let guard: TokenSuperAdminGuard; + let authService: jasmine.SpyObj; + let userService: jasmine.SpyObj; + let alertService: jasmine.SpyObj; + let router: jasmine.SpyObj; + + beforeEach(() => { + const authServiceSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); + const userServiceSpy = jasmine.createSpyObj('UserService', ['getRoles']); + const alertServiceSpy = jasmine.createSpyObj('AlertService', ['showMessage']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + providers: [ + TokenSuperAdminGuard, + { provide: AuthService, useValue: authServiceSpy }, + { provide: UserService, useValue: userServiceSpy }, + { provide: AlertService, useValue: alertServiceSpy }, + { provide: Router, useValue: routerSpy } + ], + }); + + guard = TestBed.inject(TokenSuperAdminGuard); + authService = TestBed.inject(AuthService) as jasmine.SpyObj; + userService = TestBed.inject(UserService) as jasmine.SpyObj; + alertService = TestBed.inject(AlertService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + }); + + it('should allow access if user is authenticated and role is ADMIN', () => { + authService.isAuthenticated.and.returnValue(true); + userService.getRoles.and.returnValue('ADMIN'); + + const result = guard.canActivate({} as any, {} as any); + + expect(result).toBe(true); + }); + + it('should deny access and navigate to /homeAdmin if user is not ADMIN', () => { + authService.isAuthenticated.and.returnValue(true); + userService.getRoles.and.returnValue('USER'); + + const result = guard.canActivate({} as any, {} as any); + + expect(result).toBe(false); + expect(alertService.showMessage).toHaveBeenCalledWith('error', 'Erro', 'Você não possui acesso!'); + expect(router.navigate).toHaveBeenCalledWith(['/homeAdmin']); + }); + + it('should deny access and navigate to /loginsocial if user is not authenticated', () => { + authService.isAuthenticated.and.returnValue(false); + + const result = guard.canActivate({} as any, { url: '/someUrl' } as any); + + expect(result).toBe(false); + expect(alertService.showMessage).toHaveBeenCalledWith('error', 'Erro', 'Você não está logado!'); + expect(router.navigate).toHaveBeenCalledWith(['/loginsocial'], { queryParams: { returnUrl: '/someUrl' } }); + }); +}); + diff --git a/src/app/guard/super-admin.guard.ts b/src/app/guard/super-admin.guard.ts new file mode 100644 index 00000000..ab3a3dbc --- /dev/null +++ b/src/app/guard/super-admin.guard.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { UserService } from '../services/user.service'; +import { AlertService } from '../services/alert.service'; + +@Injectable({ + providedIn: 'root', +}) +export class TokenSuperAdminGuard implements CanActivate { + constructor( + private authService: AuthService, + private userService: UserService, + private router: Router, + private alertService: AlertService + ) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + if (this.authService.isAuthenticated()) { + const userRole = this.userService.getRoles(); + + if (userRole === 'ADMIN') { + return true; + } else { + this.alertService.showMessage('error', 'Erro', 'Você não possui acesso!') + this.router.navigate(['/homeAdmin']); + return false; + } + } else { + this.alertService.showMessage('error', 'Erro', 'Você não está logado!') + this.router.navigate(['/loginsocial'], { queryParams: { returnUrl: state.url } }); + return false; + } + } +} \ No newline at end of file diff --git a/src/app/pages/category-table/category-table.component.html b/src/app/pages/category-table/category-table.component.html index c8bcdd92..5d6549b2 100644 --- a/src/app/pages/category-table/category-table.component.html +++ b/src/app/pages/category-table/category-table.component.html @@ -13,6 +13,7 @@
  • Dados - Vídeos
  • Dados - Categorias
  • Dashboard - Categorias
  • +
  • Administração de Usuários
  • diff --git a/src/app/pages/controle-super-admin/controle-super-admin.component.css b/src/app/pages/controle-super-admin/controle-super-admin.component.css new file mode 100644 index 00000000..8faa75ee --- /dev/null +++ b/src/app/pages/controle-super-admin/controle-super-admin.component.css @@ -0,0 +1,123 @@ +/* Estilos gerais */ +.home-container { + display: flex; +} + +aside { + width: 13.2em; + background-color: white; + padding: 1em 0.75em; + box-shadow: 0.125em 0 0.3125em rgba(0, 0, 0, 0.1); + text-align: left; + position: fixed; + height: 100%; + margin: 0; +} + +.user-info p { + margin: 0; + font-size: 1.2em; +} + +.linksBarraLateral { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0.75em 0; +} + +.linkLogout, +.linkVoltar { + color: #0087c8; + text-decoration: none; +} + +.linkLogout:hover, +.linkVoltar:hover { + text-decoration: underline; + color: #0056b3; + cursor: pointer; +} + +hr.solid { + border-top: 0.0625em solid #bbb; +} + +nav ul { + list-style: none; + padding: 0; +} + +nav ul li { + margin: 0.9375em 0; +} + +nav ul li a { + text-decoration: none; + color: #1d1d1d; + font-size: 1.1em; +} + +nav ul li a:hover, +nav ul li a.linkSelecionado { + color: #00a550; +} + +main { + flex: 1; + padding: 1em; + margin-left: 14.2em; + display: flex; + flex-direction: column; +} + +header h1 { + font-size: 2.4em; + color: #00a550; + margin-top: 1em; +} + +hr.solid2 { + border-top: 0.125em solid #bbb; +} + +/* Estilos da tabela */ +table { + width: 100%; + border-collapse: collapse; + margin-top: 1em; +} + +thead { + background-color: #00a550; + color: white; +} + +th, +td { + padding: 0.75em; + text-align: left; + border: 1px solid #ddd; +} + +tbody tr:nth-child(even) { + background-color: #f9f9f9; +} + +button { + background-color: #ff4c4c; + color: white; + border: none; + padding: 0.5em 1em; + cursor: pointer; + border-radius: 5px; + margin-right: 0.5em; +} + +.btn-setRole { + background-color: #0087c8; +} + +button:hover { + background-color: #7ad0eb; +} diff --git a/src/app/pages/controle-super-admin/controle-super-admin.component.html b/src/app/pages/controle-super-admin/controle-super-admin.component.html new file mode 100644 index 00000000..779f95e7 --- /dev/null +++ b/src/app/pages/controle-super-admin/controle-super-admin.component.html @@ -0,0 +1,57 @@ +
    + +
    +
    +

    Lista de usuários cadastrados:

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    NomeVínculoEmailRoleAções
    {{ user.name }}{{ user.connection }}{{ user.email }}{{ user.role }} + +
    + + +

    Carregando usuários...

    +
    +
    +
    diff --git a/src/app/pages/controle-super-admin/controle-super-admin.component.spec.ts b/src/app/pages/controle-super-admin/controle-super-admin.component.spec.ts new file mode 100644 index 00000000..837765f7 --- /dev/null +++ b/src/app/pages/controle-super-admin/controle-super-admin.component.spec.ts @@ -0,0 +1,158 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ControleSuperAdminComponent } from './controle-super-admin.component'; +import { UserService } from 'src/app/services/user.service'; +import { AlertService } from 'src/app/services/alert.service'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { AuthService } from 'src/app/services/auth.service'; +import { Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { VideoService } from 'src/app/services/video.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Confirmation } from 'primeng/api'; + +class UserServiceMock { + getAllUsers() { + return of({ + body: [{ id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }] + }); + } + + deleteUser(userId: number) { + // Simulate successful user deletion + return of(null); + } +} + +class AlertServiceMock { + showMessage(type: string, title: string, message: string) { + console.log(`[AlertService] ${type.toUpperCase()}: ${title} - ${message}`); + } + + errorMessage(error: any) { + console.error(`[AlertService] ERROR: ${error.status} - ${error.statusText}`); + } +} + +class ConfirmationServiceMock { + confirm(options: any) { + if (options.accept) { + console.log("[ConfirmationService] User accepted the confirmation."); + options.accept(); + } else if (options.reject) { + console.log("[ConfirmationService] User rejected the confirmation."); + options.reject(); + } + } +} + +class AuthServiceMock { + logout() { + console.log("[AuthService] User logged out."); + } +} + +describe('ControleSuperAdminComponent', () => { + let component: ControleSuperAdminComponent; + let fixture: ComponentFixture; + let userService: UserService; + let alertService: AlertService; + let confirmationService: ConfirmationService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ControleSuperAdminComponent], + imports: [HttpClientTestingModule, RouterTestingModule], + providers: [ + { provide: UserService, useClass: UserServiceMock }, + { provide: AlertService, useClass: AlertServiceMock }, + { provide: ConfirmationService, useClass: ConfirmationServiceMock }, + { provide: AuthService, useClass: AuthServiceMock }, + { provide: VideoService, useClass: VideoService }, + MessageService, + Router + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ControleSuperAdminComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserService); + alertService = TestBed.inject(AlertService); + confirmationService = TestBed.inject(ConfirmationService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should load users on init', () => { + spyOn(userService, 'getAllUsers').and.callThrough(); + component.ngOnInit(); + expect(userService.getAllUsers).toHaveBeenCalled(); + expect(component.users.length).toBe(2); + }); + + it('should handle error when loading users fails', () => { + const errorResponse = new HttpErrorResponse({ + error: 'Erro de rede', + status: 500, + statusText: 'Erro Interno do Servidor' + }); + spyOn(userService, 'getAllUsers').and.returnValue(throwError(() => errorResponse)); + spyOn(alertService, 'errorMessage').and.callThrough(); + component.loadUsers(); + expect(alertService.errorMessage).toHaveBeenCalledWith(errorResponse); + expect(component.loading).toBeFalse(); + }); + + it('should delete a user and reload users', () => { + spyOn(userService, 'deleteUser').and.callThrough(); + spyOn(component, 'loadUsers').and.callThrough(); + component.deleteUser(1); + expect(userService.deleteUser).toHaveBeenCalledWith(1); + expect(component.loadUsers).toHaveBeenCalled(); + }); + + it('should handle error when deleting user fails', () => { + const errorResponse = new HttpErrorResponse({ + error: 'Erro de rede', + status: 500, + statusText: 'Erro Interno do Servidor' + }); + spyOn(userService, 'deleteUser').and.returnValue(throwError(() => errorResponse)); + spyOn(alertService, 'errorMessage').and.callThrough(); + component.deleteUser(1); + expect(alertService.errorMessage).toHaveBeenCalledWith(errorResponse); + }); + + it('should confirm logout', () => { + spyOn(confirmationService, 'confirm').and.callThrough(); + const authService = TestBed.inject(AuthService); + spyOn(authService, 'logout').and.callThrough(); + + component.logoutUser(); + + expect(confirmationService.confirm).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should not logout if reject is called', () => { + spyOn(confirmationService, 'confirm').and.callFake((confirmation: Confirmation) => { + if (confirmation.reject) { + confirmation.reject(); + } + return confirmationService; // Retorna o próprio serviço para respeitar o tipo esperado + }); + + const authService = TestBed.inject(AuthService); + spyOn(authService, 'logout').and.callThrough(); + + component.logoutUser(); + + expect(confirmationService.confirm).toHaveBeenCalled(); + expect(authService.logout).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/app/pages/controle-super-admin/controle-super-admin.component.ts b/src/app/pages/controle-super-admin/controle-super-admin.component.ts new file mode 100644 index 00000000..3c935930 --- /dev/null +++ b/src/app/pages/controle-super-admin/controle-super-admin.component.ts @@ -0,0 +1,79 @@ +import { Component, OnInit } from '@angular/core'; +import { UserService } from 'src/app/services/user.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { AlertService } from 'src/app/services/alert.service'; +import { ConfirmationService } from 'primeng/api'; +import { MessageService } from 'primeng/api'; + +import { AuthService } from 'src/app/services/auth.service'; +import { Router } from '@angular/router'; +import { VideoService } from 'src/app/services/video.service'; + +@Component({ + selector: 'app-controle-super-admin', + templateUrl: './controle-super-admin.component.html', + styleUrls: ['./controle-super-admin.component.css'], +}) +export class ControleSuperAdminComponent implements OnInit { + users: any[] = []; + loading: boolean = true; + + constructor( + private videoService: VideoService, + private router: Router, + private authService: AuthService, + private userService: UserService, + private alertService: AlertService, + private confirmationService: ConfirmationService, + private messageService: MessageService + ) {} + + ngOnInit(): void { + this.loadUsers(); + } + + loadUsers() { + this.userService.getAllUsers({}).subscribe({ + next: (response: any) => { + console.log('API response:', response); + this.users = response.body; + this.loading = false; + }, + error: (error: HttpErrorResponse) => { + console.error('Error loading users:', error); + this.loading = false; + this.alertService.errorMessage(error); + }, + }); + } + + deleteUser(userId: any) { + this.userService.deleteUser(userId).subscribe({ + next: () => { + this.alertService.showMessage( + 'success', + 'Sucesso', + 'Usuário excluído com sucesso.' + ); + this.loadUsers(); // Recarrega a lista de usuários após exclusão + }, + error: (error: HttpErrorResponse) => { + console.error('Error deleting user:', error); + this.alertService.errorMessage(error); + }, + }); + } + + logoutUser() { + this.confirmationService.confirm({ + message: 'Tem certeza que deseja sair?', + header: 'Confirmação', + key: 'myDialog', + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.authService.logout(); + }, + reject: () => {}, + }); + } +} diff --git a/src/app/pages/dashboard-category/dashboard-category.component.html b/src/app/pages/dashboard-category/dashboard-category.component.html index 3f99126b..3d89799f 100644 --- a/src/app/pages/dashboard-category/dashboard-category.component.html +++ b/src/app/pages/dashboard-category/dashboard-category.component.html @@ -13,6 +13,7 @@
  • Dados - Vídeos
  • Dados - Categorias
  • Dashboard - Categorias
  • +
  • Administração de Usuários
  • diff --git a/src/app/pages/home-admin/home-admin.component.html b/src/app/pages/home-admin/home-admin.component.html index 0bc84a1c..89fa701a 100644 --- a/src/app/pages/home-admin/home-admin.component.html +++ b/src/app/pages/home-admin/home-admin.component.html @@ -3,22 +3,23 @@ -
    +

    Olá, administrador!

    -
    +
    @@ -33,4 +34,3 @@

    Olá, administrador!

    - diff --git a/src/app/pages/super-admin-activate/super-admin-activate.component.css b/src/app/pages/super-admin-activate/super-admin-activate.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/super-admin-activate/super-admin-activate.component.html b/src/app/pages/super-admin-activate/super-admin-activate.component.html new file mode 100644 index 00000000..3c4c3fe3 --- /dev/null +++ b/src/app/pages/super-admin-activate/super-admin-activate.component.html @@ -0,0 +1 @@ +

    super-admin-activate works!

    diff --git a/src/app/pages/super-admin-activate/super-admin-activate.component.spec.ts b/src/app/pages/super-admin-activate/super-admin-activate.component.spec.ts new file mode 100644 index 00000000..0d14ed57 --- /dev/null +++ b/src/app/pages/super-admin-activate/super-admin-activate.component.spec.ts @@ -0,0 +1,121 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SuperAdminActivateComponent } from './super-admin-activate.component'; +import { AuthService } from 'src/app/services/auth.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AlertService } from 'src/app/services/alert.service'; +import { of, throwError, Subscription } from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { MessageService } from 'primeng/api'; + +class MockAuthService { + super_admin_setup(data: any) { + return of({ success: true }); + } +} + +class MockAlertService { + showMessage() {} + errorMessage() {} +} + +class MockActivatedRoute { + queryParams = of({ email: 'test@example.com' }); +} + +describe('SuperAdminActivateComponent', () => { + let component: SuperAdminActivateComponent; + let fixture: ComponentFixture; + let authService: AuthService; + let alertService: AlertService; + let router: Router; + let route: ActivatedRoute; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SuperAdminActivateComponent], + imports: [HttpClientTestingModule, RouterTestingModule], + providers: [ + { provide: AuthService, useClass: MockAuthService }, + { provide: AlertService, useClass: MockAlertService }, + { provide: ActivatedRoute, useClass: MockActivatedRoute }, + MessageService, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SuperAdminActivateComponent); + component = fixture.componentInstance; + authService = TestBed.inject(AuthService); + alertService = TestBed.inject(AlertService); + router = TestBed.inject(Router); + route = TestBed.inject(ActivatedRoute); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call setupSuperAdmin if email is provided', () => { + spyOn(component, 'setupSuperAdmin').and.callThrough(); + spyOn(authService, 'super_admin_setup').and.returnValue(of({})); + spyOn(router, 'navigate'); + + component.ngOnInit(); + + expect(component.setupSuperAdmin).toHaveBeenCalledWith('test@example.com'); + expect(authService.super_admin_setup).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/homeAdmin']); + }); + + it('should handle error if no email is provided', () => { + (route.queryParams as any) = of({}); + + spyOn(alertService, 'errorMessage').and.callThrough(); + spyOn(router, 'navigate'); + + component.ngOnInit(); + + expect(alertService.errorMessage).toHaveBeenCalledWith({ + message: 'Email não fornecido', + }); + expect(router.navigate).toHaveBeenCalledWith(['/loginsocial']); + }); + + it('should show success message on successful admin setup', () => { + spyOn(authService, 'super_admin_setup').and.returnValue(of({})); + spyOn(alertService, 'showMessage').and.callThrough(); + spyOn(router, 'navigate'); + + component.setupSuperAdmin('test@example.com'); + + expect(alertService.showMessage).toHaveBeenCalledWith( + 'success', + 'Sucesso', + 'Super Admin configurado com sucesso!' + ); + expect(alertService.showMessage).toHaveBeenCalledWith( + 'info', + 'Alerta', + 'Saia da sua conta e entre novamente para acessar a tela de administrador.' + ); + expect(router.navigate).toHaveBeenCalledWith(['/homeAdmin']); + }); + + it('should handle error during admin setup', () => { + spyOn(authService, 'super_admin_setup').and.returnValue( + throwError(() => ({ + error: { detail: 'Erro desconhecido' }, + })) + ); + spyOn(alertService, 'errorMessage').and.callThrough(); + spyOn(router, 'navigate'); + + component.setupSuperAdmin('test@example.com'); + + expect(alertService.errorMessage).toHaveBeenCalledWith({ + message: 'Erro desconhecido', + }); + expect(router.navigate).toHaveBeenCalledWith(['/loginsocial']); + }); +}); diff --git a/src/app/pages/super-admin-activate/super-admin-activate.component.ts b/src/app/pages/super-admin-activate/super-admin-activate.component.ts new file mode 100644 index 00000000..80b13db6 --- /dev/null +++ b/src/app/pages/super-admin-activate/super-admin-activate.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { AuthService } from 'src/app/services/auth.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AlertService } from 'src/app/services/alert.service'; +import { IError } from 'src/shared/model/http-error.model'; +import { HttpErrorResponse } from '@angular/common/http'; + +type ErrorResponseType = HttpErrorResponse; + +@Component({ + selector: 'app-super-admin-activate', + templateUrl: './super-admin-activate.component.html', + styleUrls: ['./super-admin-activate.component.css'], +}) +export class SuperAdminActivateComponent implements OnInit { + constructor( + private authService: AuthService, + private route: ActivatedRoute, + private router: Router, + private alertService: AlertService + ) {} + + ngOnInit(): void { + this.route.queryParams.subscribe({ + next: (params: any) => { + const email = params['email']; + if (email) { + this.setupSuperAdmin(email); + } else { + const error: IError = { message: 'Email não fornecido' }; + this.alertService.errorMessage(error); + this.router.navigate(['/loginsocial']); + } + }, + error: (error: ErrorResponseType) => { + const errorMsg: IError = { + message: error.error.detail || 'Erro ao obter parâmetros da URL', + }; + this.alertService.errorMessage(errorMsg); + this.router.navigate(['/loginsocial']); + }, + }); + } + + setupSuperAdmin(email: string): void { + const data = { email: email }; + + this.authService.super_admin_setup(data).subscribe({ + next: () => { + this.alertService.showMessage( + 'success', + 'Sucesso', + 'Super Admin configurado com sucesso!' + ); + this.alertService.showMessage( + 'info', + 'Alerta', + 'Saia da sua conta e entre novamente para acessar a tela de administrador.' + ); + this.router.navigate(['/homeAdmin']); + }, + error: (error: ErrorResponseType) => { + const errorMsg: IError = { + message: error.error.detail || 'Erro desconhecido', + }; + this.alertService.errorMessage(errorMsg); + this.router.navigate(['/loginsocial']); + }, + }); + } +} diff --git a/src/app/pages/video-viewer/video-viewer.component.css b/src/app/pages/video-viewer/video-viewer.component.css index 0dfa5e17..d3abb1fe 100755 --- a/src/app/pages/video-viewer/video-viewer.component.css +++ b/src/app/pages/video-viewer/video-viewer.component.css @@ -4,75 +4,68 @@ -webkit-line-clamp: 3; overflow: hidden; } +.button-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 1.25rem; +} -.watch-later-button { +.favorite-button, .watch-later-button, .next-video-button { background-color: #0087c8; color: #fff; border: none; - padding: 0.15em 0.9em; - border-radius: 5em; + padding: 0.5rem 1rem; + border-radius: 2.5rem; cursor: pointer; - font-size: 1.2em; + font-size: 1.2rem; display: flex; align-items: center; - gap: 0.3em; -} - -.watch-later-button.watch-later-active { - background-color: #f00; /* Cor para quando o vídeo estiver na lista de assistir mais tarde */ -} - -.watch-later-button:hover { - background-color: #00000094; /* Fundo ao passar o mouse */ - color: white; /* Cor das letras ao passar o mouse */ -} - -.watch-later-button:active { - background-color: red; /* Fundo ao clicar */ - color: white; /* Cor das letras ao clicar */ + gap: 0.5rem; + transition: background-color 0.3s, transform 0.2s; } -.title-button-container { - display: flex; - justify-content: space-between; - align-items: center; +.favorite-button.favorited, .watch-later-button.watch-later-active { + background-color: #f00; } -.favorite-button { - color: white; - background-color: #0087c8; - padding: 0.15em 0.9em; - border: none; - cursor: pointer; - border-radius: 5em; - font-size: 1.2em; - display: flex; /* Utiliza flexbox para alinhar o conteúdo */ - align-items: center; /* Alinha verticalmente o conteúdo */ - gap: 0.3em; +.favorite-button:hover, .watch-later-button:hover, .next-video-button:hover { + background-color: #00000094; + color: #fff; + transform: scale(1.05); } -.favorite-button.favorited { - background-color: red; /* Fundo quando favorito */ - color: white; /* Cor das letras quando favorito */ +.favorite-button:active, .watch-later-button:active, .next-video-button:active { + background-color: red; + color: #fff; + transform: scale(0.95); } .favorite-icon { - display: flex; - width: 1em; /* Define a largura do ícone */ - height: 1em; /* Define a altura do ícone */ - fill: #ffffff; /* Define a cor do ícone */ + width: 1.2rem; + height: 1.2rem; + fill: #ffffff; } -.favorite-button:hover { - background-color: #0085c894; /* Fundo ao passar o mouse */ - color: white; /* Cor das letras ao passar o mouse */ +.next-video-button { + background-color: #00a651; } -.favorite-button:active { - background-color: red; /* Fundo ao clicar */ - color: white; /* Cor das letras ao clicar */ +.next-video-button:hover { + background-color: #008c4a; } -button.favorited span { - color: rgb(255, 255, 255); +@media (max-width: 1180px) { + .button-container { + flex-direction: column; + gap: 0.7rem; + align-items: flex-start; + font-size: 0.5rem; + line-height: 1rem; + } + + .favorite-button, .watch-later-button, .next-video-button { + width: 100%; + justify-content: center; + } } diff --git a/src/app/pages/video-viewer/video-viewer.component.html b/src/app/pages/video-viewer/video-viewer.component.html index e05d0762..46580ec3 100755 --- a/src/app/pages/video-viewer/video-viewer.component.html +++ b/src/app/pages/video-viewer/video-viewer.component.html @@ -15,7 +15,7 @@

    {{ video.title }}

    -
    +
    +
    { expect(addToRecordSpy(component.userId, component.idVideo.toString()).subscribe).toBeDefined(); }); - it('should check tracking status and set trackingEnabled correctly', fakeAsync(() => { + it('should filter videos by record and set filteredVideos correctly video-viewer', () => { + const recordVideos = { + videos: { + 190329: true, + 190330: true, + }, + }; + + const unbTvVideos = [ + { id: 190329, title: 'Video Title 1' }, + { id: 190330, title: 'Video Title 2' }, + { id: 190331, title: 'Video Title 3' }, + ]; + + component.recordVideos = recordVideos; + component.unbTvVideos = unbTvVideos; + + component.filterVideosByRecord(); + fixture.detectChanges(); + + const expectedFilteredVideos = [ + { id: 190329, title: 'Video Title 1' }, + { id: 190330, title: 'Video Title 2' }, + ]; + + expect(component.filteredVideos).toEqual(expectedFilteredVideos); + }); + + it('should filter videos by channel and populate unbTvVideos video-viewer', () => { + const mockVideos: IVideo[] = [ + { id: 1, title: 'Video 1', channels: [{ id: 12, name: "unbtvchannel" }] }, + { id: 2, title: 'Video 2', channels: [{ id: 13, name: "otherchannel" }] } + ]; + + component.unbTvChannelId = 12; + component.unbTvVideos = []; + + component.filterVideosByChannel(mockVideos); + + expect(component.unbTvVideos.length).toBe(1); + expect(component.unbTvVideos[0].id).toBe(1); + }); + + it('should call checkRecord service method and set recordVideos video-viewer', async () => { + const expectedResponse = [{ id: 1, title: 'Video 1' }]; + const checkRecordSpy = spyOn(videoService, 'checkRecord').and.returnValue(of(expectedResponse)); + + component.userId = '12345'; + + await component.checkRecord(); + + expect(checkRecordSpy).toHaveBeenCalledWith('12345'); + expect(component.recordVideos).toEqual(expectedResponse); + }); + + /*it('should check tracking status and set trackingEnabled correctly', fakeAsync(() => { const mySpy = spyOn(videoService, 'checkTrackingStatus').and.returnValue(of({ track_enabled: true })); component.userId = '1'; tick(); // Simulate passage of time for the async call @@ -328,5 +407,5 @@ describe('VideoViewerComponent', () => { expect(mySpy).toHaveBeenCalledWith('1'); expect(component.trackingEnabled).toBe(true); }); - })); + }));*/ }); \ No newline at end of file diff --git a/src/app/pages/video-viewer/video-viewer.component.ts b/src/app/pages/video-viewer/video-viewer.component.ts index 477d3520..6cd3f479 100755 --- a/src/app/pages/video-viewer/video-viewer.component.ts +++ b/src/app/pages/video-viewer/video-viewer.component.ts @@ -7,6 +7,9 @@ import { UserService } from 'src/app/services/user.service'; import { AlertService } from 'src/app/services/alert.service'; import { AuthService } from 'src/app/services/auth.service'; import jwt_decode from 'jwt-decode'; +import { UNB_TV_CHANNEL_ID } from 'src/app/app.constant'; +import { Catalog } from 'src/shared/model/catalog.model'; +import { Router } from '@angular/router'; @Component({ selector: 'app-video-viewer', @@ -23,14 +26,27 @@ export class VideoViewerComponent implements OnInit { isFavorite = true; eduplayVideoUrl = "https://eduplay.rnp.br/portal/video/embed/"; userId: string = ''; - user: any; - trackingEnabled: boolean = true; // Estado do rastreamento + user : any; + unbTvChannelId = UNB_TV_CHANNEL_ID; + videosEduplay: IVideo[] = []; + unbTvVideos: IVideo[] = []; + catalog: Catalog = new Catalog(); + categoryVideo: any; + videosAssistidos: IVideo[] = []; + recordVideos: any; + filteredVideos: IVideo[] = []; + program: any; + videosByCategory: IVideo[] = []; + idNextVideo: number; + titleNextVideo: any; + showTitleNextVideo: boolean = false; expandDescription() { this.showDescription = !this.showDescription; } constructor( + private router: Router, private route: ActivatedRoute, private authService: AuthService, private videoService: VideoService, @@ -38,46 +54,113 @@ export class VideoViewerComponent implements OnInit { private alertService: AlertService ) { } -ngOnInit(): void { + ngOnInit(): void { const iframe = document.getElementById('embeddedVideo') as HTMLIFrameElement; this.idVideo = this.route.snapshot.params['idVideo']; - if (this.authService.isAuthenticated()) { - this.setUserIdFromToken(localStorage.getItem('token') as string); - this.getUserDetails(); - this.checkTrackingStatus().then(() => { - if (this.trackingEnabled) { - this.addRecord(); - } - }); + if (this.authService.isAuthenticated()){ + this.setUserIdFromToken(localStorage.getItem('token') as string); + this.getUserDetails(); + this.addRecord(); } this.findVideoById(); iframe.src = this.eduplayVideoUrl + this.idVideo; -} + this.checkRecord(); + this.findAll(); + } - setUserIdFromToken(token: string) { - const decodedToken: any = jwt_decode(token); - this.userId = decodedToken.id; + checkRecord(): Promise { + return new Promise((resolve, reject) => { + this.videoService.checkRecord(this.userId.toString()).subscribe({ + next: (response) => { + this.recordVideos = response; + resolve(); + }, + error: (err) => { + console.error('Error checking record', err); + reject(err); + } + }); + }); + } + + //Função responsável por trazer todos os vídeos já assistidos pelo usuário + filterVideosByRecord(): void { + const keys = Object.keys(this.recordVideos.videos).map(id => parseInt(id, 10)) + this.filteredVideos = this.unbTvVideos.filter(video => video.id !== undefined && keys.includes(video.id)); } -async checkTrackingStatus(): Promise { - const status = await this.videoService.checkTrackingStatus(this.userId).toPromise(); - this.trackingEnabled = status.track_enabled; - console.log('Tracking status:', this.trackingEnabled); -} + findAll(): void { + this.videoService.findAll().subscribe({ + next: (data) => { + this.videosEduplay = data.body?.videoList ?? []; + this.filterVideosByChannel(this.videosEduplay); + this.videoService.videosCatalog(this.unbTvVideos, this.catalog) - addRecord() { - this.videoService.addToRecord(this.userId, this.idVideo.toString()).subscribe({ - next: () => { + //Loop para encontrar a categoria do vídeo atual + this.unbTvVideos.forEach((video) => { + if(video.id == this.idVideo){ + this.categoryVideo = video.catalog + return; + } + }) + //Chamada de função para encontrar o programa do vídeo atual + this.program = this.videoService.findProgramName(this.catalog, this.categoryVideo, this.idVideo); + + this.videosByCategory = this.videoService.filterVideosByCategory(this.unbTvVideos, this.categoryVideo); + console.log("vídeos da categoria do atual: ", this.videosByCategory) + this.filterVideosByRecord(); + console.log("videos assistidos: ", this.filteredVideos) + this.idNextVideo = this.videoService.recommendVideo(this.videosByCategory, this.catalog, this.categoryVideo, this.filteredVideos, this.program); + //Se o id for diferente de -1, o usuário ainda não viu todos os vídeos da categoria atual + if(this.idNextVideo != -1){ + //Loop para encontrar o título do próximo vídeo + this.unbTvVideos.forEach((video) => { + if(video.id == this.idNextVideo){ + this.titleNextVideo = video.title; + return; + } + }) + }else{ + this.titleNextVideo = "Não há vídeo para ser recomendado" + } + console.log("id do próximo vídeo: ", this.idNextVideo) + console.log("título do próximo vídeo: ", this.titleNextVideo) }, - error: (err) => { - console.error('Error fetching user details', err); + error: (error) => { + console.log(error); + } + }); + } + + filterVideosByChannel(videos: IVideo[]): void { + videos.forEach((video) => { + const channel = video?.channels; + if (channel && channel[0].id === this.unbTvChannelId) { + this.unbTvVideos.push(video); } }); } - getUserDetails() { + nextVideo(): void { + if(this.idNextVideo != -1){ + this.router.navigate([`/video/${this.idNextVideo}`]).then(() => { + window.location.reload(); + }); + }else{ + this.router.navigate([`/catalog`]).then(() => { + window.location.reload(); + }); + } + } + + setUserIdFromToken(token: string) { + const decodedToken: any = jwt_decode(token); + this.userId = decodedToken.id; + } + + getUserDetails() { this.userService.getUser(this.userId).subscribe({ next: (user) => { this.user = user; @@ -90,17 +173,29 @@ async checkTrackingStatus(): Promise { }); } - findVideoById() { + addRecord() { + this.videoService.addToRecord(this.userId, this.idVideo.toString()).subscribe({ + next: () => { + }, + error: (err) => { + console.error('Error fetching user details', err); + } + }); + } + + findVideoById = () => { this.videoService.findVideoById(this.idVideo).subscribe({ next: (data: HttpResponse) => { this.video = data.body ? data.body : this.video; - this.videoDescription = this.video.description ? this.video.description : ''; + this.videoDescription = this.video.description + ? this.video.description + : ''; }, error: (err) => { console.error('Error fetching video details', err); } }); - } + }; // Assistir mais tarde toggleWatchLater() { diff --git a/src/app/pages/video-views/video-views.component.html b/src/app/pages/video-views/video-views.component.html index 8881ac66..c29d9d2c 100755 --- a/src/app/pages/video-views/video-views.component.html +++ b/src/app/pages/video-views/video-views.component.html @@ -7,39 +7,66 @@ Voltar
    -
    +
    - +

    Dados - Vídeos

    - +
    -
    +
    - +
    - +
    - +