// 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"));
}
}
}
}