// 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.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Authentication.Models; using Kyoo.Authentication.Models.DTO; using Kyoo.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using static Kyoo.Abstractions.Models.Utils.Constants; using BCryptNet = BCrypt.Net.BCrypt; namespace Kyoo.Authentication.Views { /// /// Sign in, Sign up or refresh tokens. /// [ApiController] [Route("auth")] [ApiDefinition("Authentication", Group = UsersGroup)] public class AuthApi : ControllerBase { /// /// The repository to handle users. /// private readonly IRepository _users; /// /// The token generator. /// private readonly ITokenController _token; /// /// The permisson options. /// private readonly PermissionOption _permissions; /// /// Create a new . /// /// The repository used to check if the user exists. /// The token generator. /// The permission opitons. public AuthApi( IRepository users, ITokenController token, PermissionOption permissions ) { _users = users; _token = token; _permissions = permissions; } /// /// Create a new Forbidden result from an object. /// /// The json value to output on the response. /// A new forbidden result with the given json object. public static ObjectResult Forbid(object value) { return new ObjectResult(value) { StatusCode = StatusCodes.Status403Forbidden }; } /// /// Login. /// /// /// Login as a user and retrieve an access and a refresh token. /// /// The body of the request. /// A new access and a refresh token. /// The user and password does not match. [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> Login([FromBody] LoginRequest request) { User? user = await _users.GetOrDefault( new Filter.Eq(nameof(Abstractions.Models.User.Username), request.Username) ); if (user == null || !BCryptNet.Verify(request.Password, user.Password)) return Forbid(new RequestError("The user and password does not match.")); return new JwtToken( _token.CreateAccessToken(user, out TimeSpan expireIn), await _token.CreateRefreshToken(user), expireIn ); } /// /// Register. /// /// /// Register a new user and get a new access/refresh token for this new user. /// /// The body of the request. /// A new access and a refresh token. /// The request is invalid. /// A user already exists with this username or email address. [HttpPost("register")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))] public async Task> Register([FromBody] RegisterRequest request) { User user = request.ToUser(); user.Permissions = _permissions.NewUser; // If no users exists, the new one will be an admin. Give it every permissions. if ((await _users.GetAll(limit: new Pagination(1))).Any()) user.Permissions = PermissionOption.Admin; try { await _users.Create(user); } catch (DuplicatedItemException) { return Conflict(new RequestError("A user already exists with this username.")); } return new JwtToken( _token.CreateAccessToken(user, out TimeSpan expireIn), await _token.CreateRefreshToken(user), expireIn ); } /// /// Refresh a token. /// /// /// Refresh an access token using the given refresh token. A new access and refresh token are generated. /// /// A valid refresh token. /// A new access and refresh token. /// The given refresh token is invalid. [HttpGet("refresh")] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> Refresh([FromQuery] string token) { try { Guid userId = _token.GetRefreshTokenUserID(token); User user = await _users.Get(userId); return new JwtToken( _token.CreateAccessToken(user, out TimeSpan expireIn), await _token.CreateRefreshToken(user), expireIn ); } catch (ItemNotFoundException) { return Forbid(new RequestError("Invalid refresh token.")); } catch (SecurityTokenException ex) { return Forbid(new RequestError(ex.Message)); } } /// /// Get authenticated user. /// /// /// Get information about the currently authenticated user. This can also be used to ensure that you are /// logged in. /// /// The currently authenticated user. /// The user is not authenticated. /// The given access token is invalid. [HttpGet("me")] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> GetMe() { try { return await _users.Get(User.GetIdOrThrow()); } catch (ItemNotFoundException) { return Forbid(new RequestError("Invalid token")); } } /// /// Edit self /// /// /// Edit information about the currently authenticated user. /// /// The new data for the current user. /// The currently authenticated user after modifications. /// The user is not authenticated. /// The given access token is invalid. [HttpPut("me")] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> EditMe(User user) { try { user.Id = User.GetIdOrThrow(); return await _users.Edit(user); } catch (ItemNotFoundException) { return Forbid(new RequestError("Invalid token")); } } /// /// Patch self /// /// /// Edit only provided informations about the currently authenticated user. /// /// The new data for the current user. /// The currently authenticated user after modifications. /// The user is not authenticated. /// The given access token is invalid. [HttpPatch("me")] [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> PatchMe(PartialResource user) { Guid userId = User.GetIdOrThrow(); try { if (user.Id.HasValue && user.Id != userId) throw new ArgumentException("Can't edit your user id."); return await _users.Patch(userId, TryUpdateModelAsync); } catch (ItemNotFoundException) { return Forbid(new RequestError("Invalid token")); } } /// /// Delete account /// /// /// Delete the current account. /// /// The currently authenticated user after modifications. /// The user is not authenticated. /// The given access token is invalid. [HttpDelete("me")] [UserOnly] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> DeleteMe() { try { await _users.Delete(User.GetIdOrThrow()); return NoContent(); } catch (ItemNotFoundException) { return Forbid(new RequestError("Invalid token")); } } } }