Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Normalize line endings to LF for all text files
* text=auto eol=lf

# Explicitly declare text files (ensures LF in the repo)
*.ts text eol=lf
*.html text eol=lf
*.scss text eol=lf
*.css text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.xml text eol=lf
*.java text eol=lf
*.sql text eol=lf
*.sh text eol=lf
*.env text eol=lf
*.txt text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.js text eol=lf

# Binary files — do not touch line endings
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.eot binary
*.jar binary
*.zip binary
1 change: 1 addition & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ jwt.refreshSecret=QkZJbG9DWE9hU3h1YzBzYkF6V1pUM1pXb0Q=
refresh.cleanup.days=3

app.rate-limit-per-minute=100

23 changes: 16 additions & 7 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import {
ActivatedRoute,
NavigationEnd,
Router,
RouterOutlet,
} from '@angular/router';
import { filter } from 'rxjs/internal/operators/filter';
import { NavbarComponent } from './navbar/navbar.component';
import { ThemeService } from './services/theme.service';
Expand All @@ -17,16 +22,20 @@ export class AppComponent {

constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
public themeService: ThemeService,
) {
this.showNavbar = !['/login', '/register'].includes(this.router.url);
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
// show navbar if not in Login/Register
this.showNavbar = !['/login', '/register'].includes(
event.urlAfterRedirects,
);
.subscribe(() => {
// Derive navbar visibility from the active route's data, not the raw URL.
// This correctly handles the wildcard (**) route where the URL is the
// unknown path (e.g. /blah) rather than /not-found.
let route = this.activatedRoute;
while (route.firstChild) {
route = route.firstChild;
}
this.showNavbar = !route.snapshot.data['hideNavbar'];
});
}

Expand Down
22 changes: 19 additions & 3 deletions frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { CloudComponent } from './pages/cloud/cloud.component';
import { TrashComponent } from './pages/cloud/trash/trash.component';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { PasswordManagerComponent } from './pages/password-manager/password-manager.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
import { ServerErrorComponent } from './pages/server-error/server-error.component';

export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'login', component: LoginComponent, data: { hideNavbar: true } },
{ path: '', component: HomeComponent, canActivate: [authGuard] },
{
path: 'dashboard',
Expand All @@ -22,8 +24,22 @@ export const routes: Routes = [
canActivate: [authGuard],
},
{ path: 'password-manager', redirectTo: 'passwords', pathMatch: 'full' },
{ path: 'register', component: RegisterComponent },
{
path: 'register',
component: RegisterComponent,
data: { hideNavbar: true },
},
{ path: 'cloud', component: CloudComponent, canActivate: [authGuard] },
{ path: 'cloud/trash', component: TrashComponent, canActivate: [authGuard] },
{ path: '**', redirectTo: 'login' },
{
path: 'not-found',
component: NotFoundComponent,
data: { hideNavbar: true },
},
{
path: 'error',
component: ServerErrorComponent,
data: { hideNavbar: true },
},
{ path: '**', component: NotFoundComponent, data: { hideNavbar: true } },
];
22 changes: 22 additions & 0 deletions frontend/src/app/core/interceptors/token.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError, ReplaySubject } from 'rxjs';
import { Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { NetworkStatusService } from '../services/network-status.service';

Expand All @@ -15,6 +16,7 @@ let isRefreshing = false;
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const networkStatusService = inject(NetworkStatusService);
const router = inject(Router);

// Explicit Authorization Header
if (req.headers.has('Authorization')) {
Expand All @@ -28,6 +30,9 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
if (error.status === 401) {
return handleUnauthorized(req, next, authService, error);
}
if (isServerError(error) && !isExcludedFromErrorPage(req.url)) {
void router.navigate(['/error']);
}
return throwError(() => error);
}),
);
Expand Down Expand Up @@ -55,6 +60,9 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
}

if (error.status !== 401) {
if (isServerError(error) && !isExcludedFromErrorPage(req.url)) {
void router.navigate(['/error']);
}
return throwError(() => error);
}

Expand Down Expand Up @@ -114,3 +122,17 @@ function handleUnauthorized(
function isBackendUnavailable(error: HttpErrorResponse): boolean {
return error.status === 0;
}

function isServerError(error: HttpErrorResponse): boolean {
return error.status >= 500;
}

// Exclude specific background endpoints (like refresh and devices) to avoid redirect loops
// or interrupting the user. We allow login/register 500s to redirect to the error page.
function isExcludedFromErrorPage(url: string): boolean {
return (
url.includes('/auth/refresh') ||
url.includes('/auth/logout') ||
url.includes('/devices')
);
}
16 changes: 16 additions & 0 deletions frontend/src/app/pages/not-found/not-found.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="error-page flex items-center justify-center min-h-screen px-4">
<div class="error-card w-full max-w-md p-8 rounded-lg shadow text-center">
<i class="pi pi-compass error-icon mb-4" aria-hidden="true"></i>
<h1 class="error-code font-bold mb-2">404</h1>
<h2 class="error-title font-semibold mb-3">Page Not Found</h2>
<p class="error-message mb-6">
The page you are looking for does not exist or has been moved.
</p>
<a
routerLink="/"
class="error-btn inline-block text-white py-2 px-6 rounded"
>
Go Home
</a>
</div>
</div>
9 changes: 9 additions & 0 deletions frontend/src/app/pages/not-found/not-found.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
selector: 'app-not-found',
imports: [RouterLink],
templateUrl: './not-found.component.html',
})
export class NotFoundComponent {}
19 changes: 19 additions & 0 deletions frontend/src/app/pages/server-error/server-error.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="error-page flex items-center justify-center min-h-screen px-4">
<div class="error-card w-full max-w-md p-8 rounded-lg shadow text-center">
<i
class="pi pi-exclamation-triangle error-icon mb-4"
aria-hidden="true"
></i>
<h1 class="error-code font-bold mb-2">500</h1>
<h2 class="error-title font-semibold mb-3">Something Went Wrong</h2>
<p class="error-message mb-6">
An unexpected server error occurred. Please try again later.
</p>
<a
routerLink="/"
class="error-btn inline-block text-white py-2 px-6 rounded"
>
Go Home
</a>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
selector: 'app-server-error',
imports: [RouterLink],
templateUrl: './server-error.component.html',
})
export class ServerErrorComponent {}
45 changes: 45 additions & 0 deletions frontend/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,48 @@ body {
.vw-toast .p-toast-message .p-toast-detail {
opacity: 0.92;
}

/* Global Error Page Styles */
.error-page {
background-color: var(--color-background-secondary);
}

.error-card {
background-color: var(--color-surface);
box-shadow: 0 4px 6px var(--color-shadow);
}

.error-icon {
display: block;
font-size: 3.5rem;
color: var(--color-primary);
}

.error-code {
font-size: 4rem;
line-height: 1;
color: var(--color-text-primary);
}

.error-title {
font-size: 1.25rem;
color: var(--color-text-primary);
}

.error-message {
color: var(--color-text-secondary);
font-size: 0.95rem;
line-height: 1.5;
}

.error-btn {
background: var(--color-primary-strong);
border: 1px solid
color-mix(in srgb, var(--color-primary-strong) 82%, #0f172a 18%);
text-decoration: none;
transition: background-color 0.2s ease;

&:hover {
background: var(--color-primary-hover);
}
}