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">
|
<ul class="social-list">
|
||||||
|
|
||||||
@if (model.socialLinks?.linkedin) {
|
@if (model.socialLinks?.linkedin) {
|
||||||
<li class="social-item">
|
<li class="social-item">
|
||||||
<a href="{{model.socialLinks?.linkedin}}" target="_blank" class="social-link">
|
<a href="{{model.socialLinks?.linkedin}}" target="_blank" class="social-link">
|
||||||
<i class="fa-brands fa-linkedin"></i>
|
<i class="fa-brands fa-linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (model.socialLinks?.gitHub) {
|
@if (model.socialLinks?.gitHub) {
|
||||||
<li class="social-item">
|
<li class="social-item">
|
||||||
<a href="{{model.socialLinks?.gitHub}}" target="_blank" class="social-link">
|
<a href="{{model.socialLinks?.gitHub}}" target="_blank" class="social-link">
|
||||||
<i class="fa-brands fa-github"></i>
|
<i class="fa-brands fa-github"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (model.socialLinks?.blogUrl) {
|
@if (model.socialLinks?.blogUrl) {
|
||||||
<li class="social-item">
|
<li class="social-item">
|
||||||
<a href="{{model.socialLinks?.blogUrl}}" target="_blank" class="social-link">
|
<a href="{{model.socialLinks?.blogUrl}}" target="_blank" class="social-link">
|
||||||
<i class="fa-duotone fa-blog"></i>
|
<i class="fa-duotone fa-blog"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
@ -1,3 +1,62 @@
|
|||||||
img {
|
img {
|
||||||
border-radius: inherit;
|
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 { AdminService } from '../services/admin.service';
|
||||||
import { BaseComponent } from '../base.component';
|
import { BaseComponent } from '../base.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../../auth/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-contact',
|
selector: 'app-contact',
|
||||||
@ -13,6 +14,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
export class Contact extends BaseComponent<IContactModel> implements OnInit {
|
export class Contact extends BaseComponent<IContactModel> implements OnInit {
|
||||||
sideBarExpanded = false;
|
sideBarExpanded = false;
|
||||||
displayName!: string;
|
displayName!: string;
|
||||||
|
authSvc: AuthService = inject(AuthService);
|
||||||
constructor(){
|
constructor(){
|
||||||
const svc = inject(AdminService);
|
const svc = inject(AdminService);
|
||||||
super(svc);
|
super(svc);
|
||||||
@ -28,4 +30,8 @@ export class Contact extends BaseComponent<IContactModel> implements OnInit {
|
|||||||
this.assignData(response);
|
this.assignData(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.authSvc.logout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,9 +56,9 @@
|
|||||||
<a>
|
<a>
|
||||||
|
|
||||||
<figure class="project-img">
|
<figure class="project-img">
|
||||||
<!-- <div class="project-item-icon-box">
|
<div class="project-item-icon-box">
|
||||||
<ion-icon name="eye-outline"></ion-icon>
|
<ion-icon name="eye-outline"></ion-icon>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<img src="{{imagesOrigin + project.imagePath}}" alt="{{project.name}}" loading="lazy">
|
<img src="{{imagesOrigin + project.imagePath}}" alt="{{project.name}}" loading="lazy">
|
||||||
</figure>
|
</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 { IProject } from '../models/project.model';
|
||||||
import { BaseComponent } from '../base.component';
|
import { BaseComponent } from '../base.component';
|
||||||
import { AdminService } from '../services/admin.service';
|
import { AdminService } from '../services/admin.service';
|
||||||
@ -10,7 +10,8 @@ import { CommonModule } from '@angular/common';
|
|||||||
selector: 'app-projects',
|
selector: 'app-projects',
|
||||||
templateUrl: './projects.html',
|
templateUrl: './projects.html',
|
||||||
styleUrl: './projects.scss',
|
styleUrl: './projects.scss',
|
||||||
imports: [CommonModule]
|
imports: [CommonModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class Projects extends BaseComponent<IProjects> implements OnInit {
|
export class Projects extends BaseComponent<IProjects> implements OnInit {
|
||||||
filter = 'All';
|
filter = 'All';
|
||||||
|
|||||||
@ -4,13 +4,12 @@ import { routes } from './app.routes';
|
|||||||
import { provideHttpClient, withFetch, withInterceptors} from '@angular/common/http';
|
import { provideHttpClient, withFetch, withInterceptors} from '@angular/common/http';
|
||||||
import { loadingInterceptor } from './interceptors/loading-interceptor';
|
import { loadingInterceptor } from './interceptors/loading-interceptor';
|
||||||
import { AuthInterceptor } from './interceptors/auth-interceptor';
|
import { AuthInterceptor } from './interceptors/auth-interceptor';
|
||||||
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideHttpClient(withInterceptors([loadingInterceptor, AuthInterceptor]), withFetch()),
|
provideHttpClient(withInterceptors([loadingInterceptor, AuthInterceptor]), withFetch()),
|
||||||
provideRouter(routes), provideClientHydration(withEventReplay())
|
provideRouter(routes)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,11 @@ export const routes: Routes = [
|
|||||||
redirectTo: 'admin/about',
|
redirectTo: 'admin/about',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'admin',
|
||||||
|
redirectTo: 'admin/about',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
component: AdminLayout,
|
component: AdminLayout,
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export class AuthService {
|
|||||||
router = inject(Router);
|
router = inject(Router);
|
||||||
|
|
||||||
private readonly storageKey = 'accessToken';
|
private readonly storageKey = 'accessToken';
|
||||||
|
tokenReady$ = new BehaviorSubject<boolean | null>(null);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log('🔥 AuthService constructor started');
|
console.log('🔥 AuthService constructor started');
|
||||||
@ -36,31 +37,31 @@ export class AuthService {
|
|||||||
return `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
return `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Call on app start, or from guard
|
// Call on app start, or from guard
|
||||||
// async ensureTokenReady(): Promise<void>{
|
async ensureTokenReady(): Promise<void>{
|
||||||
// if(this.tokenReady$.value) return;
|
if(this.tokenReady$.value) return;
|
||||||
|
|
||||||
// const stored = this.safeGetToken();
|
const stored = this.safeGetToken();
|
||||||
|
|
||||||
// // try to restore from storage
|
// try to restore from storage
|
||||||
// if(stored){
|
if(stored){
|
||||||
// this.accessTokenSub.next(stored);
|
this.accessTokenSub.next(stored);
|
||||||
// this.tokenReady$.next(true);
|
this.tokenReady$.next(true);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
|
// // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
|
||||||
// // try {
|
// try {
|
||||||
// // const res = await firstValueFrom(this.refreshToken());
|
// const res = await firstValueFrom(this.refreshToken());
|
||||||
// // this.safeSetToken(res.accessToken);
|
// this.safeSetToken(res.accessToken);
|
||||||
// // }
|
// }
|
||||||
// // catch{
|
// catch{
|
||||||
// // console.warn('Silent token refresh failed on startup');
|
// console.warn('Silent token refresh failed on startup');
|
||||||
// // }
|
// }
|
||||||
// // finally{
|
// finally{
|
||||||
// // this.tokenReady$.next(true);
|
// this.tokenReady$.next(true);
|
||||||
// // }
|
// }
|
||||||
// }
|
}
|
||||||
|
|
||||||
get currentToken(): string | null {
|
get currentToken(): string | null {
|
||||||
return this.safeGetToken();
|
return this.safeGetToken();
|
||||||
@ -123,7 +124,7 @@ export class AuthService {
|
|||||||
logout(): Observable<void> {
|
logout(): Observable<void> {
|
||||||
this.accessToken = null;
|
this.accessToken = null;
|
||||||
this.safeRemoveToken();
|
this.safeRemoveToken();
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/admin/login']);
|
||||||
return of();
|
return of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
<div class="verify-wrapper">
|
<div class="verify-wrapper">
|
||||||
<div class="verify-card">
|
<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 -->
|
<!-- Step 1: Enter Email -->
|
||||||
@if (!isOtpSent()) {
|
@if (!isOtpSent()) {
|
||||||
@ -35,17 +41,5 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Step 3: Success -->
|
|
||||||
<!-- @if (isVerified()) {
|
|
||||||
<div>
|
|
||||||
<p class="success-msg">{{ message() }}✅</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (isError()) {
|
|
||||||
<p class="error-message">{{ message() }}</p>
|
|
||||||
} -->
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -9,93 +9,94 @@ import {
|
|||||||
import { BehaviorSubject, catchError, filter, Observable, switchMap, take, throwError } from 'rxjs';
|
import { BehaviorSubject, catchError, filter, Observable, switchMap, take, throwError } from 'rxjs';
|
||||||
import { AuthService } from '../auth/auth.service';
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
|
||||||
|
let refreshInProgress = false;
|
||||||
|
const refreshSubject = new BehaviorSubject<string | null>(null);
|
||||||
|
|
||||||
export const AuthInterceptor: HttpInterceptorFn = (
|
export const AuthInterceptor: HttpInterceptorFn = (
|
||||||
req: HttpRequest<unknown>,
|
req: HttpRequest<unknown>,
|
||||||
next: HttpHandlerFn
|
next: HttpHandlerFn
|
||||||
): Observable<HttpEvent<unknown>> => {
|
): Observable<HttpEvent<unknown>> => {
|
||||||
|
|
||||||
let refreshInProgress = false;
|
|
||||||
const refreshSubject = new BehaviorSubject<string | null>(null);
|
|
||||||
const authSvc: AuthService = inject(AuthService);
|
const authSvc: AuthService = inject(AuthService);
|
||||||
let headers = req.headers;
|
let headers = req.headers;
|
||||||
// add API key
|
// add API key
|
||||||
const apiKey = authSvc.getApiKey();
|
const apiKey = authSvc.getApiKey();
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
headers = headers.set('XApiKey', apiKey);
|
headers = headers.set('XApiKey', apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add content type if needed
|
// add content type if needed
|
||||||
if (!(req.body instanceof FormData) &&
|
if (!(req.body instanceof FormData) &&
|
||||||
(req.method === 'POST' || req.method === 'PUT') &&
|
(req.method === 'POST' || req.method === 'PUT') &&
|
||||||
!headers.has('Content-Type')) {
|
!headers.has('Content-Type')) {
|
||||||
|
|
||||||
headers = headers.set('Content-Type', 'application/json');
|
headers = headers.set('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
// add access token
|
// add access token
|
||||||
const token = authSvc.currentToken;
|
const token = authSvc.currentToken;
|
||||||
if (token) {
|
if (token) {
|
||||||
headers = headers.set('Authorization', `Bearer ${token}`);
|
headers = headers.set('Authorization', `Bearer ${token}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// final cloned request
|
// final cloned request
|
||||||
const clonedRequest = req.clone({
|
const clonedRequest = req.clone({
|
||||||
headers,
|
headers,
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return next(clonedRequest).pipe(
|
return next(clonedRequest).pipe(
|
||||||
catchError((err: HttpErrorResponse) => {
|
catchError((err: HttpErrorResponse) => {
|
||||||
|
|
||||||
if (err.status !== 401) {
|
if (err.status !== 401) {
|
||||||
return throwError(() => err);
|
return throwError(() => err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if refresh is already in progress
|
// if refresh is already in progress
|
||||||
if (refreshInProgress) {
|
if (refreshInProgress) {
|
||||||
return refreshSubject.pipe(
|
return refreshSubject.pipe(
|
||||||
filter(t => t !== null),
|
filter(t => t !== null),
|
||||||
take(1),
|
take(1),
|
||||||
switchMap(newToken => {
|
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({
|
const retryReq = clonedRequest.clone({
|
||||||
headers: clonedRequest.headers.set('Authorization', `Bearer ${newToken}`),
|
headers: clonedRequest.headers.set('Authorization', `Bearer ${newToken}`),
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return next(retryReq);
|
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>
|
<main>
|
||||||
@if(loggedIn()){
|
<app-contact></app-contact>
|
||||||
<app-contact></app-contact>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
|
|
||||||
@if(loggedIn()){
|
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<ul class="navbar-list">
|
<ul class="navbar-list">
|
||||||
<li class="navbar-item">
|
<li class="navbar-item">
|
||||||
@ -22,11 +21,12 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
</main>
|
||||||
|
}
|
||||||
@ -17,6 +17,5 @@ export class AdminLayout {
|
|||||||
this.authSvc.accessTokenSub.subscribe(token => {
|
this.authSvc.accessTokenSub.subscribe(token => {
|
||||||
this.loggedIn.set(token !== null);
|
this.loggedIn.set(token !== null);
|
||||||
});
|
});
|
||||||
console.log("AdminLayout constructed");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -321,7 +321,7 @@
|
|||||||
transition: var(--transition-2);
|
transition: var(--transition-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.active { max-height: 405px; }
|
.sidebar.active { max-height: 505px; }
|
||||||
|
|
||||||
.sidebar-info {
|
.sidebar-info {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user