From 235a70160b24c216916c5c666f5f5d7cd5acdbd1 Mon Sep 17 00:00:00 2001 From: Matteo Gheza Date: Mon, 23 Oct 2023 08:08:10 +0200 Subject: [PATCH] Add support for etag and not modified resources --- .../app/Http/Controllers/AlertController.php | 29 +++++++--- backend/composer.json | 1 + backend/composer.lock | 57 ++++++++++++++++++- backend/routes/api.php | 13 +++-- frontend/angular.json | 3 +- .../modal-alert/modal-alert.component.ts | 6 +- .../app/_components/table/table.component.ts | 5 +- .../src/app/_routes/list/list.component.ts | 6 +- .../src/app/_services/api-client.service.ts | 17 +++++- frontend/src/app/app.component.ts | 5 +- 10 files changed, 120 insertions(+), 22 deletions(-) diff --git a/backend/app/Http/Controllers/AlertController.php b/backend/app/Http/Controllers/AlertController.php index c98ae27..494cbb9 100644 --- a/backend/app/Http/Controllers/AlertController.php +++ b/backend/app/Http/Controllers/AlertController.php @@ -16,10 +16,20 @@ class AlertController extends Controller public function index() { return response()->json( - Alert::with('crew.user') - ->where('closed', false) - ->orderBy('created_at', 'desc') - ->get() + request()->query('full', false) ? + Alert::with(['crew.user' => [ + "name", + "username", + "chief", + "driver" + ]]) + ->where('closed', false) + ->orderBy('created_at', 'desc') + ->get() + : + Alert::where('closed', false) + ->orderBy('created_at', 'desc') + ->get() ); } @@ -113,10 +123,15 @@ class AlertController extends Controller */ public function show(Request $request, $id) { - User::where('id', $request->user()->id)->update(['last_access' => now()]); - return response()->json( - Alert::with('crew.user')->find($id) + Alert::with(['crew.user' => [ + "name", + "username", + "chief", + "driver" + ]]) + ->where('id', $id) + ->first() ); } diff --git a/backend/composer.json b/backend/composer.json index 4981c80..596a644 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -12,6 +12,7 @@ "laravel/framework": "^10.0", "laravel/sanctum": "^3.2", "laravel/tinker": "^2.8", + "matthewbdaly/laravel-etag-middleware": "^1.3", "santigarcor/laratrust": "^7.2", "sentry/sentry-laravel": "^3.7" }, diff --git a/backend/composer.lock b/backend/composer.lock index 80faef4..3be0fb1 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f099d4ac47fd2d3bbc07fe77412bb9bd", + "content-hash": "f8b683b55dd63e861103c15d13fed172", "packages": [ { "name": "brick/math", @@ -2202,6 +2202,61 @@ ], "time": "2023-08-05T12:09:49+00:00" }, + { + "name": "matthewbdaly/laravel-etag-middleware", + "version": "1.3.6", + "source": { + "type": "git", + "url": "https://github.com/matthewbdaly/laravel-etag-middleware.git", + "reference": "048cb22f5849f695f134e7e13cb52b4c21ef6fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matthewbdaly/laravel-etag-middleware/zipball/048cb22f5849f695f134e7e13cb52b4c21ef6fd4", + "reference": "048cb22f5849f695f134e7e13cb52b4c21ef6fd4", + "shasum": "" + }, + "require": { + "illuminate/http": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/support": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^4.8|^5.2|^9.0", + "squizlabs/php_codesniffer": "^3.7", + "vimeo/psalm": "^5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matthewbdaly\\ETagMiddleware\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Daly", + "email": "450801+matthewbdaly@users.noreply.github.com" + } + ], + "description": "A Laravel middleware for adding ETags to HTTP requests to improve response times", + "keywords": [ + "ETags", + "Etag", + "http", + "laravel", + "middleware" + ], + "support": { + "issues": "https://github.com/matthewbdaly/laravel-etag-middleware/issues", + "source": "https://github.com/matthewbdaly/laravel-etag-middleware/tree/1.3.6" + }, + "time": "2023-03-20T16:12:09+00:00" + }, { "name": "monolog/monolog", "version": "3.4.0", diff --git a/backend/routes/api.php b/backend/routes/api.php index 89593a6..c89582b 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -14,6 +14,7 @@ use App\Http\Controllers\ServiceTypeController; use App\Http\Controllers\TrainingController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Artisan; +use \Matthewbdaly\ETagMiddleware\ETag; /* |-------------------------------------------------------------------------- @@ -44,16 +45,16 @@ Route::middleware('auth:sanctum')->group( function () { Route::get('/schedules', [ScheduleSlotsController::class, 'index']); Route::post('/schedules', [ScheduleSlotsController::class, 'store']); - Route::get('/availability', [AvailabilityController::class, 'get']); + Route::get('/availability', [AvailabilityController::class, 'get'])->middleware(ETag::class); Route::post('/availability', [AvailabilityController::class, 'updateAvailability']); Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']); - Route::get('/alerts', [AlertController::class, 'index']); + Route::get('/alerts', [AlertController::class, 'index'])->middleware(ETag::class); Route::post('/alerts', [AlertController::class, 'store']); - Route::get('/alerts/{id}', [AlertController::class, 'show']); + Route::get('/alerts/{id}', [AlertController::class, 'show'])->middleware(ETag::class); Route::patch('/alerts/{id}', [AlertController::class, 'update']); - Route::get('/services', [ServiceController::class, 'index']); + Route::get('/services', [ServiceController::class, 'index'])->middleware(ETag::class); Route::post('/services', [ServiceController::class, 'createOrUpdate']); Route::get('/services/{id}', [ServiceController::class, 'show']); Route::delete('/services/{id}', [ServiceController::class, 'destroy']); @@ -63,12 +64,12 @@ Route::middleware('auth:sanctum')->group( function () { Route::get('/places/search', [PlacesController::class, 'search']); Route::get('/places/{id}', [PlacesController::class, 'show']); - Route::get('/trainings', [TrainingController::class, 'index']); + Route::get('/trainings', [TrainingController::class, 'index'])->middleware(ETag::class); Route::post('/trainings', [TrainingController::class, 'createOrUpdate']); Route::get('/trainings/{id}', [TrainingController::class, 'show']); Route::delete('/trainings/{id}', [TrainingController::class, 'destroy']); - Route::get('/logs', [LogsController::class, 'index']); + Route::get('/logs', [LogsController::class, 'index'])->middleware(ETag::class); Route::post('/telegram_login_token', [TelegramController::class, 'loginToken']); diff --git a/frontend/angular.json b/frontend/angular.json index 00fc8bd..084dc4a 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -59,7 +59,8 @@ "scripts": [], "serviceWorker": true, "ngswConfigPath": "ngsw-config.json", - "sourceMap": true + "sourceMap": true, + "allowedCommonJsDependencies": ["crypto-js"] }, "configurations": { "production": { diff --git a/frontend/src/app/_components/modal-alert/modal-alert.component.ts b/frontend/src/app/_components/modal-alert/modal-alert.component.ts index 11f6d9f..3944e43 100644 --- a/frontend/src/app/_components/modal-alert/modal-alert.component.ts +++ b/frontend/src/app/_components/modal-alert/modal-alert.component.ts @@ -26,6 +26,8 @@ export class ModalAlertComponent implements OnInit, OnDestroy { alertClosed = 0; + private etag = ""; + constructor( public bsModalRef: BsModalRef, private api: ApiClientService, @@ -36,7 +38,9 @@ export class ModalAlertComponent implements OnInit, OnDestroy { loadResponsesData() { //TODO: do not update data if not changed. Support for content hash in response header? - this.api.get(`alerts/${this.id}`).then((response) => { + this.api.get(`alerts/${this.id}`, {}, this.etag).then((response) => { + if(this.api.isLastSame) return; + this.etag = this.api.lastEtag; console.log(response, this.alertClosed, response.closed); if(this.alertClosed !== response.closed) this.alertClosed = response.closed; if(!isEqual(this.crewUsers, response.crew)) this.crewUsers = response.crew; diff --git a/frontend/src/app/_components/table/table.component.ts b/frontend/src/app/_components/table/table.component.ts index c46a4ef..aa3d030 100644 --- a/frontend/src/app/_components/table/table.component.ts +++ b/frontend/src/app/_components/table/table.component.ts @@ -60,6 +60,7 @@ export class TableComponent implements OnInit, OnDestroy { public data: any = []; public displayedData: any = []; public originalData: any = []; + private etag: string = ""; public loadDataInterval: NodeJS.Timer | undefined = undefined; @@ -87,7 +88,9 @@ export class TableComponent implements OnInit, OnDestroy { loadTableData() { if(!this.sourceType) this.sourceType = "list"; - this.api.get(this.sourceType).then((data: any) => { + this.api.get(this.sourceType, {}, this.etag).then((data: any) => { + if(this.api.isLastSame) return; + this.etag = this.api.lastEtag; this.data = data.filter((row: any) => typeof row.hidden !== 'undefined' ? !row.hidden : true); this.originalData = this.data; this.totalElements = this.data.length; diff --git a/frontend/src/app/_routes/list/list.component.ts b/frontend/src/app/_routes/list/list.component.ts index 0916cd0..77c705a 100644 --- a/frontend/src/app/_routes/list/list.component.ts +++ b/frontend/src/app/_routes/list/list.component.ts @@ -25,6 +25,8 @@ export class ListComponent implements OnInit, OnDestroy { public alertLoading = false; + private etag = ""; + constructor( public api: ApiClientService, public auth: AuthService, @@ -35,7 +37,9 @@ export class ListComponent implements OnInit, OnDestroy { } loadAvailability() { - this.api.get("availability").then((response) => { + this.api.get("availability", {}, this.etag).then((response) => { + if(this.api.isLastSame) return; + this.etag = this.api.lastEtag; this.available = response.available; this.manual_mode = response.manual_mode; }).catch((err) => { diff --git a/frontend/src/app/_services/api-client.service.ts b/frontend/src/app/_services/api-client.service.ts index 4d579d0..20e723f 100644 --- a/frontend/src/app/_services/api-client.service.ts +++ b/frontend/src/app/_services/api-client.service.ts @@ -8,6 +8,9 @@ import { Subject } from "rxjs"; export class ApiClientService { private apiRoot = 'api/'; + public lastEtag = ""; + public isLastSame = false; + public alertsChanged = new Subject(); public availableUsers: undefined | number = undefined; @@ -20,12 +23,20 @@ export class ApiClientService { return this.apiRoot + endpoint; } - public get(endpoint: string, data: any = {}) { + public get(endpoint: string, data: any = {}, etag: string = "") { return new Promise((resolve, reject) => { this.http.get(this.apiEndpoint(endpoint), { - params: new HttpParams({ fromObject: data }) + params: new HttpParams({ fromObject: data }), + observe: 'response', + headers: (etag !== "" && etag !== null) ? { + 'If-None-Match': etag + } : {} }).subscribe({ - next: (v) => resolve(v), + next: (v: any) => { + this.lastEtag = v.headers.get("etag"); + this.isLastSame = etag === this.lastEtag; + resolve(v.body); + }, error: (e) => reject(e) }); }); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9281804..7b3824b 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -21,6 +21,7 @@ export class AppComponent { public loadingRoute = false; private loadAlertsInterval: NodeJS.Timer | undefined = undefined; public alerts = []; + private alertsEtag = ""; constructor( public auth: AuthService, @@ -37,8 +38,10 @@ export class AppComponent { loadAlerts() { if(this.auth.profile) { - this.api.get("alerts").then((response) => { + this.api.get("alerts", {}, this.alertsEtag).then((response) => { + if(this.api.isLastSame) return; this.alerts = response; + this.alertsEtag = this.api.lastEtag; }); } }