Skip to content

Commit bf2dd0a

Browse files
feat: adicionar modal de login e proteger rotas com authGuard
1 parent 059a865 commit bf2dd0a

10 files changed

Lines changed: 385 additions & 7 deletions

File tree

src/app/app.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<app-header />
2+
23
<main class="main-content">
34
<router-outlet />
45
</main>
5-
<app-footer />
6+
7+
<app-footer />
8+
9+
<!-- Modal de login — renderizado condicionalmente por cima de tudo -->
10+
<app-login-modal
11+
*ngIf="showLoginModal"
12+
(closed)="onModalClosed()">
13+
</app-login-modal>

src/app/app.routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Routes } from '@angular/router';
2+
import { authGuard } from './services/auth-guard';
23

34
export const routes: Routes = [
45
{
@@ -8,11 +9,13 @@ export const routes: Routes = [
89
},
910
{
1011
path: 'chat',
12+
canActivate: [authGuard],
1113
loadComponent: () =>
1214
import('./pages/chat/chat').then(m => m.ChatComponent)
1315
},
1416
{
1517
path: 'upload',
18+
canActivate: [authGuard],
1619
loadComponent: () =>
1720
import('./pages/upload/upload').then(m => m.UploadComponent)
1821
},

src/app/app.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
1-
import { Component } from '@angular/core';
1+
import { Component, OnInit } from '@angular/core';
22
import { RouterOutlet } from '@angular/router';
3+
import { CommonModule } from '@angular/common';
34
import { HeaderComponent } from './layout/header/header';
45
import { FooterComponent } from './layout/footer/footer';
6+
import { LoginModalComponent } from './components/login-modal/login-modal';
7+
import { AuthModalService } from './services/auth-modal';
58

69
@Component({
710
selector: 'app-root',
811
standalone: true,
9-
imports: [RouterOutlet, HeaderComponent, FooterComponent],
12+
imports: [RouterOutlet, CommonModule, HeaderComponent, FooterComponent, LoginModalComponent],
1013
templateUrl: './app.html',
11-
styleUrls: ['./app.css']
14+
styleUrl: './app.css'
1215
})
13-
export class AppComponent {}
16+
export class AppComponent implements OnInit {
17+
18+
// Controla a visibilidade do modal
19+
showLoginModal = false;
20+
21+
constructor(private authModalService: AuthModalService) {}
22+
23+
// ========================================
24+
// ngOnInit - Escuta o evento do AuthGuard
25+
// para abrir o modal quando necessário.
26+
// ========================================
27+
ngOnInit(): void {
28+
this.authModalService.openModal$.subscribe(() => {
29+
this.showLoginModal = true;
30+
});
31+
}
32+
33+
// ========================================
34+
// onModalClosed - Fecha o modal após
35+
// login bem-sucedido emitido pelo componente.
36+
// ========================================
37+
onModalClosed(): void {
38+
this.showLoginModal = false;
39+
}
40+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* Overlay escuro que cobre toda a tela */
2+
.modal-overlay {
3+
position: fixed;
4+
inset: 0;
5+
background-color: rgba(2, 6, 23, 0.48); /* slightly less opaque */
6+
display: flex;
7+
align-items: center;
8+
justify-content: center;
9+
z-index: 1050;
10+
backdrop-filter: blur(4px);
11+
animation: fadeIn 0.18s ease;
12+
}
13+
14+
/* Caixa do modal */
15+
.modal-box {
16+
background: #ffffff; /* light modal body */
17+
border-radius: 14px;
18+
width: 100%;
19+
max-width: 420px;
20+
box-shadow: 0 20px 40px rgba(2,6,23,0.18);
21+
overflow: hidden;
22+
animation: slideUp 0.22s cubic-bezier(.2,.9,.3,1);
23+
color: #04263b; /* dark text for readability */
24+
}
25+
26+
.modal-header-custom {
27+
background: linear-gradient(90deg, rgba(13,27,45,0.98), rgba(2,6,23,0.98));
28+
color: #e6eef8;
29+
padding: 18px 20px;
30+
display: flex;
31+
gap: 12px;
32+
align-items: center;
33+
}
34+
35+
.header-icon { background: rgba(125,211,252,0.06); padding:10px; border-radius:8px; display:flex; align-items:center; justify-content:center }
36+
.header-icon .fa-robot { color: #7dd3fc; font-size:1.25rem }
37+
.header-text h5 { margin:0; font-weight:700; color: #e6eef8 }
38+
.header-text .hint { font-size:0.86rem; color:#9aa6b2 }
39+
40+
41+
.modal-body-custom { padding: 20px; background: #ffffff; }
42+
43+
.error-box { background: rgba(220,38,38,0.08); color: #6b1414; padding:8px 10px; border-radius:8px; margin-bottom:12px; display:flex; align-items:center }
44+
45+
/* Make input area brighter for readability */
46+
.input-password .input-group-text {
47+
background: #f3fbff;
48+
border: 1px solid rgba(2,6,23,0.06);
49+
color: #04263b;
50+
}
51+
.input-password .form-control {
52+
background: #f7fcff; /* light input for high contrast */
53+
border: 1px solid rgba(2,6,23,0.06);
54+
color: #04263b; /* dark text for readability */
55+
border-radius:8px;
56+
}
57+
.input-password .form-control:focus {
58+
box-shadow: 0 10px 28px rgba(56,189,248,0.08);
59+
border-color: rgba(56,189,248,0.2);
60+
outline: none;
61+
}
62+
63+
/* placeholder color */
64+
.input-password .form-control::placeholder { color: rgba(4,38,59,0.45); }
65+
.input-password .form-control:-ms-input-placeholder { color: rgba(4,38,59,0.45); }
66+
.input-password .form-control::-ms-input-placeholder { color: rgba(4,38,59,0.45); }
67+
68+
.btn-submit { display:inline-flex; align-items:center; justify-content:center; padding:10px 14px; border-radius:10px; border:none; font-weight:700; background: linear-gradient(90deg,#7dd3fc,#38bdf8); color:#04263b }
69+
.btn-submit:disabled { opacity:0.6 }
70+
71+
.modal-footer-custom {
72+
padding:12px 20px;
73+
text-align:center;
74+
background: transparent;
75+
border-top: 1px solid rgba(2,6,23,0.06); /* subtle divider on light background */
76+
color:#64748b;
77+
}
78+
79+
/* Footer note layout and separator */
80+
.footer-note { display:inline-flex; align-items:center; gap:8px; color: #64748b; font-size:0.9rem }
81+
.footer-note .footer-text { display:inline-block }
82+
.footer-note .footer-separator {
83+
width:6px;
84+
height:6px;
85+
background: #cbd5e1;
86+
border-radius:50%;
87+
display:inline-block;
88+
margin:0 6px;
89+
}
90+
91+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
92+
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
93+
94+
/* small screens */
95+
@media (max-width:420px) {
96+
.modal-box { max-width: 92%; }
97+
.modal-header-custom { padding: 14px }
98+
.modal-body-custom { padding: 14px }
99+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!-- Overlay escuro por trás do modal -->
2+
<div class="modal-overlay" role="dialog" aria-modal="true" aria-label="Login">
3+
<div class="modal-box">
4+
5+
<!-- Cabeçalho -->
6+
<div class="modal-header-custom">
7+
<div class="header-icon">
8+
<i class="fas fa-robot" aria-hidden="true"></i>
9+
</div>
10+
<div class="header-text">
11+
<h5 class="mb-0">Assistente de Vendas</h5>
12+
<small class="hint">Digite a senha para acessar</small>
13+
</div>
14+
</div>
15+
16+
<!-- Corpo -->
17+
<div class="modal-body-custom">
18+
19+
<!-- Erro -->
20+
<div class="error-box" *ngIf="errorMessage" role="alert">
21+
<i class="fas fa-circle-exclamation me-2"></i>
22+
<span>{{ errorMessage }}</span>
23+
</div>
24+
25+
<!-- Input de senha -->
26+
<div class="input-group input-password">
27+
<span class="input-group-text">
28+
<i class="fas fa-lock" aria-hidden="true"></i>
29+
</span>
30+
<input
31+
type="password"
32+
class="form-control"
33+
placeholder="Senha de acesso"
34+
[(ngModel)]="password"
35+
(keydown)="onKeyDown($event)"
36+
[disabled]="isLoading"
37+
aria-label="Senha de acesso"
38+
autofocus>
39+
</div>
40+
41+
<!-- Botão -->
42+
<button
43+
class="btn btn-submit w-100 mt-3"
44+
(click)="login()"
45+
[disabled]="isLoading || !password.trim()"
46+
aria-disabled="{{ isLoading || !password.trim() }}">
47+
<span *ngIf="isLoading">
48+
<i class="fas fa-spinner fa-spin me-2"></i>Verificando...
49+
</span>
50+
<span *ngIf="!isLoading">
51+
<i class="fas fa-right-to-bracket me-2"></i>Entrar
52+
</span>
53+
</button>
54+
55+
</div>
56+
57+
<!-- Rodapé -->
58+
<div class="modal-footer-custom">
59+
<small class="footer-note">
60+
<i class="fas fa-shield-halved me-1" aria-hidden="true"></i>
61+
<span class="footer-text">Acesso restrito</span>
62+
<span class="footer-separator" aria-hidden="true"></span>
63+
<span class="footer-text">Portfólio Pessoal</span>
64+
</small>
65+
</div>
66+
67+
</div>
68+
</div>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Component, EventEmitter, Output } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
import { AuthService } from '../../services/auth';
5+
import { Router } from '@angular/router';
6+
import { AuthModalService } from '../../services/auth-modal';
7+
8+
@Component({
9+
selector: 'app-login-modal',
10+
standalone: true,
11+
imports: [CommonModule, FormsModule],
12+
templateUrl: './login-modal.html',
13+
styleUrls: ['./login-modal.css']
14+
})
15+
export class LoginModalComponent {
16+
17+
// ========================================
18+
// closed - Evento emitido ao AppComponent
19+
// para fechar o modal após login bem-sucedido.
20+
// ========================================
21+
@Output() closed = new EventEmitter<void>();
22+
23+
password: string = '';
24+
isLoading: boolean = false;
25+
errorMessage: string = '';
26+
27+
constructor(private authService: AuthService, private router: Router) {}
28+
29+
// ========================================
30+
// login - Envia a senha ao backend.
31+
// Se autenticado com sucesso, fecha o modal
32+
// e redireciona para o chat.
33+
// Se a senha estiver errada, exibe o erro.
34+
// ========================================
35+
login(): void {
36+
if (!this.password.trim() || this.isLoading) return;
37+
38+
this.isLoading = true;
39+
this.errorMessage = '';
40+
41+
this.authService.login(this.password).subscribe({
42+
next: () => {
43+
this.isLoading = false;
44+
this.closed.emit();
45+
this.router.navigate(['/chat']);
46+
},
47+
error: () => {
48+
this.isLoading = false;
49+
this.errorMessage = 'Senha incorreta. Tente novamente.';
50+
this.password = '';
51+
}
52+
});
53+
}
54+
55+
// ========================================
56+
// onKeyDown - Permite enviar com Enter
57+
// sem precisar clicar no botão.
58+
// ========================================
59+
onKeyDown(event: KeyboardEvent): void {
60+
if (event.key === 'Enter') {
61+
this.login();
62+
}
63+
}
64+
}

src/app/pages/upload/upload.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@
9191
<i class="fas fa-broom me-1"></i>Limpar enviados
9292
</button>
9393
<button
94-
class="btn btn-primary btn-sm"
94+
class="btn btn-primary btn-sm btn-send"
9595
[disabled]="!hasPending"
9696
(click)="uploadAll()">
97-
<i class="fas fa-upload me-1"></i>Enviar documentos
97+
<i class="fas fa-upload me-1"></i>Enviar
9898
</button>
9999
</div>
100100
</div>

src/app/services/auth-guard.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { inject } from '@angular/core';
2+
import { CanActivateFn } from '@angular/router';
3+
import { AuthService } from './auth';
4+
import { AuthModalService } from './auth-modal';
5+
6+
// ========================================
7+
// authGuard - Protege as rotas do chat e upload.
8+
// Em vez de redirecionar para /login,
9+
// abre o modal de login por cima da página atual.
10+
// ========================================
11+
export const authGuard: CanActivateFn = () => {
12+
const authService = inject(AuthService);
13+
const authModalService = inject(AuthModalService);
14+
15+
if (authService.isAuthenticated()) {
16+
return true;
17+
}
18+
19+
// Abre o modal e bloqueia a navegação
20+
authModalService.open();
21+
return false;
22+
};

src/app/services/auth-modal.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Injectable } from '@angular/core';
2+
import { Subject } from 'rxjs';
3+
4+
@Injectable({
5+
providedIn: 'root'
6+
})
7+
export class AuthModalService {
8+
9+
// ========================================
10+
// openModal$ - Subject que emite um sinal
11+
// para abrir o modal de login.
12+
// O AppComponent escuta esse evento.
13+
// ========================================
14+
private openModalSubject = new Subject<void>();
15+
openModal$ = this.openModalSubject.asObservable();
16+
17+
// ========================================
18+
// open - Emite o sinal para abrir o modal.
19+
// Chamado pelo AuthGuard quando o usuário
20+
// tenta acessar uma rota protegida sem token.
21+
// ========================================
22+
open(): void {
23+
this.openModalSubject.next();
24+
}
25+
}

0 commit comments

Comments
 (0)