diff --git a/BUGS.md b/BUGS.md new file mode 100644 index 00000000..a512ea64 --- /dev/null +++ b/BUGS.md @@ -0,0 +1,420 @@ +# OVDB Bug Analysis Report + +This document contains a comprehensive analysis of bugs found throughout the application, with focus on the user interface (Angular frontend) and supporting backend services. + +--- + +## Table of Contents + +1. [Critical Frontend Bugs](#critical-frontend-bugs) +2. [UI Component Bugs](#ui-component-bugs) +3. [Service & Guard Bugs](#service--guard-bugs) +4. [Backend Bugs](#backend-bugs) +5. [Summary](#summary) + +--- + +## Critical Frontend Bugs + +### 1. Infinite Recursion in Map Component Bounds Setter + +**Files:** `map.component.ts`, `single-route-map.component.ts` + +The `bounds` setter calls itself recursively when the value is invalid: + +```typescript +set bounds(value: LatLngBounds) { + if (!!value && value.isValid()) { + this._bounds = value; + } else { + this.bounds = new LatLngBounds(...); // Calls setter recursively instead of this._bounds + } +} +``` + +**Impact:** Stack overflow if the fallback `LatLngBounds` is also invalid. +**Fix:** Change `this.bounds = ...` to `this._bounds = ...`. + +--- + +### 2. Copy-Paste Logic Error in Map Query Parameter Parsing + +**File:** `map.component.ts` (line ~246) + +```typescript +if (queryParams.has("from")) { + this.from = moment(+queryParams.get("from")); +} +if (queryParams.has("from")) { // BUG: should be "to" + this.to = moment(+queryParams.get("to")); +} +``` + +**Impact:** The `to` parameter is only parsed when `from` exists, not when `to` exists. +**Fix:** Change second condition to `queryParams.has("to")`. + +--- + +### 3. Double `+=` Syntax Error in Map Filter Construction + +**File:** `map.component.ts` (line ~430) + +```typescript +filter += filter += ... +``` + +**Impact:** Produces incorrect filter string by doubling the concatenation. +**Fix:** Remove one `+=`. + +--- + +### 4. Null Comparison Bug in Map Filter Cache Check + +**File:** `map.component.ts` (line ~569) + +```typescript +(value.to?.isSame(to) ?? (value.to == null && from == null)) +``` + +**Impact:** The fallback checks `from == null` when it should check `to == null`. The cache may return stale data. +**Fix:** Change `from == null` to `to == null`. + +--- + +### 5. Auth Interceptor Sends "Bearer null" Header + +**File:** `auth.interceptor.ts` (line ~19) + +```typescript +Authorization: `Bearer ${this.authService.token}` +``` + +**Impact:** After logout or when token is null, the header becomes `Bearer null`, causing malformed requests to the backend. +**Fix:** Only add the Authorization header when token is not null/empty. + +--- + +### 6. SignalR Service Allows Multiple Concurrent Connections + +**File:** `signal-r.service.ts` (lines 16-37) + +The `connected` flag is set only after the async `.start()` completes. Multiple rapid `connect()` calls before the first resolves create duplicate connections and accumulated event listeners. + +**Impact:** Duplicate event handling, memory leaks, multiple WebSocket connections. +**Fix:** Set a "connecting" state immediately, or use a promise-based guard. + +--- + +## UI Component Bugs + +### Login & Registration + +| Bug | File | Description | +|-----|------|-------------| +| Missing unsubscribe | `login.component.ts` | `ActivatedRoute.data` subscription never cleaned up | +| Missing unsubscribe | `login.component.ts` | `authService.login()` subscription not managed | +| Empty success handler | `registration.component.ts` | Registration success does nothing - no redirect or message | +| Missing unsubscribe | `registration.component.ts` | Registration subscription not managed | + +### Home Component + +| Bug | File | Description | +|-----|------|-------------| +| Incorrect signal handling | `home.component.html` | `@if (!!maps() \|\| !!stationMaps())` - `!!` on signals doesn't check array length | +| Missing unsubscribe | `home.component.ts` | `apiService.getMaps()` and `apiService.listStationMaps()` subscriptions unmanaged | +| Non-atomic loading counter | `home.component.ts` | Loading counter incremented/decremented without synchronization | + +### Profile Component + +| Bug | File | Description | +|-----|------|-------------| +| Memory leak via setInterval | `profile.component.ts` | Polling interval for Träwelling OAuth window not cleaned up on destroy | +| Null window.open result | `profile.component.ts` | `window.open()` can return null (popup blocked), polling continues anyway | +| Multiple missing unsubscribes | `profile.component.ts` | 10+ subscriptions without `takeUntilDestroyed()` | + +### Routes List Component + +| Bug | File | Description | +|-----|------|-------------| +| Missing null check on ViewChild | `routes-list.component.ts` | `this.input().nativeElement` accessed without null check in `ngAfterViewInit` | +| Missing null check on sort/paginator | `routes-list.component.ts` | `this.sort().sortChange` and `this.paginator().page` accessed without existence check | +| Null operatorIds in template | `routes-list.component.html` | `@for (operatorId of element.operatorIds)` - array could be null | +| Missing routeType check | `routes-list.component.html` | `name(element.routeType)` called when only `routeTypeColour` is checked | + +### Route Detail Component + +| Bug | File | Description | +|-----|------|-------------| +| Unsafe non-null assertions | `route-detail.component.ts` | Multiple `this.route()!.routeId` without prior null checks | +| Signal vs property confusion | `route-detail.component.ts` | `!this.countries` should be `!this.countries()` (signal not invoked) | +| Same issue for maps | `route-detail.component.ts` | `!this.maps` should be `!this.maps()` | + +### Route Instances List Component + +| Bug | File | Description | +|-----|------|-------------| +| Missing null check on input | `route-instances-list.component.ts` | `fromEvent(this.input().nativeElement, "keyup")` without null guard | +| Null paginator access | `route-instances-list.component.ts` | `this.paginator().pageIndex = 0` without null check | +| Return type mismatch | `route-instances-list.component.ts` | `formatDelay()` can return `null` but type says `string` | +| Unsafe date pipe | `route-instances-list.component.html` | Date pipes applied to potentially null `startTime`/`endTime` | + +### Route Instances Edit Component + +| Bug | File | Description | +|-----|------|-------------| +| Two-way binding conflict | `route-instances-edit.component.html` | `[(ngModel)]` and `(selectionChange)` both modify same signal | +| Inverted disabled logic | `route-instances-edit.component.html` | `[disabled]="canAddNewRow"` should be `[disabled]="!canAddNewRow"` | +| Missing null check | `route-instances-edit.component.ts` | `this.table()` could be null before calling `renderRows()` | +| Unsafe Moment check | `route-instances-edit.component.ts` | `this.instance.date['_isAMomentObject']` throws if date is undefined | + +### Route Add Component + +| Bug | File | Description | +|-----|------|-------------| +| Missing unsubscribe | `route-add.component.ts` | `this.route.queryParams.subscribe()` without cleanup | +| Missing error handling | `route-add.component.ts` | `apiService.postFiles()` has no error handler | +| Null FileList assignment | `route-add.component.ts` | `this.fileToUpload = null` but type is `FileList` (not nullable) | +| Null files from event | `route-add.component.html` | `$event.target.files` could be null | + +### Map View / Map Filter Components + +| Bug | File | Description | +|-----|------|-------------| +| Missing unsubscribe | `map-view.component.ts` | `activatedRoute.paramMap` subscription not cleaned up | +| Global component exposure | `map.component.ts` | `window["angularComponentRef"] = { component: this }` - security risk | +| Missing unsubscribe | `map-filter.component.ts` | `translationService.languageChanged` subscription leaks | +| Inconsistent selectedYears | `map-filter.component.ts` | Set to empty array instead of actual selected years when dates provided | + +### Stats Components + +| Bug | File | Description | +|-----|------|-------------| +| Missing unsubscribes | `time-stats.component.ts` | Multiple subscriptions without cleanup | +| Loading flag not reset on error | `time-stats.component.ts` | `loadingMap` stays true if API call fails | +| Null data properties | `time-stats.component.ts` | `data.latMin`, `data.latMax` etc. accessed without null checks | +| Missing unsubscribe | `region-stats.component.ts` | API subscription not cleaned up | + +### Single Route Map Component + +| Bug | File | Description | +|-----|------|-------------| +| Infinite recursion (same as map) | `single-route-map.component.ts` | Bounds setter calls itself | +| Unhandled promise rejection | `single-route-map.component.ts` | `toPromise()` rejection leaves component in undefined state | +| Missing null checks | `single-route-map.component.ts` | `feature.properties` and `layer.getPopup()` accessed without null guards | +| Missing unsubscribes | `single-route-map.component.ts` | Two subscriptions without cleanup | + +### Countries Component + +| Bug | File | Description | +|-----|------|-------------| +| Missing unsubscribe | `countries.component.ts` | `translationService.languageChanged` subscription leaks | +| Uninitialized array | `countries.component.ts` | `data: Country[]` used in template before initialization | +| Template null access | `countries.component.html` | `data.length` throws if `data` is undefined | + +### Wizard Components + +| Bug | File | Description | +|-----|------|-------------| +| Missing unsubscribes | `wizard-step1.component.ts` | `languageChanged` and `queryParams` subscriptions leak | +| Potential null reference | `wizard-step1.component.ts` | `trawellingTripData.tripId` accessed without null check | +| Missing unsubscribes | `wizard-step2.component.ts` | `activatedRoute.params` and `queryParamMap` subscriptions leak | +| Nested subscribes anti-pattern | `wizard-step2.component.ts` | Nested subscriptions instead of `switchMap`/`mergeMap` | + +### Traewelling Component + +| Bug | File | Description | +|-----|------|-------------| +| Async ngOnInit race condition | `traewelling.component.ts` | Component may be destroyed before promises resolve | +| Missing null check | `traewelling.component.html` | `connectionStatus?.user.displayName` - `user` itself could be null | + +### Image Creator Component + +| Bug | File | Description | +|-----|------|-------------| +| Uninitialized array | `image-creator.component.ts` | `maps: Map[]` used before API call completes | +| Missing error handling | `image-creator.component.ts` | `getMaps()` has no error handler | +| Template null crash | `image-creator.component.html` | `@for (map of maps)` throws if maps is undefined | + +### Maps List Component + +| Bug | File | Description | +|-----|------|-------------| +| Missing error handling | `maps-list.component.ts` | `getMaps()` subscription has no error handler, loading stays true | + +--- + +## Service & Guard Bugs + +### Authentication Service + +| Bug | Description | +|-----|-------------| +| Type mismatch | `token` declared as `string` but assigned `null` on logout | +| Missing null check before token decode | `helper.decodeToken(this.token)` called without verifying token exists | +| String comparison for admin | `this.admin === 'true'` compares to string, fragile if backend changes | +| Race condition in token refresh | Multiple `setTimeout` calls with stale expiry values | +| Subscription memory leak | `refreshTheToken()` subscribes without cleanup | + +### Auth Interceptor + +| Bug | Description | +|-----|-------------| +| "Bearer null" header | Token null check missing before setting Authorization header | +| No 401 response handling | Expired tokens cause 401 errors without triggering logout/refresh | + +### Login Guard + +| Bug | Description | +|-----|-------------| +| Incorrect URL construction | `'/' + next.url.join('/')` creates `//` when url is empty | +| Race condition | `isLoggedIn` could change between check and return | + +### Administrator Guard + +| Bug | Description | +|-----|-------------| +| Missing null safety | `this.authService.admin` may throw if token is null | +| No error handling | Guard crashes instead of returning false on error | + +### SignalR Service + +| Bug | Description | +|-----|-------------| +| Multiple connections | Rapid `connect()` calls create duplicates before first resolves | +| Event listener accumulation | Each `connection.on()` adds listeners without removing previous ones | +| Connection overwrite leak | Previous connection overwritten without `.stop()` | +| Subjects never completed | `updates$`, `regionUpdates$`, `stationUpdates$` never call `.complete()` | + +### Theme Service + +| Bug | Description | +|-----|-------------| +| Null pointer | `document.querySelector('meta[name="theme-color"]')` may return null, `.setAttribute()` crashes | + +### Translation Service + +| Bug | Description | +|-----|-------------| +| EventEmitter leak | `languageChanged` subscribers never cleaned up | + +--- + +## Backend Bugs + +### Pervasive Pattern: Unsafe Claim Access (All Controllers) + +Nearly every controller has this pattern repeated: + +```csharp +User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value +``` + +`SingleOrDefault` returns `null` when the claim doesn't exist, then `.Value` throws `NullReferenceException`. + +**Affected Controllers:** AuthenticationController, RoutesController, RoutesOdataController, MapsController, StationController, AdminController, TraewellingController, StationMergeController (35+ locations) + +**Fix:** Use `?.Value ?? ""` or extract into a helper method with proper null handling. + +--- + +### AuthenticationController + +| Bug | Line | Description | +|-----|------|-------------| +| Null reference | ~61 | Admin claim `.Value` access without null check | +| Missing validation | ~235 | No null check on `createAccount.Password` before `.Length` | +| Missing null check | ~49 | `loginRequest` parameter not validated | + +### RoutesController + +| Bug | Line | Description | +|-----|------|-------------| +| InvalidOperationException | ~241 | `.Single()` throws if no routes found | +| InvalidOperationException | ~367 | `.Single()` on `PostKmlToDatabase()` result throws if empty | +| Unsafe claim access | Multiple | Same pervasive pattern | + +### RoutesOdataController + +| Bug | Line | Description | +|-----|------|-------------| +| Null RouteType | ~268 | `route.RouteType` accessed without null check | +| Console.WriteLine in production | ~187 | Should use ILogger | + +### MapsController + +| Bug | Line | Description | +|-----|------|-------------| +| Null dbMap access | ~71 | `dbMap` not null-checked after database query | +| Missing input validation | ~73 | No length validation on `SharingLinkName` and `Name` | + +### StationController + +| Bug | Line | Description | +|-----|------|-------------| +| Null station access | ~110 | `station.Regions` accessed after `SingleOrDefaultAsync` without null check | +| Race condition | ~106-112 | Station fetched after `SaveChangesAsync` could be deleted by another user | + +### AdminController + +| Bug | Line | Description | +|-----|------|-------------| +| InvalidOperationException | ~320 | `.Single()` on parsed elements could throw if empty | +| Console.WriteLine in production | Multiple | Should use ILogger | + +### ImporterController + +| Bug | Line | Description | +|-----|------|-------------| +| Null reference | ~108 | `relation.Members` could be null | +| Null reference | ~193 | `wayOsm.FirstOrDefault()` result `.Nodes` accessed without null check | +| Null reference | ~199 | `osm.Elements.FirstOrDefault(...)` result accessed without null check | +| Null reference | ~209 | `relation.Tags` accessed but relation could be null | + +### StationMergeController + +| Bug | Line | Description | +|-----|------|-------------| +| Race condition | ~343-346 | Stations could be deleted between queries | + +### TrawellingService + +| Bug | Line | Description | +|-----|------|-------------| +| Race condition | ~79-89 | `_oauthStates` dictionary not consistently locked | +| Null deserialization | ~150, 265, 325 | `JsonConvert.DeserializeObject` results used without null checks | +| Thread safety | ~331, 407 | `_memoryCache.Set()` called without thread-safety consideration | +| Null navigation | ~412 | `statusesResponse.Links.Next` - `Links` could be null | + +--- + +## Summary + +### Bug Count by Category + +| Category | Count | +|----------|-------| +| Memory leaks (missing unsubscribe) | 25+ | +| Null reference / null pointer errors | 50+ | +| Missing error handling | 15+ | +| Logic errors (incorrect conditions) | 8 | +| Race conditions | 8 | +| Type mismatches | 5 | +| Security concerns | 2 | +| Infinite recursion | 2 | +| **Total** | **~115+** | + +### Most Critical Issues (Likely to Crash the App) + +1. **Infinite recursion** in bounds setter (map & single-route-map components) +2. **"Bearer null" header** sent after logout (auth interceptor) +3. **Duplicate SignalR connections** causing double event handling +4. **Copy-paste bug** in map query parameter parsing (`"from"` checked twice instead of `"to"`) +5. **Double `+=`** in filter string construction +6. **Backend NullReferenceException** from unsafe claim access pattern (35+ locations) +7. **`Single()` instead of `SingleOrDefault()`** in backend causing unhandled exceptions + +### Most Common Pattern + +The single most pervasive issue is **missing subscription cleanup** in Angular components. Over 25 subscriptions across the application lack `takeUntilDestroyed()`, `unsubscribe()`, or similar cleanup, leading to memory leaks during navigation. + +The second most common pattern is **unsafe null access** in the C# backend, where `SingleOrDefault()` results are accessed without null checks in 35+ locations across all controllers. diff --git a/OV_DB/Controllers/AdminController.cs b/OV_DB/Controllers/AdminController.cs index 76132d49..31897fcb 100644 --- a/OV_DB/Controllers/AdminController.cs +++ b/OV_DB/Controllers/AdminController.cs @@ -36,7 +36,7 @@ public AdminController(OVDBDatabaseContext dbContext, IConfiguration configurati [HttpPost("AddMissingGuidsForRoute")] public async Task AddMissingGuidsForRoutes() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -53,7 +53,7 @@ public async Task AddMissingGuidsForRoutes() [HttpGet("users")] public async Task GetAdministratorUsers() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -101,7 +101,7 @@ public async Task GetAdministratorUsers() [HttpGet("maps")] public async Task GetAdministratorMaps() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -122,7 +122,7 @@ public async Task GetAdministratorMaps() [HttpGet("distance/{id:int}")] public async Task CalculateDistanceById(int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -141,7 +141,7 @@ public async Task CalculateDistanceById(int id) [HttpGet("distance/missing")] public async Task CalculateDistanceForAllMissing() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -159,7 +159,7 @@ public async Task CalculateDistanceForAllMissing() [HttpGet("distance/all")] public async Task CalculateDistanceForAll() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -185,7 +185,7 @@ public async Task CalculateDistanceForAll() [HttpGet("convertToInstances")] public async Task ConvertToInstances() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -211,7 +211,7 @@ public async Task ConvertToInstances() [HttpGet("addRegions")] public async Task AddRegionsToAllRoutes([FromServices] IRouteRegionsService routeRegionsService) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -241,7 +241,7 @@ public async Task AddRegionsToAllRoutes([FromServices] IRouteRegio [HttpGet("fixOriginalnames")] public async Task FixOriginalNames() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -267,7 +267,7 @@ public async Task FixOriginalNames() [AllowAnonymous] public async Task AssignRegionsToStations([FromServices] IStationRegionsService stationRegionsService, [FromQuery] int? regionId) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); diff --git a/OV_DB/Controllers/ImporterController.cs b/OV_DB/Controllers/ImporterController.cs index c1c220ed..cc7050c2 100644 --- a/OV_DB/Controllers/ImporterController.cs +++ b/OV_DB/Controllers/ImporterController.cs @@ -622,7 +622,7 @@ private async Task> CreateRoutesListAsync(string reference, OSM [HttpPost("addRoute")] public async Task> ImportRouteToDatabaseAsync([FromBody] OSMLineDTO line) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/MapsController.cs b/OV_DB/Controllers/MapsController.cs index 7d3b7f93..0e13e506 100644 --- a/OV_DB/Controllers/MapsController.cs +++ b/OV_DB/Controllers/MapsController.cs @@ -26,7 +26,7 @@ public MapsController(OVDBDatabaseContext context) [HttpGet] public async Task>> GetMaps() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -42,7 +42,7 @@ public async Task>> GetMaps() [HttpGet("{id}")] public async Task> GetMap(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -63,13 +63,18 @@ public async Task> GetMap(int id) [HttpPut] public async Task PutMap(Map map) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); } var dbMap = await _context.Maps.Where(m => m.UserId == userIdClaim).SingleOrDefaultAsync(m => m.MapId == map.MapId); + if (dbMap == null) + { + return NotFound(); + } + dbMap.SharingLinkName = map.SharingLinkName; dbMap.Name = map.Name; dbMap.NameNL = map.NameNL; @@ -109,7 +114,7 @@ public async Task PutMap(Map map) [HttpPost] public async Task> PostMap(Map map) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -132,7 +137,7 @@ public async Task> PostMap(Map map) [HttpDelete("{id}")] public async Task> DeleteMap(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -153,7 +158,7 @@ public async Task> DeleteMap(int id) [HttpPost("order")] public async Task UpdateMapsOrdering([FromBody] List mapOrdering) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/OperatorsController.cs b/OV_DB/Controllers/OperatorsController.cs index e0cfc00d..46e914f0 100644 --- a/OV_DB/Controllers/OperatorsController.cs +++ b/OV_DB/Controllers/OperatorsController.cs @@ -35,7 +35,7 @@ public OperatorsController(IConfiguration configuration, OVDBDatabaseContext dbC [HttpPost] public async Task> CreateOperator(OperatorUpdateDTO createOperator) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -58,7 +58,7 @@ public async Task> CreateOperator(OperatorUpdateDTO creat [HttpGet("{id}")] public async Task> GetOperatorById(int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -127,7 +127,7 @@ public async Task> GetOperatorLogo(int id) [HttpGet] public async Task>> GetAllOperators() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -139,7 +139,7 @@ public async Task>> GetAllOperators() [HttpPut("{id}")] public async Task UpdateOperator(int id, OperatorUpdateDTO updatedOperator) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -165,7 +165,7 @@ public async Task UpdateOperator(int id, OperatorUpdateDTO update [HttpDelete("{id}")] public async Task DeleteOperator(int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -185,7 +185,7 @@ public async Task DeleteOperator(int id) [HttpPost("{id}/uploadLogo")] public async Task Upload(IFormFile file, int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -218,7 +218,7 @@ public async Task Upload(IFormFile file, int id) [HttpPatch("{id}/connect")] public async Task ConnectAllRoutesForThisOperator(int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -258,7 +258,7 @@ public async Task ConnectAllRoutesForThisOperator(int id) [HttpGet("openOperators/{regionId}")] public async Task>> GetUnclaimedOperators(int regionId) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -279,7 +279,7 @@ public async Task>> GetUnclaimedOperators(int r [HttpGet("groupedByRegion")] public async Task>> GetOperatorsGroupedByRegion() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/RegionsController.cs b/OV_DB/Controllers/RegionsController.cs index 1260ffb4..d0dde010 100644 --- a/OV_DB/Controllers/RegionsController.cs +++ b/OV_DB/Controllers/RegionsController.cs @@ -75,7 +75,7 @@ private async Task> GetTagsAsync(long id) [HttpPut("{id}")] public async Task Update(int id, [FromBody] RegionDTO input) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -94,7 +94,7 @@ public async Task Update(int id, [FromBody] RegionDTO input) [HttpPost] public async Task CreateNew(NewRegion newRegion) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -258,7 +258,7 @@ public async Task>> GetAllWithStations() [HttpGet("map/{mapGuid}")] public async Task>> GetAllForMap(Guid mapGuid) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -345,7 +345,7 @@ private static IEnumerable BuildHierarchy(IEnumerable RefreshAll() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -373,7 +373,7 @@ public async Task RefreshAll() [HttpPost("{id}/refreshRoutes")] public async Task RefreshRoutesAsync(int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -388,7 +388,7 @@ public async Task RefreshRoutesAsync(int id) [HttpPost("refreshRoutesWithoutRegions")] public IActionResult RefreshRoutesWithoutRegions() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); diff --git a/OV_DB/Controllers/RequestsController.cs b/OV_DB/Controllers/RequestsController.cs index cac5dea6..498d2a93 100644 --- a/OV_DB/Controllers/RequestsController.cs +++ b/OV_DB/Controllers/RequestsController.cs @@ -20,7 +20,7 @@ public class RequestsController(OVDBDatabaseContext dbContext, TelegramBotServic [HttpGet] public async Task GetUserRequests() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -49,7 +49,7 @@ public async Task GetUserRequests() [HttpGet("admin")] public async Task GetAdminRequests() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -80,7 +80,7 @@ public async Task GetAdminRequests() [HttpPost] public async Task CreateRequest([FromBody] CreateRequestDTO createRequest) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -114,7 +114,7 @@ public async Task CreateRequest([FromBody] CreateRequestDTO creat [HttpPatch("admin/{id}/respond")] public async Task RespondToRequest(int id, [FromBody] CreateRequestDTO response) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -136,7 +136,7 @@ public async Task RespondToRequest(int id, [FromBody] CreateReque [HttpGet("anyUnread")] public async Task AnyUnreadRequests() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -149,7 +149,7 @@ public async Task AnyUnreadRequests() [HttpGet("admin/anyUnread")] public async Task AnyUnreadAdminRequests() { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); diff --git a/OV_DB/Controllers/RouteTypesController.cs b/OV_DB/Controllers/RouteTypesController.cs index 234db7e6..067ed52c 100644 --- a/OV_DB/Controllers/RouteTypesController.cs +++ b/OV_DB/Controllers/RouteTypesController.cs @@ -25,7 +25,7 @@ public RouteTypesController(OVDBDatabaseContext context) [HttpGet] public async Task>> GetRouteTypes() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -36,7 +36,7 @@ public async Task>> GetRouteTypes() [HttpGet("{id}")] public async Task> GetRouteType(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -53,7 +53,7 @@ public async Task> GetRouteType(int id) [HttpPost] public async Task AddRouteType([FromBody] RouteType routeType) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -67,7 +67,7 @@ public async Task AddRouteType([FromBody] RouteType routeType) [HttpPut] public async Task UpdateRouteType([FromBody] RouteType routeType) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -90,7 +90,7 @@ public async Task UpdateRouteType([FromBody] RouteType routeType) [HttpDelete("{id}")] public async Task DeleteRouteType(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -115,7 +115,7 @@ public async Task DeleteRouteType(int id) [HttpPost("order")] public async Task UpdateRouteTypeOrdering([FromBody] List routeTypeOrder) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/RoutesController.cs b/OV_DB/Controllers/RoutesController.cs index 14d732af..78648594 100644 --- a/OV_DB/Controllers/RoutesController.cs +++ b/OV_DB/Controllers/RoutesController.cs @@ -45,7 +45,7 @@ public RoutesController(OVDBDatabaseContext context, IRouteRegionsService routeR [HttpGet] public async Task> GetRoutes([FromQuery] int? start, [FromQuery] int? count, [FromQuery] string sortColumn, [FromQuery] bool? descending, [FromQuery] string filter, CancellationToken cancellationToken) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -129,7 +129,7 @@ public async Task> GetRoutes([FromQuery] int? start, [HttpGet("instances/list")] public async Task> GetRouteInstances([FromQuery] int? start, [FromQuery] int? count, [FromQuery] string sortColumn, [FromQuery] bool? descending, [FromQuery] string filter, CancellationToken cancellationToken) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -200,7 +200,7 @@ public async Task> GetRouteInstances( [HttpGet("missingInfo")] public async Task>> GetRoutesWithMissingInfo() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -228,7 +228,7 @@ public async Task>> GetRoutesWithMissingInfo( [HttpGet("{id}")] public async Task> GetRoute(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -238,7 +238,7 @@ public async Task> GetRoute(int id) .Include(r => r.RouteInstances) .Include(r => r.RouteMaps) .SelectToRouteDTO() - .SingleAsync(r => r.RouteId == id); + .SingleOrDefaultAsync(r => r.RouteId == id); if (route == null) { return NotFound(); @@ -252,7 +252,7 @@ public async Task> GetRoute(int id) [HttpPut("{id}")] public async Task PutRoute(int id, UpdateRoute route) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -356,7 +356,7 @@ public async Task PutRoute(int id, UpdateRoute route) [HttpPost("kml")] public async Task> PostKML() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -364,7 +364,12 @@ public async Task> PostKML() using var stream = new StreamReader(HttpContext.Request.Body); - var route = (await PostKmlToDatabase(stream, "Nieuwe route", userIdClaim)).Single(); + var routes = await PostKmlToDatabase(stream, "Nieuwe route", userIdClaim); + var route = routes.FirstOrDefault(); + if (route == null) + { + return BadRequest("No route could be imported from the provided KML."); + } return CreatedAtAction("GetRoute", new { id = route.RouteId }, route); } @@ -372,7 +377,7 @@ public async Task> PostKML() [HttpPost("kmlfile")] public async Task> PostKMLFiles() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -645,7 +650,7 @@ private async Task> PostGpxToDatabase(StreamReader stream, int mapId [HttpDelete("{id}")] public async Task> DeleteRoute(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -669,7 +674,7 @@ public async Task> DeleteRoute(int id) [HttpGet("{id}/export")] public async Task> ExportRoute(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -716,7 +721,7 @@ private static void WriteRouteToStreamAsGPX(string routeName, NetTopologySuite.G public async Task> ExportSetOfRoutes([FromQuery] string routeIds) { var splitRouteIds = routeIds.Split(',').Select(int.Parse).ToList(); - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -758,7 +763,7 @@ public async Task> ExportSetOfRoutes([FromQuery] string rou [HttpGet("export")] public async Task> ExportAllRoutes(int id, [FromQuery] Guid? map, [FromQuery] int? year) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -884,7 +889,7 @@ private bool RouteExists(int id) [HttpPut("editmultiple")] public async Task UpdateMultipleRoutes([FromBody] EditMultiple editMultiple) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -1018,7 +1023,7 @@ public async Task> GetRouteInstances(Guid ma [HttpPut("instances")] public async Task UpdateInstance([FromBody] RouteInstanceUpdate update) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -1142,7 +1147,7 @@ public async Task UpdateInstance([FromBody] RouteInstanceUpdate up [HttpDelete("instances/{id:int}")] public async Task DeleteInstance(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -1165,7 +1170,7 @@ public async Task DeleteInstance(int id) [HttpGet("instances/tags/autocomplete")] public async Task AutocompleteTags() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -1184,7 +1189,7 @@ public async Task AutocompleteTags() [HttpGet("operators/autocomplete")] public async Task>> AutocompleteOperators() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/RoutesOdataController.cs b/OV_DB/Controllers/RoutesOdataController.cs index 7b917fa8..2e3b7c37 100644 --- a/OV_DB/Controllers/RoutesOdataController.cs +++ b/OV_DB/Controllers/RoutesOdataController.cs @@ -58,7 +58,7 @@ public async Task> GetGeoJsonAsync(string id, ODataQuer { return Forbid(); } - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if ((userIdClaim < 0 || map.UserId != userIdClaim) && !string.Equals(adminClaim, "true", StringComparison.OrdinalIgnoreCase)) { diff --git a/OV_DB/Controllers/StationController.cs b/OV_DB/Controllers/StationController.cs index da405ab5..4c823803 100644 --- a/OV_DB/Controllers/StationController.cs +++ b/OV_DB/Controllers/StationController.cs @@ -27,7 +27,7 @@ public StationController(OVDBDatabaseContext dbContext) [HttpGet] public async Task GetVisitedStations([FromQuery] string countryIds = "") { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -79,7 +79,7 @@ public async Task GetVisitedStations([FromQuery] string countryId [HttpPut("{id:int}")] public async Task UpdateVisitedStations(int id, [FromBody] BoolValue value) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -120,7 +120,7 @@ public async Task UpdateVisitedStations(int id, [FromBody] BoolVa [HttpGet("map")] public async Task GetAdminMap([FromQuery] List regions) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -148,7 +148,7 @@ public async Task GetAdminMap([FromQuery] List regions) [HttpPut("admin/{id:int}")] public async Task AdminUpdateStation(int id, [FromBody] StationVisibilityAdmin stationVisibility) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -171,7 +171,7 @@ public async Task AdminUpdateStation(int id, [FromBody] StationVi [HttpDelete("admin/{id:int}")] public async Task AdminDeleteStation(int id) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); diff --git a/OV_DB/Controllers/StationImporterController.cs b/OV_DB/Controllers/StationImporterController.cs index 5d686313..f18d3819 100644 --- a/OV_DB/Controllers/StationImporterController.cs +++ b/OV_DB/Controllers/StationImporterController.cs @@ -22,7 +22,7 @@ public class StationImporterController(OVDBDatabaseContext dbContext, IStationRe [HttpPost("region/{regionId}")] public async Task UpdateRegionAsync(int regionId) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); @@ -38,7 +38,7 @@ public async Task UpdateRegionAsync(int regionId) [HttpPost("{stationId}")] public async Task CreateStation(string stationId) { - var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin").Value ?? "false"); + var adminClaim = (User.Claims.SingleOrDefault(c => c.Type == "admin")?.Value ?? "false"); if (string.Equals(adminClaim, "false", StringComparison.OrdinalIgnoreCase)) { return Forbid(); diff --git a/OV_DB/Controllers/StationMapsController.cs b/OV_DB/Controllers/StationMapsController.cs index 25c1dca4..58bf53c0 100644 --- a/OV_DB/Controllers/StationMapsController.cs +++ b/OV_DB/Controllers/StationMapsController.cs @@ -29,7 +29,7 @@ public StationMapsController(OVDBDatabaseContext context) [HttpGet] public async Task>> GetMaps() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -43,7 +43,7 @@ public async Task>> GetMaps() [HttpGet("{id}")] public async Task> GetMap(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -63,7 +63,7 @@ public async Task> GetMap(int id) [HttpPut] public async Task PutMap(StationMapDTO stationMap) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -103,7 +103,7 @@ public async Task PutMap(StationMapDTO stationMap) [HttpPost] public async Task> PostMap(StationMapDTO stationMap) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -130,7 +130,7 @@ public async Task> PostMap(StationMapDTO stationMap) [HttpDelete("{id}")] public async Task> DeleteMap(int id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -149,7 +149,7 @@ public async Task> DeleteMap(int id) [HttpPost("order")] public async Task UpdateMapsOrdering([FromBody] List mapOrdering) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -165,7 +165,7 @@ public async Task UpdateMapsOrdering([FromBody] List mapOrder [HttpGet("map/{id}")] public async Task GetVisitedStations(string id) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/StatsController.cs b/OV_DB/Controllers/StatsController.cs index 65a884f6..54cd7b36 100644 --- a/OV_DB/Controllers/StatsController.cs +++ b/OV_DB/Controllers/StatsController.cs @@ -28,7 +28,7 @@ public StatsController(OVDBDatabaseContext context) [HttpGet("{map}")] public async Task GetStats(Guid map, [FromQuery] int? year) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -65,7 +65,7 @@ private IQueryable QueryForInstances(Guid map, int? year, int use [HttpGet("time/{map}")] public async Task GetTimedStats(Guid map, [FromQuery] int? year, [FromQuery] string language = "nl") { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -163,7 +163,7 @@ public async Task GetTimedStats(Guid map, [FromQuery] int? year, [ [HttpGet("reach/{map}")] public async Task GetReachStats(Guid map, [FromQuery] int? year) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -264,7 +264,7 @@ public async Task GetReachStats(Guid map, [FromQuery] int? year) [HttpGet("region")] public async Task>> GetRegionStats() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) return Forbid(); diff --git a/OV_DB/Controllers/TripReportController.cs b/OV_DB/Controllers/TripReportController.cs index 1aa2521c..6a7c9a6e 100644 --- a/OV_DB/Controllers/TripReportController.cs +++ b/OV_DB/Controllers/TripReportController.cs @@ -29,7 +29,7 @@ public TripReportController(OVDBDatabaseContext databaseContext) [HttpGet] public async Task GetTripReport([FromQuery] List guid, [FromQuery] int year, [FromQuery] bool english = false) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/Controllers/UserController.cs b/OV_DB/Controllers/UserController.cs index b4e5be4d..53e23e6b 100644 --- a/OV_DB/Controllers/UserController.cs +++ b/OV_DB/Controllers/UserController.cs @@ -33,7 +33,7 @@ public UserController(OVDBDatabaseContext databaseContext) [HttpGet("profile")] public async Task> GetProfileAsync() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -99,7 +99,7 @@ public async Task> GetProfileAsync() [HttpPut("profile")] public async Task UpdateProfileAsync([FromBody] UpdateProfileDTO updateProfile) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -156,7 +156,7 @@ public async Task UpdateProfileAsync([FromBody] UpdateProfileDTO u [HttpPost("change-password")] public async Task ChangePasswordAsync([FromBody] ChangePasswordDTO changePassword) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -192,7 +192,7 @@ public async Task ChangePasswordAsync([FromBody] ChangePasswordDTO [HttpPut("tag-mappings")] public async Task UpdateTagMappingsAsync([FromBody] List tagMappings) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -223,7 +223,7 @@ public async Task UpdateTagMappingsAsync([FromBody] List UpdateOperatorMappingsAsync([FromBody] List operatorMappings) { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); @@ -248,7 +248,7 @@ public async Task UpdateOperatorMappingsAsync([FromBody] List>> GetMapsAsync() { - var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value ?? "-1"); + var userIdClaim = int.Parse(User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "-1"); if (userIdClaim < 0) { return Forbid(); diff --git a/OV_DB/OVDBFrontend/src/app/admin/countries/countries.component.ts b/OV_DB/OVDBFrontend/src/app/admin/countries/countries.component.ts index 61c83ef3..e27d2f4b 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/countries/countries.component.ts +++ b/OV_DB/OVDBFrontend/src/app/admin/countries/countries.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, DestroyRef, OnInit, inject } from '@angular/core'; import { ApiService } from 'src/app/services/api.service'; import { Router } from '@angular/router'; import { Country } from 'src/app/models/country.model'; @@ -12,6 +12,7 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatList, MatListItem } from '@angular/material/list'; import { MatButton, MatIconButton, MatFabButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-countries', @@ -26,25 +27,29 @@ export class CountriesComponent implements OnInit { private translateService = inject(TranslateService); private translationService = inject(TranslationService); private dataUpdateService = inject(DataUpdateService); + private destroyRef = inject(DestroyRef); - data: Country[]; + data: Country[] = []; loading = false; ngOnInit() { this.loadData(); - this.translationService.languageChanged.subscribe(() => this.sort()); + this.translationService.languageChanged + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.sort()); } private loadData() { this.loading = true; this.apiService.getCountries().subscribe(data => { - this.data = data; + this.data = data ?? []; this.sort(); this.loading = false; }); } sort() { + if (!this.data) { return; } this.data = this.data.sort((a, b) => { if (this.name(a) > this.name(b)) { return 1; diff --git a/OV_DB/OVDBFrontend/src/app/admin/route-detail/route-detail.component.ts b/OV_DB/OVDBFrontend/src/app/admin/route-detail/route-detail.component.ts index 6ea005d3..9ffea409 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/route-detail/route-detail.component.ts +++ b/OV_DB/OVDBFrontend/src/app/admin/route-detail/route-detail.component.ts @@ -182,11 +182,14 @@ export class RouteDetailComponent implements OnInit { return; } const mapsSelection = this.mapsSelection(); - if (mapsSelection.selectedOptions.selected.length === 0) { + if (!mapsSelection || mapsSelection.selectedOptions.selected.length === 0) { return false; } const route = values as UpdateRoute; - route.routeId = this.route()!.routeId; + if (!this.route()) { + return; + } + route.routeId = this.route().routeId; route.overrideColour = this.colour(); route.maps = mapsSelection.selectedOptions.selected.map( (s) => s.value @@ -237,7 +240,7 @@ export class RouteDetailComponent implements OnInit { } get countriesString() { - if (!this.countriesSelection() || !this.countries) { + if (!this.countriesSelection() || !this.countries()) { return ""; } const countries = this.countries() @@ -259,7 +262,7 @@ export class RouteDetailComponent implements OnInit { } get mapsString() { - if (!this.mapsSelection() || !this.maps) { + if (!this.mapsSelection() || !this.maps()) { return ""; } const maps = this.maps() diff --git a/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.html b/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.html index 902bcab3..febda92b 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.html +++ b/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.html @@ -12,7 +12,7 @@

