Rebuild on Angular 22 + ASP.NET Core 10, containerised
The 2018 stack (Angular 5.2 / CLI 1.7, PHP, MySQL) had not been touched since
July 2018. Rebuilt rather than upgraded in place: the frontend was 17 major
versions behind, and of ~16,700 lines of PHP only ~150 were application logic —
the rest was four near-identical vendored copies of php-crud-api plus
class.upload.php.
Backend — ASP.NET Core 10, EF Core, SQLite
* ASP.NET Core Identity (PBKDF2) + JWT bearer auth
* Clean REST API replacing php-crud-api's filter[]/transform query syntax
* Box art uploads re-encoded to WebP via SkiaSharp
* Imports the 105 games recovered from the 2018 dump on first run
Frontend — Angular 22, zoneless, signals, Material 22
* Standalone components, lazy routes, functional guards and interceptor
* Vitest replaces Karma/Jasmine; fonts and icons bundled, no CDN calls
* No provideAnimations: @angular/animations is deprecated in v22 and
Material no longer depends on it (pinned by a test)
Docker
* Multi-stage builds for both services, non-root at runtime
* nginx serves the SPA and reverse-proxies the API, so everything is
same-origin; one volume holds the database, uploads and DP keys
Security issues in the old code, not carried across:
* Two endpoints exposed unauthenticated CRUD over every table
* The client chose whose rows to read (filter[]=userId,eq,N); ownership now
comes from the JWT subject server-side
* Login was hardcoded to a single username
* crypt() with one global salt, silently truncating passwords to 8 chars
* JWT secret was the literal string "testing", tokens never expired
* Token travelled in the query string rather than a header
* Uploads were anonymous with the path built from the client filename
* Access-Control-Allow-Origin: *
The live MySQL password committed in 2018 remains in git history and must be
rotated independently of this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../core/auth.service';
|
||||
|
||||
/** App bar shared by the library and editor screens. */
|
||||
@Component({
|
||||
selector: 'app-toolbar',
|
||||
imports: [MatToolbarModule, MatButtonModule, MatIconModule, MatMenuModule, RouterLink],
|
||||
template: `
|
||||
<mat-toolbar color="primary" class="toolbar">
|
||||
<a class="brand" routerLink="/games">
|
||||
<mat-icon>videogame_asset</mat-icon>
|
||||
<span class="brand-text">LudosData</span>
|
||||
</a>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
<ng-content />
|
||||
|
||||
<a mat-button routerLink="/games/new">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span class="label-md">New game</span>
|
||||
</a>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu" aria-label="Account menu">
|
||||
<mat-icon>account_circle</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
@if (user(); as currentUser) {
|
||||
<div class="menu-header">
|
||||
<strong>{{ currentUser.userName }}</strong>
|
||||
@if (currentUser.email) {
|
||||
<small>{{ currentUser.email }}</small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<a mat-menu-item routerLink="/account">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>Account</span>
|
||||
</a>
|
||||
<button mat-menu-item (click)="logout()">
|
||||
<mat-icon>logout</mat-icon>
|
||||
<span>Sign out</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</mat-toolbar>
|
||||
`,
|
||||
styles: `
|
||||
.toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.menu-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.menu-header small {
|
||||
opacity: 0.7;
|
||||
}
|
||||
/* Keep the bar usable on a phone: icons stay, text labels drop out. */
|
||||
@media (max-width: 599px) {
|
||||
.brand-text,
|
||||
.label-md {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Toolbar {
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly user = this.auth.user;
|
||||
|
||||
logout(): void {
|
||||
this.auth.logout();
|
||||
void this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user