feat: implement logout functionality and enhance contact layout
This commit is contained in:
parent
38f305067a
commit
e83e2b2161
@ -89,30 +89,37 @@
|
||||
<ul class="social-list">
|
||||
|
||||
@if (model.socialLinks?.linkedin) {
|
||||
<li class="social-item">
|
||||
<a href="{{model.socialLinks?.linkedin}}" target="_blank" class="social-link">
|
||||
<i class="fa-brands fa-linkedin"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="social-item">
|
||||
<a href="{{model.socialLinks?.linkedin}}" target="_blank" class="social-link">
|
||||
<i class="fa-brands fa-linkedin"></i>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (model.socialLinks?.gitHub) {
|
||||
<li class="social-item">
|
||||
<a href="{{model.socialLinks?.gitHub}}" target="_blank" class="social-link">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="social-item">
|
||||
<a href="{{model.socialLinks?.gitHub}}" target="_blank" class="social-link">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (model.socialLinks?.blogUrl) {
|
||||
<li class="social-item">
|
||||
<a href="{{model.socialLinks?.blogUrl}}" target="_blank" class="social-link">
|
||||
<i class="fa-duotone fa-blog"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="social-item">
|
||||
<a href="{{model.socialLinks?.blogUrl}}" target="_blank" class="social-link">
|
||||
<i class="fa-duotone fa-blog"></i>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<hr class="logout-divider" />
|
||||
|
||||
<div class="logout-section">
|
||||
<button class="logout-btn" (click)="logout()">
|
||||
<span class="icon">🔓</span>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@ -1,3 +1,62 @@
|
||||
img {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
margin-top: 14px;
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
padding: 14px 18px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
background: #191919; // darker for better contrast
|
||||
border: 1px solid rgba(227, 179, 65, 0.18); // subtle gold border
|
||||
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
color: #e3b341; // gold text
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
|
||||
transition: 0.3s ease;
|
||||
|
||||
/* Subtle depth */
|
||||
box-shadow:
|
||||
inset 0 0 8px rgba(255, 255, 255, 0.03),
|
||||
0 4px 18px rgba(0, 0, 0, 0.45);
|
||||
|
||||
&:hover {
|
||||
background: rgba(227, 179, 65, 0.12);
|
||||
|
||||
border-color: rgba(227, 179, 65, 0.35);
|
||||
|
||||
box-shadow:
|
||||
0 0 12px rgba(227, 179, 65, 0.25),
|
||||
inset 0 0 10px rgba(227, 179, 65, 0.1);
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.logout-divider {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
margin: 18px 0 14px 0;
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { IContactModel } from './contact.model';
|
||||
import { AdminService } from '../services/admin.service';
|
||||
import { BaseComponent } from '../base.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AuthService } from '../../auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
@ -13,6 +14,7 @@ import { CommonModule } from '@angular/common';
|
||||
export class Contact extends BaseComponent<IContactModel> implements OnInit {
|
||||
sideBarExpanded = false;
|
||||
displayName!: string;
|
||||
authSvc: AuthService = inject(AuthService);
|
||||
constructor(){
|
||||
const svc = inject(AdminService);
|
||||
super(svc);
|
||||
@ -28,4 +30,8 @@ export class Contact extends BaseComponent<IContactModel> implements OnInit {
|
||||
this.assignData(response);
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authSvc.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,9 +56,9 @@
|
||||
<a>
|
||||
|
||||
<figure class="project-img">
|
||||
<!-- <div class="project-item-icon-box">
|
||||
<ion-icon name="eye-outline"></ion-icon>
|
||||
</div> -->
|
||||
<div class="project-item-icon-box">
|
||||
<ion-icon name="eye-outline"></ion-icon>
|
||||
</div>
|
||||
|
||||
<img src="{{imagesOrigin + project.imagePath}}" alt="{{project.name}}" loading="lazy">
|
||||
</figure>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit } from '@angular/core';
|
||||
import { IProject } from '../models/project.model';
|
||||
import { BaseComponent } from '../base.component';
|
||||
import { AdminService } from '../services/admin.service';
|
||||
@ -10,7 +10,8 @@ import { CommonModule } from '@angular/common';
|
||||
selector: 'app-projects',
|
||||
templateUrl: './projects.html',
|
||||
styleUrl: './projects.scss',
|
||||
imports: [CommonModule]
|
||||
imports: [CommonModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class Projects extends BaseComponent<IProjects> implements OnInit {
|
||||
filter = 'All';
|
||||
|
||||
@ -4,13 +4,12 @@ import { routes } from './app.routes';
|
||||
import { provideHttpClient, withFetch, withInterceptors} from '@angular/common/http';
|
||||
import { loadingInterceptor } from './interceptors/loading-interceptor';
|
||||
import { AuthInterceptor } from './interceptors/auth-interceptor';
|
||||
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZonelessChangeDetection(),
|
||||
provideHttpClient(withInterceptors([loadingInterceptor, AuthInterceptor]), withFetch()),
|
||||
provideRouter(routes), provideClientHydration(withEventReplay())
|
||||
provideRouter(routes)
|
||||
]
|
||||
};
|
||||
|
||||
@ -19,6 +19,11 @@ export const routes: Routes = [
|
||||
redirectTo: 'admin/about',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
redirectTo: 'admin/about',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
component: AdminLayout,
|
||||
|
||||
@ -25,6 +25,7 @@ export class AuthService {
|
||||
router = inject(Router);
|
||||
|
||||
private readonly storageKey = 'accessToken';
|
||||
tokenReady$ = new BehaviorSubject<boolean | null>(null);
|
||||
|
||||
constructor() {
|
||||
console.log('🔥 AuthService constructor started');
|
||||
@ -36,31 +37,31 @@ export class AuthService {
|
||||
return `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||
}
|
||||
|
||||
// // Call on app start, or from guard
|
||||
// async ensureTokenReady(): Promise<void>{
|
||||
// if(this.tokenReady$.value) return;
|
||||
// Call on app start, or from guard
|
||||
async ensureTokenReady(): Promise<void>{
|
||||
if(this.tokenReady$.value) return;
|
||||
|
||||
// const stored = this.safeGetToken();
|
||||
const stored = this.safeGetToken();
|
||||
|
||||
// // try to restore from storage
|
||||
// if(stored){
|
||||
// this.accessTokenSub.next(stored);
|
||||
// this.tokenReady$.next(true);
|
||||
// return;
|
||||
// }
|
||||
// try to restore from storage
|
||||
if(stored){
|
||||
this.accessTokenSub.next(stored);
|
||||
this.tokenReady$.next(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// // // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
|
||||
// // try {
|
||||
// // const res = await firstValueFrom(this.refreshToken());
|
||||
// // this.safeSetToken(res.accessToken);
|
||||
// // }
|
||||
// // catch{
|
||||
// // console.warn('Silent token refresh failed on startup');
|
||||
// // }
|
||||
// // finally{
|
||||
// // this.tokenReady$.next(true);
|
||||
// // }
|
||||
// }
|
||||
// // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
|
||||
// try {
|
||||
// const res = await firstValueFrom(this.refreshToken());
|
||||
// this.safeSetToken(res.accessToken);
|
||||
// }
|
||||
// catch{
|
||||
// console.warn('Silent token refresh failed on startup');
|
||||
// }
|
||||
// finally{
|
||||
// this.tokenReady$.next(true);
|
||||
// }
|
||||
}
|
||||
|
||||
get currentToken(): string | null {
|
||||
return this.safeGetToken();
|
||||
@ -123,7 +124,7 @@ export class AuthService {
|
||||
logout(): Observable<void> {
|
||||
this.accessToken = null;
|
||||
this.safeRemoveToken();
|
||||
this.router.navigate(['/login']);
|
||||
this.router.navigate(['/admin/login']);
|
||||
return of();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
<div class="verify-wrapper">
|
||||
<div class="verify-card">
|
||||
|
||||
<h2 class="title">🔐 OTP Verification</h2>
|
||||
@if(!isOtpSent()){
|
||||
<h2 class="title">🔐 Login</h2>
|
||||
}
|
||||
|
||||
@if(isOtpSent() && !isVerified()){
|
||||
<h2 class="title">🔐 OTP Verification</h2>
|
||||
}
|
||||
|
||||
<!-- Step 1: Enter Email -->
|
||||
@if (!isOtpSent()) {
|
||||
@ -35,17 +41,5 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
<!-- @if (isVerified()) {
|
||||
<div>
|
||||
<p class="success-msg">{{ message() }}✅</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isError()) {
|
||||
<p class="error-message">{{ message() }}</p>
|
||||
} -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -9,93 +9,94 @@ import {
|
||||
import { BehaviorSubject, catchError, filter, Observable, switchMap, take, throwError } from 'rxjs';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
|
||||
let refreshInProgress = false;
|
||||
const refreshSubject = new BehaviorSubject<string | null>(null);
|
||||
|
||||
export const AuthInterceptor: HttpInterceptorFn = (
|
||||
req: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn
|
||||
): Observable<HttpEvent<unknown>> => {
|
||||
|
||||
let refreshInProgress = false;
|
||||
const refreshSubject = new BehaviorSubject<string | null>(null);
|
||||
const authSvc: AuthService = inject(AuthService);
|
||||
let headers = req.headers;
|
||||
// add API key
|
||||
const apiKey = authSvc.getApiKey();
|
||||
if (apiKey) {
|
||||
headers = headers.set('XApiKey', apiKey);
|
||||
}
|
||||
// add API key
|
||||
const apiKey = authSvc.getApiKey();
|
||||
if (apiKey) {
|
||||
headers = headers.set('XApiKey', apiKey);
|
||||
}
|
||||
|
||||
// add content type if needed
|
||||
if (!(req.body instanceof FormData) &&
|
||||
(req.method === 'POST' || req.method === 'PUT') &&
|
||||
!headers.has('Content-Type')) {
|
||||
// add content type if needed
|
||||
if (!(req.body instanceof FormData) &&
|
||||
(req.method === 'POST' || req.method === 'PUT') &&
|
||||
!headers.has('Content-Type')) {
|
||||
|
||||
headers = headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
headers = headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// add access token
|
||||
const token = authSvc.currentToken;
|
||||
if (token) {
|
||||
headers = headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
// add access token
|
||||
const token = authSvc.currentToken;
|
||||
if (token) {
|
||||
headers = headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
// final cloned request
|
||||
const clonedRequest = req.clone({
|
||||
headers,
|
||||
withCredentials: true
|
||||
});
|
||||
// final cloned request
|
||||
const clonedRequest = req.clone({
|
||||
headers,
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
return next(clonedRequest).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
return next(clonedRequest).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
|
||||
if (err.status !== 401) {
|
||||
return throwError(() => err);
|
||||
}
|
||||
if (err.status !== 401) {
|
||||
return throwError(() => err);
|
||||
}
|
||||
|
||||
// if refresh is already in progress
|
||||
if (refreshInProgress) {
|
||||
return refreshSubject.pipe(
|
||||
filter(t => t !== null),
|
||||
take(1),
|
||||
switchMap(newToken => {
|
||||
// if refresh is already in progress
|
||||
if (refreshInProgress) {
|
||||
return refreshSubject.pipe(
|
||||
filter(t => t !== null),
|
||||
take(1),
|
||||
switchMap(newToken => {
|
||||
|
||||
const retryReq = clonedRequest.clone({
|
||||
headers: clonedRequest.headers.set('Authorization', `Bearer ${newToken}`),
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
return next(retryReq);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// start refresh
|
||||
refreshInProgress = true;
|
||||
refreshSubject.next(null);
|
||||
|
||||
return authSvc.refreshToken()!.pipe(
|
||||
switchMap(res => {
|
||||
|
||||
refreshInProgress = false;
|
||||
const newToken = res.accessToken;
|
||||
authSvc.safeSetToken(newToken);
|
||||
|
||||
refreshSubject.next(newToken);
|
||||
const retryReq = clonedRequest.clone({
|
||||
headers: clonedRequest.headers.set('Authorization', `Bearer ${newToken}`),
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
return next(retryReq);
|
||||
}),
|
||||
|
||||
catchError(refreshErr => {
|
||||
refreshInProgress = false;
|
||||
refreshSubject.next(null);
|
||||
|
||||
authSvc.logout();
|
||||
return throwError(() => refreshErr);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// start refresh
|
||||
refreshInProgress = true;
|
||||
refreshSubject.next(null);
|
||||
|
||||
return authSvc.refreshToken()!.pipe(
|
||||
switchMap(res => {
|
||||
|
||||
refreshInProgress = false;
|
||||
const newToken = res.accessToken;
|
||||
authSvc.safeSetToken(newToken);
|
||||
|
||||
refreshSubject.next(newToken);
|
||||
const retryReq = clonedRequest.clone({
|
||||
headers: clonedRequest.headers.set('Authorization', `Bearer ${newToken}`),
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
return next(retryReq);
|
||||
}),
|
||||
|
||||
catchError(refreshErr => {
|
||||
refreshInProgress = false;
|
||||
refreshSubject.next(null);
|
||||
|
||||
authSvc.logout();
|
||||
return throwError(() => refreshErr);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,10 @@
|
||||
@if(!loggedIn()){
|
||||
<router-outlet></router-outlet>
|
||||
} @else {
|
||||
<main>
|
||||
@if(loggedIn()){
|
||||
<app-contact></app-contact>
|
||||
}
|
||||
|
||||
<app-contact></app-contact>
|
||||
<div class="main-content">
|
||||
|
||||
@if(loggedIn()){
|
||||
<nav class="navbar">
|
||||
<ul class="navbar-list">
|
||||
<li class="navbar-item">
|
||||
@ -22,11 +21,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
<div class="page-container">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</main>
|
||||
}
|
||||
@ -17,6 +17,5 @@ export class AdminLayout {
|
||||
this.authSvc.accessTokenSub.subscribe(token => {
|
||||
this.loggedIn.set(token !== null);
|
||||
});
|
||||
console.log("AdminLayout constructed");
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,7 +321,7 @@
|
||||
transition: var(--transition-2);
|
||||
}
|
||||
|
||||
.sidebar.active { max-height: 405px; }
|
||||
.sidebar.active { max-height: 505px; }
|
||||
|
||||
.sidebar-info {
|
||||
position: relative;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user