diff --git a/back/src/Kyoo.Abstractions/Extensions.cs b/back/src/Kyoo.Abstractions/Extensions.cs index 1d4a26c2..05756ba6 100644 --- a/back/src/Kyoo.Abstractions/Extensions.cs +++ b/back/src/Kyoo.Abstractions/Extensions.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Authentication.Models; namespace Kyoo.Authentication @@ -52,5 +53,13 @@ namespace Kyoo.Authentication return id; return null; } + + public static Guid GetIdOrThrow(this ClaimsPrincipal user) + { + Guid? ret = user.GetId(); + if (ret == null) + throw new UnauthorizedException(); + return ret.Value; + } } } diff --git a/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs b/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs new file mode 100644 index 00000000..fc4f5f08 --- /dev/null +++ b/back/src/Kyoo.Abstractions/Models/Exceptions/UnauthorizedException.cs @@ -0,0 +1,37 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.Runtime.Serialization; + +namespace Kyoo.Abstractions.Models.Exceptions +{ + [Serializable] + public class UnauthorizedException : Exception + { + public UnauthorizedException() { } + + public UnauthorizedException(string message) + : base(message) + { } + + protected UnauthorizedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + } +} diff --git a/back/src/Kyoo.Authentication/Views/AuthApi.cs b/back/src/Kyoo.Authentication/Views/AuthApi.cs index 8840f3cd..3652306c 100644 --- a/back/src/Kyoo.Authentication/Views/AuthApi.cs +++ b/back/src/Kyoo.Authentication/Views/AuthApi.cs @@ -197,11 +197,9 @@ namespace Kyoo.Authentication.Views [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> GetMe() { - if (!Guid.TryParse(User.FindFirstValue(Claims.Id), out Guid userID)) - return Unauthorized(new RequestError("User not authenticated or token invalid.")); try { - return await _users.Get(userID); + return await _users.Get(User.GetIdOrThrow()); } catch (ItemNotFoundException) { @@ -226,11 +224,9 @@ namespace Kyoo.Authentication.Views [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> EditMe(User user) { - if (!Guid.TryParse(User.FindFirstValue(Claims.Id), out Guid userID)) - return Unauthorized(new RequestError("User not authenticated or token invalid.")); try { - user.Id = userID; + user.Id = User.GetIdOrThrow(); return await _users.Edit(user); } catch (ItemNotFoundException) @@ -256,13 +252,12 @@ namespace Kyoo.Authentication.Views [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> PatchMe(PartialResource user) { - if (!Guid.TryParse(User.FindFirstValue(Claims.Id), out Guid userID)) - return Unauthorized(new RequestError("User not authenticated or token invalid.")); + Guid userId = User.GetIdOrThrow(); try { - if (user.Id.HasValue && user.Id != userID) + if (user.Id.HasValue && user.Id != userId) throw new ArgumentException("Can't edit your user id."); - return await _users.Patch(userID, TryUpdateModelAsync); + return await _users.Patch(userId, TryUpdateModelAsync); } catch (ItemNotFoundException) { @@ -286,11 +281,9 @@ namespace Kyoo.Authentication.Views [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> DeleteMe() { - if (!Guid.TryParse(User.FindFirstValue(Claims.Id), out Guid userID)) - return Unauthorized(new RequestError("User not authenticated or token invalid.")); try { - await _users.Delete(userID); + await _users.Delete(User.GetIdOrThrow()); return NoContent(); } catch (ItemNotFoundException) diff --git a/back/src/Kyoo.Core/ExceptionFilter.cs b/back/src/Kyoo.Core/ExceptionFilter.cs index 9a5bdb28..3e5ad9be 100644 --- a/back/src/Kyoo.Core/ExceptionFilter.cs +++ b/back/src/Kyoo.Core/ExceptionFilter.cs @@ -61,6 +61,9 @@ namespace Kyoo.Core // Should not happen but if it does, it is better than returning a 409 with no body since clients expect json content context.Result = new ConflictObjectResult(new RequestError("Duplicated item")); break; + case UnauthorizedException ex: + context.Result = new UnauthorizedObjectResult(new RequestError(ex.Message ?? "User not authenticated or token invalid.")); + break; case Exception ex: _logger.LogError(ex, "Unhandled error"); context.Result = new ServerErrorObjectResult(new RequestError("Internal Server Error")); diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index ed92e82d..ceb39f6d 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -122,7 +122,6 @@ namespace Kyoo.Core.Api /// This episode does not have a specific status. /// No episode with the given ID or slug could be found. [HttpGet("{identifier:id}/watchStatus")] - [HttpGet("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -133,7 +132,7 @@ namespace Kyoo.Core.Api id => Task.FromResult(id), async slug => (await _libraryManager.Episodes.Get(slug)).Id ); - return await _libraryManager.WatchStatus.GetEpisodeStatus(id, User.GetId()!.Value); + return await _libraryManager.WatchStatus.GetEpisodeStatus(id, User.GetIdOrThrow()); } /// @@ -150,7 +149,6 @@ namespace Kyoo.Core.Api /// The status was not considered impactfull enough to be saved (less then 5% of watched for example). /// No episode with the given ID or slug could be found. [HttpPost("{identifier:id}/watchStatus")] - [HttpPost("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -164,7 +162,7 @@ namespace Kyoo.Core.Api ); return await _libraryManager.WatchStatus.SetEpisodeStatus( id, - User.GetId()!.Value, + User.GetIdOrThrow(), status, watchedTime ); @@ -181,7 +179,6 @@ namespace Kyoo.Core.Api /// The status has been deleted. /// No episode with the given ID or slug could be found. [HttpDelete("{identifier:id}/watchStatus")] - [HttpDelete("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -191,7 +188,7 @@ namespace Kyoo.Core.Api id => Task.FromResult(id), async slug => (await _libraryManager.Episodes.Get(slug)).Id ); - await _libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetId()!.Value); + await _libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow()); } } } diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index d42226f3..52a117c2 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -163,7 +163,6 @@ namespace Kyoo.Core.Api /// This movie does not have a specific status. /// No movie with the given ID or slug could be found. [HttpGet("{identifier:id}/watchStatus")] - [HttpGet("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -174,7 +173,7 @@ namespace Kyoo.Core.Api id => Task.FromResult(id), async slug => (await _libraryManager.Movies.Get(slug)).Id ); - return await _libraryManager.WatchStatus.GetMovieStatus(id, User.GetId()!.Value); + return await _libraryManager.WatchStatus.GetMovieStatus(id, User.GetIdOrThrow()); } /// @@ -192,7 +191,6 @@ namespace Kyoo.Core.Api /// WatchedTime can't be specified if status is not watching. /// No movie with the given ID or slug could be found. [HttpPost("{identifier:id}/watchStatus")] - [HttpPost("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -206,7 +204,7 @@ namespace Kyoo.Core.Api ); return await _libraryManager.WatchStatus.SetMovieStatus( id, - User.GetId()!.Value, + User.GetIdOrThrow(), status, watchedTime ); @@ -223,7 +221,6 @@ namespace Kyoo.Core.Api /// The status has been deleted. /// No movie with the given ID or slug could be found. [HttpDelete("{identifier:id}/watchStatus")] - [HttpDelete("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -233,7 +230,7 @@ namespace Kyoo.Core.Api id => Task.FromResult(id), async slug => (await _libraryManager.Movies.Get(slug)).Id ); - await _libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetId()!.Value); + await _libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow()); } } } diff --git a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs index 46a03639..8859c8f2 100644 --- a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -240,7 +240,6 @@ namespace Kyoo.Core.Api /// This show does not have a specific status. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/watchStatus")] - [HttpGet("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -251,7 +250,7 @@ namespace Kyoo.Core.Api id => Task.FromResult(id), async slug => (await _libraryManager.Shows.Get(slug)).Id ); - return await _libraryManager.WatchStatus.GetShowStatus(id, User.GetId()!.Value); + return await _libraryManager.WatchStatus.GetShowStatus(id, User.GetIdOrThrow()); } /// @@ -267,7 +266,6 @@ namespace Kyoo.Core.Api /// The status was not considered impactfull enough to be saved (less then 5% of watched for example). /// No movie with the given ID or slug could be found. [HttpPost("{identifier:id}/watchStatus")] - [HttpPost("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -281,7 +279,7 @@ namespace Kyoo.Core.Api ); return await _libraryManager.WatchStatus.SetShowStatus( id, - User.GetId()!.Value, + User.GetIdOrThrow(), status ); } @@ -297,7 +295,6 @@ namespace Kyoo.Core.Api /// The status has been deleted. /// No show with the given ID or slug could be found. [HttpDelete("{identifier:id}/watchStatus")] - [HttpDelete("{identifier:id}/watchStatus", Order = AlternativeRoute)] [UserOnly] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -307,7 +304,7 @@ namespace Kyoo.Core.Api id => Task.FromResult(id), async slug => (await _libraryManager.Shows.Get(slug)).Id ); - await _libraryManager.WatchStatus.DeleteShowStatus(id, User.GetId()!.Value); + await _libraryManager.WatchStatus.DeleteShowStatus(id, User.GetIdOrThrow()); } } }