- + {{'ROUTEINSTANCESEDIT.ADDTIMES'|translate}}
@@ -49,7 +49,7 @@

{{'ROUTEINSTANCESEDIT.DAYOFFSET'|translate}} - + @for (offset of dayOffsetOptions(); track offset) { @if (offset === 0) { @@ -65,7 +65,7 @@

- + {{'ROUTEINSTANCESEDIT.TRIP_SCHEDULED_TIMES'|translate}}
@@ -148,7 +148,7 @@

{{'ROUTEINSTANCESEDIT.SELECTADDITIONALMAPS'|translate}} - + @for (map of maps(); track map) { {{name(map)}} diff --git a/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.ts b/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.ts index 43e1e946..93d89779 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.ts +++ b/OV_DB/OVDBFrontend/src/app/admin/route-instances-edit/route-instances-edit.component.ts @@ -268,7 +268,7 @@ export class RouteInstancesEditComponent implements OnInit { this.instance.routeInstanceProperties.slice(0, this.instance.routeInstanceProperties.length - 1); } this.instance.routeInstanceMaps = this.selectedMaps().map(s => { return { mapId: s } }); - if (this.instance.date['_isAMomentObject']) { + if (this.instance.date && (this.instance.date as any)['_isAMomentObject']) { this.instance.date = (this.instance.date as unknown as Moment).format('YYYY-MM-DD'); } if(!this.enterScheduledTimes()){ @@ -286,7 +286,7 @@ export class RouteInstancesEditComponent implements OnInit { } addRow() { this.instance.routeInstanceProperties.push({} as RouteInstanceProperty); - this.table().renderRows(); + this.table()?.renderRows(); } get canAddNewRow() { @@ -295,7 +295,7 @@ export class RouteInstancesEditComponent implements OnInit { removeRow(index: number) { this.instance.routeInstanceProperties.splice(index, 1); - this.table().renderRows(); + this.table()?.renderRows(); } rowIsEmpty(prop: RouteInstanceProperty) { diff --git a/OV_DB/OVDBFrontend/src/app/admin/route-instances-list/route-instances-list.component.ts b/OV_DB/OVDBFrontend/src/app/admin/route-instances-list/route-instances-list.component.ts index f30cda71..3a89ca48 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/route-instances-list/route-instances-list.component.ts +++ b/OV_DB/OVDBFrontend/src/app/admin/route-instances-list/route-instances-list.component.ts @@ -282,7 +282,7 @@ export class RouteInstancesListComponent implements OnInit, AfterViewInit { return element.arrivalDelayMinutes ?? null; } - formatDelay(minutes: number | null): string { + formatDelay(minutes: number | null): string | null { if (minutes === null) return null; const rounded = Math.round(minutes); if (rounded === 0) return '+0 min'; diff --git a/OV_DB/OVDBFrontend/src/app/admin/routes-list/routes-list.component.html b/OV_DB/OVDBFrontend/src/app/admin/routes-list/routes-list.component.html index e0342236..33f41f29 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/routes-list/routes-list.component.html +++ b/OV_DB/OVDBFrontend/src/app/admin/routes-list/routes-list.component.html @@ -113,7 +113,7 @@ } - @if (element.routeTypeColour) { + @if (element.routeTypeColour && element.routeType) { diff --git a/OV_DB/OVDBFrontend/src/app/admin/wizzard/wizard-step2/wizard-step2.component.ts b/OV_DB/OVDBFrontend/src/app/admin/wizzard/wizard-step2/wizard-step2.component.ts index a037f8e5..a48b36b4 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/wizzard/wizard-step2/wizard-step2.component.ts +++ b/OV_DB/OVDBFrontend/src/app/admin/wizzard/wizard-step2/wizard-step2.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, inject } from "@angular/core"; +import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "src/app/services/api.service"; import { OSMDataLine } from "src/app/models/osmDataLine.model"; @@ -22,6 +22,7 @@ import { NgClass } from "@angular/common"; import { CdkCopyToClipboard } from "@angular/cdk/clipboard"; import { TrawellingTripContext } from "src/app/models/traewelling.model"; import { TrawellingContextCardComponent } from "src/app/traewelling/context-card/traewelling-context-card.component"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @Component({ selector: "app-wizard-step2", @@ -50,6 +51,7 @@ export class WizzardStep2Component implements OnInit { private translateService = inject(TranslateService); private dialog = inject(MatDialog); private router = inject(Router); + private destroyRef = inject(DestroyRef); id: string; data: OSMDataLine; @@ -76,8 +78,12 @@ export class WizzardStep2Component implements OnInit { fromTraewelling = false; trawellingTripData: TrawellingTripContext | null = null; constructor() { - this.activatedRoute.params.subscribe((p) => (this.id = p.id)); - this.activatedRoute.queryParamMap.subscribe((p) => { + this.activatedRoute.params + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((p) => (this.id = p.id)); + this.activatedRoute.queryParamMap + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((p) => { if (p.has("date")) { this.dateTime = moment.unix(+p.get("date")); } else { diff --git a/OV_DB/OVDBFrontend/src/app/guards/administrator.guard.ts b/OV_DB/OVDBFrontend/src/app/guards/administrator.guard.ts index ea283b7a..1fdf6602 100644 --- a/OV_DB/OVDBFrontend/src/app/guards/administrator.guard.ts +++ b/OV_DB/OVDBFrontend/src/app/guards/administrator.guard.ts @@ -14,9 +14,13 @@ export class AdministratorGuard { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - if (!this.authService.admin) { + try { + if (!this.authService.isLoggedIn || !this.authService.admin) { + return false; + } + return true; + } catch { return false; } - return true; } } diff --git a/OV_DB/OVDBFrontend/src/app/guards/auth.interceptor.ts b/OV_DB/OVDBFrontend/src/app/guards/auth.interceptor.ts index fdfbf845..f6bb3c29 100644 --- a/OV_DB/OVDBFrontend/src/app/guards/auth.interceptor.ts +++ b/OV_DB/OVDBFrontend/src/app/guards/auth.interceptor.ts @@ -12,13 +12,19 @@ export class AuthInterceptor implements HttpInterceptor { intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (req.url.startsWith(environment.backend)) { + if (req.url.startsWith(environment.backend) && this.authService.token) { req = req.clone({ setHeaders: { Accept: 'application/json', Authorization: `Bearer ${this.authService.token}`, }, }); + } else if (req.url.startsWith(environment.backend)) { + req = req.clone({ + setHeaders: { + Accept: 'application/json', + }, + }); } return next.handle(req); } diff --git a/OV_DB/OVDBFrontend/src/app/guards/login.guard.ts b/OV_DB/OVDBFrontend/src/app/guards/login.guard.ts index 60581160..29c2a9a0 100644 --- a/OV_DB/OVDBFrontend/src/app/guards/login.guard.ts +++ b/OV_DB/OVDBFrontend/src/app/guards/login.guard.ts @@ -20,7 +20,8 @@ export class LoginGuard { } if (!this.authService.isLoggedIn) { - this.router.navigate(['/login'], { queryParams: { returnUrl: '/' + next.url.join('/') } }); + const returnUrl = next.url.length > 0 ? '/' + next.url.join('/') : '/'; + this.router.navigate(['/login'], { queryParams: { returnUrl } }); return false; } return true; diff --git a/OV_DB/OVDBFrontend/src/app/home/home.component.html b/OV_DB/OVDBFrontend/src/app/home/home.component.html index 5f1d41f0..487fe2be 100644 --- a/OV_DB/OVDBFrontend/src/app/home/home.component.html +++ b/OV_DB/OVDBFrontend/src/app/home/home.component.html @@ -23,7 +23,7 @@

{{ "HOME.WELCOME" | translate }}

- @if (!!maps() || !!stationMaps()) { + @if (maps().length > 0 || stationMaps().length > 0 || !loading) {
@if (maps().length > 0 || stationMaps().length > 0) { diff --git a/OV_DB/OVDBFrontend/src/app/image-creator/image-creator.component.ts b/OV_DB/OVDBFrontend/src/app/image-creator/image-creator.component.ts index 9d99a71f..665f0175 100644 --- a/OV_DB/OVDBFrontend/src/app/image-creator/image-creator.component.ts +++ b/OV_DB/OVDBFrontend/src/app/image-creator/image-creator.component.ts @@ -28,7 +28,7 @@ export class ImageCreatorComponent implements OnInit { translateService = inject(TranslateService); private cd = inject(ChangeDetectorRef); - maps: Map[]; + maps: Map[] = []; baseUrl = environment.backend + "api/images/"; selectedGuids: string[] = []; imageSrc: string = environment.backend + "api/images/"; @@ -38,8 +38,13 @@ export class ImageCreatorComponent implements OnInit { includeTotals = false; ngOnInit(): void { - this.apiService.getMaps().subscribe((maps) => { - this.maps = maps; + this.apiService.getMaps().subscribe({ + next: (maps) => { + this.maps = maps ?? []; + }, + error: () => { + this.maps = []; + } }); } diff --git a/OV_DB/OVDBFrontend/src/app/map/map.component.ts b/OV_DB/OVDBFrontend/src/app/map/map.component.ts index d7d6a60e..c2968e22 100644 --- a/OV_DB/OVDBFrontend/src/app/map/map.component.ts +++ b/OV_DB/OVDBFrontend/src/app/map/map.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit, AfterViewInit, ChangeDetectorRef, NgZone, EventEmitter, OnDestroy, input, viewChild, signal, inject } from "@angular/core"; +import { Component, OnInit, AfterViewInit, ChangeDetectorRef, NgZone, EventEmitter, OnDestroy, DestroyRef, input, viewChild, signal, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import moment from "moment"; import { tileLayer } from "leaflet"; import { ApiService } from "../services/api.service"; @@ -60,6 +61,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { private activatedRoute = inject(ActivatedRoute); private signalRService = inject(SignalRService); private cd = inject(ChangeDetectorRef); + private destroyRef = inject(DestroyRef); readonly guid = input(undefined); readonly mapContainer = viewChild("mapContainer"); @@ -84,7 +86,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { if (!!value && value.isValid()) { this._bounds = value; } else { - this.bounds = new LatLngBounds( + this._bounds = new LatLngBounds( new LatLng(50.656245, 2.92136), new LatLng(53.604563, 7.428211) ); @@ -216,18 +218,20 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { }); this.readFromQueryParams(); - this.translationService.languageChanged.subscribe(() => - this.getRoutes$.next(this.getFilter()) - ); + this.translationService.languageChanged + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.getRoutes$.next(this.getFilter())); this.signalRService.connect(); - this.signalRService.updates$.subscribe({ - next: (data) => { - if (data.requestIdentifier === this.requestIdentifier) { - this.loading.set(data.percentage); - this.cd.detectChanges(); - } - }, - }); + this.signalRService.updates$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (data) => { + if (data.requestIdentifier === this.requestIdentifier) { + this.loading.set(data.percentage); + this.cd.detectChanges(); + } + }, + }); } ngOnDestroy() { @@ -243,7 +247,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { if (queryParams.has("from")) { this.from = moment(+queryParams.get("from")); } - if (queryParams.has("from")) { + if (queryParams.has("to")) { this.to = moment(+queryParams.get("to")); } if (queryParams.has("types")) { @@ -427,7 +431,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { this.loading.set(true); let filter = ""; if (!!this.to && !!this.from) { - filter += filter += + filter += "(Date ge " + this.from.format("YYYY-MM-DD") + " and Date lt " + @@ -566,7 +570,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { this.defaults.forEach((value, key) => { if ( (value.from?.isSame(from) ?? (value.from == null && from == null)) && - (value.to?.isSame(to) ?? (value.to == null && from == null)) && + (value.to?.isSame(to) ?? (value.to == null && to == null)) && value.selectedYears.every((y) => (years??[]).includes(y)) && (years??[]).every((y) => value.selectedYears.includes(y)) ) { diff --git a/OV_DB/OVDBFrontend/src/app/profile/profile.component.ts b/OV_DB/OVDBFrontend/src/app/profile/profile.component.ts index aac93586..77a75d46 100644 --- a/OV_DB/OVDBFrontend/src/app/profile/profile.component.ts +++ b/OV_DB/OVDBFrontend/src/app/profile/profile.component.ts @@ -1,4 +1,4 @@ -import { Component, DestroyRef, OnInit, computed, inject, signal } from '@angular/core'; +import { Component, DestroyRef, OnInit, OnDestroy, computed, inject, signal } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ApiService } from '../services/api.service'; import { UserProfile, UpdateProfile, ChangePassword, TraewellingTagMapping, TrainlogOperatorMapping } from '../models/user-profile.model'; @@ -53,7 +53,7 @@ import { TrawellingService } from '../traewelling/services/traewelling.service'; OperatorMappingComponent ] }) -export class ProfileComponent implements OnInit { +export class ProfileComponent implements OnInit, OnDestroy { private formBuilder = inject(UntypedFormBuilder); private apiService = inject(ApiService); private snackBar = inject(MatSnackBar); @@ -243,6 +243,12 @@ export class ProfileComponent implements OnInit { // Open the authorization URL in a new window const authWindow = window.open(response.authorizationUrl, '_blank', 'width=600,height=700'); + if (!authWindow) { + this.trawellingConnecting = false; + this.showMessage('PROFILE.TRAEWELLING_POPUP_BLOCKED'); + return; + } + // Listen for messages from the popup const messageListener = (event: MessageEvent) => { if (event.origin !== window.location.origin) { @@ -437,6 +443,10 @@ export class ProfileComponent implements OnInit { } } + ngOnDestroy(): void { + this.stopPolling(); + } + loadAvailableOvdbTags(): void { this.apiService.getAutocompleteForTags().subscribe({ next: (tags) => { diff --git a/OV_DB/OVDBFrontend/src/app/services/authentication.service.ts b/OV_DB/OVDBFrontend/src/app/services/authentication.service.ts index 026f3cfb..99ab704e 100644 --- a/OV_DB/OVDBFrontend/src/app/services/authentication.service.ts +++ b/OV_DB/OVDBFrontend/src/app/services/authentication.service.ts @@ -153,11 +153,15 @@ export class AuthenticationService { } get email() { - return this.helper.decodeToken(this.token)['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']; + if (!this.token) return null; + return this.helper.decodeToken(this.token)?.['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] ?? null; } get admin() { - return this.helper.decodeToken(this.token).admin === 'true'; + if (!this.token) return false; + const decoded = this.helper.decodeToken(this.token); + if (!decoded) return false; + return decoded.admin === 'true' || decoded.admin === true; } getActiveSessions() { diff --git a/OV_DB/OVDBFrontend/src/app/services/signal-r.service.ts b/OV_DB/OVDBFrontend/src/app/services/signal-r.service.ts index 5182a998..a913befe 100644 --- a/OV_DB/OVDBFrontend/src/app/services/signal-r.service.ts +++ b/OV_DB/OVDBFrontend/src/app/services/signal-r.service.ts @@ -9,21 +9,19 @@ import { Subject } from "rxjs"; export class SignalRService { private connection?: HubConnection; public connected = false; + private connecting = false; updates$ = new Subject<{ requestIdentifier: string; percentage: number }>(); regionUpdates$ = new Subject<{ regionId: number; percentage: number, updatedRoutes: number | null }>(); stationUpdates$ = new Subject<{ regionId: number; percentage: number }>(); connect() { - if (this.connected) { + if (this.connected || this.connecting) { return; } + this.connecting = true; const connection = new HubConnectionBuilder() .withUrl(environment.backend + "mapGenerationHub") .withAutomaticReconnect() .build(); - connection - .start() - .then(() => (this.connected = true)) - .catch((err) => console.error(err.toString())); this.connection = connection; connection.on("GenerationUpdate", (requestIdentifier, percentage) => { this.updates$.next({ requestIdentifier, percentage }); @@ -35,7 +33,20 @@ export class SignalRService { this.stationUpdates$.next({ regionId, percentage }); console.log(regionId + ": " + percentage); }); - connection.onclose(() => (this.connected = false)); + connection.onclose(() => { + this.connected = false; + this.connecting = false; + }); + connection + .start() + .then(() => { + this.connected = true; + this.connecting = false; + }) + .catch((err) => { + console.error(err.toString()); + this.connecting = false; + }); } disconnect() { diff --git a/OV_DB/OVDBFrontend/src/app/single-route-map/single-route-map.component.ts b/OV_DB/OVDBFrontend/src/app/single-route-map/single-route-map.component.ts index 5a630594..834541ac 100644 --- a/OV_DB/OVDBFrontend/src/app/single-route-map/single-route-map.component.ts +++ b/OV_DB/OVDBFrontend/src/app/single-route-map/single-route-map.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, viewChild, inject } from '@angular/core'; +import { Component, OnInit, OnDestroy, DestroyRef, viewChild, inject } from '@angular/core'; import { LatLngBounds, LatLng, geoJSON } from 'leaflet'; import { tileLayer } from 'leaflet'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; @@ -8,6 +8,7 @@ import { ActivatedRoute } from '@angular/router'; import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { NgClass } from '@angular/common'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-single-route-map', @@ -20,6 +21,7 @@ export class SingleRouteMapComponent implements OnInit { private translationService = inject(TranslationService); private activatedRoute = inject(ActivatedRoute); private apiService = inject(ApiService); + private destroyRef = inject(DestroyRef); readonly mapContainer = viewChild('mapContainer'); loading = false; @@ -35,7 +37,7 @@ export class SingleRouteMapComponent implements OnInit { if (!!value && value.isValid()) { this._bounds = value; } else { - this.bounds = new LatLngBounds(new LatLng(50.656245, 2.921360), new LatLng(53.604563, 7.428211)); + this._bounds = new LatLngBounds(new LatLng(50.656245, 2.921360), new LatLng(53.604563, 7.428211)); } } private _bounds: LatLngBounds; @@ -76,12 +78,16 @@ export class SingleRouteMapComponent implements OnInit { ngOnInit() { - this.activatedRoute.paramMap.subscribe(p => { - this.routeId = +p.get('routeId'); - this.guid = p.get('guid'); - this.getRoute(); - }); - this.translationService.languageChanged.subscribe(() => this.getRoute()); + this.activatedRoute.paramMap + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(p => { + this.routeId = +p.get('routeId'); + this.guid = p.get('guid'); + this.getRoute(); + }); + this.translationService.languageChanged + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.getRoute()); } @@ -100,7 +106,8 @@ export class SingleRouteMapComponent implements OnInit { }; }, onEachFeature(feature, layer) { - let popup = '

' + feature.properties.name + '

' + if (!feature.properties) return; + let popup = '

' + (feature.properties.name ?? '') + '

' + parent.translateService.instant('MAP.POPUP.TYPE') + ': ' + feature.properties.type; if (feature.properties.description) { @@ -116,11 +123,14 @@ export class SingleRouteMapComponent implements OnInit { layer.on('click', f => { f.target.setStyle({ weight: 8, }); f.target.bringToFront(); - f.target.getPopup().on('remove', () => { - f.target.setStyle({ - weight: 3, + const popup = f.target.getPopup(); + if (popup) { + popup.on('remove', () => { + f.target.setStyle({ + weight: 3, + }); }); - }); + } }); layer.bindPopup(popup); } diff --git a/OV_DB/OVDBFrontend/src/app/stats/time-stats/time-stats.component.ts b/OV_DB/OVDBFrontend/src/app/stats/time-stats/time-stats.component.ts index c7017916..0a541092 100644 --- a/OV_DB/OVDBFrontend/src/app/stats/time-stats/time-stats.component.ts +++ b/OV_DB/OVDBFrontend/src/app/stats/time-stats/time-stats.component.ts @@ -1,5 +1,5 @@ import { NgClass } from '@angular/common'; -import { Component, Signal, viewChild, OnInit, inject } from '@angular/core'; +import { Component, DestroyRef, Signal, viewChild, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButton } from '@angular/material/button'; import { MatCard, MatCardContent, MatCardTitle } from '@angular/material/card'; @@ -18,6 +18,7 @@ import { Map } from 'src/app/models/map.model'; import { BaseChartDirective } from 'ng2-charts'; import 'chartjs-adapter-luxon'; import { MatTabsModule } from '@angular/material/tabs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-time-stats', imports: [MatCard, MatCardTitle, MatFormField, MatLabel, MatSelect, MatOption, FormsModule, MatButton, LeafletModule, NgClass, MatProgressSpinner, TranslateModule, BaseChartDirective, MatTabsModule, MatCardContent], @@ -27,6 +28,7 @@ import { MatTabsModule } from '@angular/material/tabs'; export class TimeStatsComponent implements OnInit { private apiService = inject(ApiService); private translationService = inject(TranslationService); + private destroyRef = inject(DestroyRef); translateService = inject(TranslateService); data: ChartConfiguration['data']; @@ -102,18 +104,22 @@ export class TimeStatsComponent implements OnInit { maps: Map[]; ngOnInit(): void { - this.apiService.getMaps().subscribe(maps => { - this.maps = maps; - }); + this.apiService.getMaps() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(maps => { + this.maps = maps ?? []; + }); } changeMap(mapGuid: string) { this.selectedMap = mapGuid; - this.apiService.getYears(mapGuid).subscribe(years => { - this.years = years.sort().reverse(); - }); + this.apiService.getYears(mapGuid) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(years => { + this.years = years.sort().reverse(); + }); this.data = null; this.layers = []; this.tableData = null; @@ -122,79 +128,94 @@ export class TimeStatsComponent implements OnInit { getData(year?: number) { if (year === 0) year = null; - this.apiService.getStatsForGraph(this.selectedMap, year).subscribe(stats => { - this.data = stats.cumulative; - this.singleData = stats.single; - }); - this.apiService.getStats(this.selectedMap, year).subscribe(data => { - this.tableData = data; - }); - this.loadingMap = true; - this.apiService.getStatsReach(this.selectedMap, year).subscribe((data: any) => { - this.layers = []; - const latMin = marker([data.latMin.lat, data.latMin.long], { - title: 'LatMin', icon: icon({ - iconSize: [25, 41], - iconAnchor: [13, 41], - iconUrl: 'assets/marker-icon.png', - shadowUrl: 'assets/marker-shadow.png' - }) + this.apiService.getStatsForGraph(this.selectedMap, year) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(stats => { + this.data = stats.cumulative; + this.singleData = stats.single; }); - let popup = `

${this.translateService.instant('EXTREMES.SOUTH')}

`; - popup += '

Latitude: ' + data.latMin.lat + '
'; - popup += 'Longitude: ' + data.latMin.long + '
'; - popup += 'Route: ' + data.latMin.route.name + '

'; - latMin.bindPopup(popup); - this.layers.push(latMin); - const latMax = marker([data.latMax.lat, data.latMax.long], { - title: 'latMax', icon: icon({ - iconSize: [25, 41], - iconAnchor: [13, 41], - iconUrl: 'assets/marker-icon.png', - shadowUrl: 'assets/marker-shadow.png' - }) + this.apiService.getStats(this.selectedMap, year) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.tableData = data; }); - popup = `

${this.translateService.instant('EXTREMES.NORTH')}

`; - popup += '

Latitude: ' + data.latMax.lat + '
'; - popup += 'Longitude: ' + data.latMax.long + '
'; - popup += 'Route: ' + data.latMax.route.name + '

'; - latMax.bindPopup(popup); - this.layers.push(latMax); - const longMin = marker([data.longMin.lat, data.longMin.long], { - title: 'longMin', icon: icon({ - iconSize: [25, 41], - iconAnchor: [13, 41], - iconUrl: 'assets/marker-icon.png', - shadowUrl: 'assets/marker-shadow.png' - }) - }); - popup = `

${this.translateService.instant('EXTREMES.WEST')}

`; - popup += '

Latitude: ' + data.longMin.lat + '
'; - popup += 'Longitude: ' + data.longMin.long + '
'; - popup += 'Route: ' + data.longMin.route.name + '

'; - longMin.bindPopup(popup); - this.layers.push(longMin); - const longMax = marker([data.longMax.lat, data.longMax.long], { - title: 'longMax', icon: icon({ - iconSize: [25, 41], - iconAnchor: [13, 41], - iconUrl: 'assets/marker-icon.png', - shadowUrl: 'assets/marker-shadow.png' - }) - }); - popup = `

${this.translateService.instant('EXTREMES.EAST')}

`; - popup += '

Latitude: ' + data.longMax.lat + '
'; - popup += 'Longitude: ' + data.longMax.long + '
'; - popup += 'Route: ' + data.longMax.route.name + '

'; - longMax.bindPopup(popup); - this.layers.push(longMax); - this.bounds = new LatLngBounds([data.latMin.lat, data.longMin.long], [data.latMax.lat, data.longMax.long]); - const rectangle = new Rectangle(this.bounds, { - fill: false + this.loadingMap = true; + this.apiService.getStatsReach(this.selectedMap, year) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (data: any) => { + if (!data) { + this.loadingMap = false; + return; + } + this.layers = []; + const latMin = marker([data.latMin.lat, data.latMin.long], { + title: 'LatMin', icon: icon({ + iconSize: [25, 41], + iconAnchor: [13, 41], + iconUrl: 'assets/marker-icon.png', + shadowUrl: 'assets/marker-shadow.png' + }) + }); + let popup = `

${this.translateService.instant('EXTREMES.SOUTH')}

`; + popup += '

Latitude: ' + data.latMin.lat + '
'; + popup += 'Longitude: ' + data.latMin.long + '
'; + popup += 'Route: ' + data.latMin.route.name + '

'; + latMin.bindPopup(popup); + this.layers.push(latMin); + const latMax = marker([data.latMax.lat, data.latMax.long], { + title: 'latMax', icon: icon({ + iconSize: [25, 41], + iconAnchor: [13, 41], + iconUrl: 'assets/marker-icon.png', + shadowUrl: 'assets/marker-shadow.png' + }) + }); + popup = `

${this.translateService.instant('EXTREMES.NORTH')}

`; + popup += '

Latitude: ' + data.latMax.lat + '
'; + popup += 'Longitude: ' + data.latMax.long + '
'; + popup += 'Route: ' + data.latMax.route.name + '

'; + latMax.bindPopup(popup); + this.layers.push(latMax); + const longMin = marker([data.longMin.lat, data.longMin.long], { + title: 'longMin', icon: icon({ + iconSize: [25, 41], + iconAnchor: [13, 41], + iconUrl: 'assets/marker-icon.png', + shadowUrl: 'assets/marker-shadow.png' + }) + }); + popup = `

${this.translateService.instant('EXTREMES.WEST')}

`; + popup += '

Latitude: ' + data.longMin.lat + '
'; + popup += 'Longitude: ' + data.longMin.long + '
'; + popup += 'Route: ' + data.longMin.route.name + '

'; + longMin.bindPopup(popup); + this.layers.push(longMin); + const longMax = marker([data.longMax.lat, data.longMax.long], { + title: 'longMax', icon: icon({ + iconSize: [25, 41], + iconAnchor: [13, 41], + iconUrl: 'assets/marker-icon.png', + shadowUrl: 'assets/marker-shadow.png' + }) + }); + popup = `

${this.translateService.instant('EXTREMES.EAST')}

`; + popup += '

Latitude: ' + data.longMax.lat + '
'; + popup += 'Longitude: ' + data.longMax.long + '
'; + popup += 'Route: ' + data.longMax.route.name + '

'; + longMax.bindPopup(popup); + this.layers.push(longMax); + this.bounds = new LatLngBounds([data.latMin.lat, data.longMin.long], [data.latMax.lat, data.longMax.long]); + const rectangle = new Rectangle(this.bounds, { + fill: false + }); + this.layers.push(rectangle); + this.loadingMap = false; + }, + error: () => { + this.loadingMap = false; + } }); - this.layers.push(rectangle); - this.loadingMap = false; - }); } diff --git a/OV_DB/OVDBFrontend/src/app/traewelling/traewelling.component.html b/OV_DB/OVDBFrontend/src/app/traewelling/traewelling.component.html index 2cfc7317..8e1cbd8b 100644 --- a/OV_DB/OVDBFrontend/src/app/traewelling/traewelling.component.html +++ b/OV_DB/OVDBFrontend/src/app/traewelling/traewelling.component.html @@ -10,7 +10,7 @@ @if (connectionStatus?.user) { - {{ 'PROFILE.TRAEWELLING_CONNECTED' | translate }} {{ connectionStatus.user.displayName }} + {{ 'PROFILE.TRAEWELLING_CONNECTED' | translate }} {{ connectionStatus?.user?.displayName }} }