diff --git a/API.Tests/Services/TokenServiceTests.cs b/API.Tests/Services/TokenServiceTests.cs new file mode 100644 index 000000000..d40be5bc9 --- /dev/null +++ b/API.Tests/Services/TokenServiceTests.cs @@ -0,0 +1,31 @@ +using API.Services; +using Xunit; + +namespace API.Tests.Services; + +public class TokenServiceTests +{ + [Fact] + public void HasTokenExpired_OldToken() + { + // ValidTo: 1/1/1990 + var result = TokenService.HasTokenExpired("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjYzMzgzMDM5OX0.KM_cUKSaCJL3ts0Qim3ZHUeJT7yf-wKoLdKb0rx0VbU"); + + Assert.True(result); + } + + [Fact] + public void HasTokenExpired_ValidInFuture() + { + // ValidTo: 4/11/2200 + var result = TokenService.HasTokenExpired("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjcyNjg0ODYzOTl9.nZrN5USbUmMYDKwkPoMtEAhTeYTeaikgAeSzDPj5kZQ"); + Assert.False(result); + } + + [Fact] + public void HasTokenExpired_NoToken() + { + var result = TokenService.HasTokenExpired(""); + Assert.True(result); + } +} diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index ad17e62b1..8e81395cf 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -30,9 +30,9 @@ public enum LibraryType [Description("Light Novel")] LightNovel = 4, /// - /// Uses Comic regex for filename parsing, uses ComicVine type of Parsing. Will replace Comic type in future + /// Uses Comic regex for filename parsing, uses Comic Vine type of Parsing. Will replace Comic type in future /// - [Description("Comic (ComicVine)")] + [Description("Comic (Comic Vine)")] ComicVine = 5, } diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs index 13c51c8d5..ddaf93b64 100644 --- a/API/Services/DeviceService.cs +++ b/API/Services/DeviceService.cs @@ -108,7 +108,7 @@ public class DeviceService : IDeviceService public async Task SendTo(IReadOnlyList chapterIds, int deviceId) { var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!settings.IsEmailSetup()) + if (!settings.IsEmailSetupForSendToDevice()) throw new KavitaException("send-to-kavita-email"); var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId); @@ -123,9 +123,16 @@ public class DeviceService : IDeviceService throw new KavitaException("send-to-size-limit"); - device.UpdateLastUsed(); - _unitOfWork.DeviceRepository.Update(device); - await _unitOfWork.CommitAsync(); + try + { + device.UpdateLastUsed(); + _unitOfWork.DeviceRepository.Update(device); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue updating device last used time"); + } var success = await _emailService.SendFilesToEmail(new SendToDto() { diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index f440548ca..57dca5943 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -126,7 +126,7 @@ public class ScrobblingService : IScrobblingService var users = await _unitOfWork.UserRepository.GetAllUsersAsync(); foreach (var user in users) { - if (string.IsNullOrEmpty(user.AniListAccessToken) || !_tokenService.HasTokenExpired(user.AniListAccessToken)) continue; + if (string.IsNullOrEmpty(user.AniListAccessToken) || !TokenService.HasTokenExpired(user.AniListAccessToken)) continue; _logger.LogInformation("User {UserName}'s AniList token has expired! They need to regenerate it for scrobbling to work", user.UserName); await _eventHub.SendMessageToAsync(MessageFactory.ScrobblingKeyExpired, MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), user.Id); @@ -151,7 +151,7 @@ public class ScrobblingService : IScrobblingService private async Task HasTokenExpired(string token, ScrobbleProvider provider) { if (string.IsNullOrEmpty(token) || - !_tokenService.HasTokenExpired(token)) return false; + !TokenService.HasTokenExpired(token)) return false; var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); if (string.IsNullOrEmpty(license.Value)) return true; @@ -778,7 +778,7 @@ public class ScrobblingService : IScrobblingService continue; } - if (_tokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) + if (TokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) { _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() { diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index 676d88d9a..4c0a721a0 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -561,7 +561,7 @@ public class ReadingListService : IReadingListService // How can we match properly with ComicVine library when year is part of the series unless we do this in 2 passes and see which has a better match - if (!userSeries.Any()) + if (userSeries.Count == 0) { // Report that no series exist in the reading list importSummary.Results.Add(new CblBookResult diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index c03c7724a..cc998de3d 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -24,8 +24,7 @@ public interface ITokenService Task CreateToken(AppUser user); Task ValidateRefreshToken(TokenRequestDto request); Task CreateRefreshToken(AppUser user); - Task GetJwtFromUser(AppUser user); - bool HasTokenExpired(string token); + Task GetJwtFromUser(AppUser user); } @@ -135,18 +134,21 @@ public class TokenService : ITokenService } } - public async Task GetJwtFromUser(AppUser user) + public async Task GetJwtFromUser(AppUser user) { var userClaims = await _userManager.GetClaimsAsync(user); var jwtClaim = userClaims.FirstOrDefault(claim => claim.Type == "jwt"); return jwtClaim?.Value; } - public bool HasTokenExpired(string? token) + public static bool HasTokenExpired(string? token) { if (string.IsNullOrEmpty(token)) return true; + var tokenHandler = new JwtSecurityTokenHandler(); var tokenContent = tokenHandler.ReadJwtToken(token); - return tokenContent.ValidTo >= DateTime.UtcNow; + var validToUtc = tokenContent.ValidTo.ToUniversalTime(); + + return validToUtc < DateTime.UtcNow; } } diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index ab0153a39..2a67c60af 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -70,6 +70,7 @@ export class ManageEmailSettingsComponent implements OnInit { this.settingsForm.get('port')?.setValue(587); this.settingsForm.get('sizeLimit')?.setValue(26214400); this.settingsForm.get('enableSsl')?.setValue(true); + this.settingsForm.markAsDirty(); this.cdRef.markForCheck(); } @@ -78,6 +79,7 @@ export class ManageEmailSettingsComponent implements OnInit { this.settingsForm.get('port')?.setValue(587 ); this.settingsForm.get('sizeLimit')?.setValue(1048576); this.settingsForm.get('enableSsl')?.setValue(true); + this.settingsForm.markAsDirty(); this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index dda773cb3..eb15f1b28 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -26,6 +26,7 @@
+
= []; isAdmin$: Observable = of(false); filterOpen: EventEmitter = new EventEmitter(); - trackByIdentity = (index: number, item: UserCollection) => `${item.id}_${item.title}`; + trackByIdentity = (index: number, item: UserCollection) => `${item.id}_${item.title}_${item.owner}_${item.promoted}`; user!: User; @HostListener('document:keydown.shift', ['$event']) @@ -146,13 +146,14 @@ export class AllCollectionsComponent implements OnInit { switch (action.action) { case Action.Promote: - this.collectionService.promoteMultipleCollections([collectionTag.id], true).subscribe(); + this.collectionService.promoteMultipleCollections([collectionTag.id], true).subscribe(_ => this.loadPage()); break; case Action.UnPromote: - this.collectionService.promoteMultipleCollections([collectionTag.id], false).subscribe(); + this.collectionService.promoteMultipleCollections([collectionTag.id], false).subscribe(_ => this.loadPage()); break; case(Action.Delete): this.collectionService.deleteTag(collectionTag.id).subscribe(res => { + this.loadPage(); this.toastr.success(res); }); break; diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index cfa8dc4a1..62ffb2e87 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -114,9 +114,6 @@ export class SeriesMetadataDetailComponent implements OnChanges, OnInit { } ngOnChanges(changes: SimpleChanges): void { - - - this.hasExtendedProperties = this.seriesMetadata.colorists.length > 0 || this.seriesMetadata.editors.length > 0 || this.seriesMetadata.coverArtists.length > 0 || diff --git a/openapi.json b/openapi.json index 7c8ee793a..338c18aca 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.14.14" + "version": "0.7.14.15" }, "servers": [ {