mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 04:04:21 -04:00
225 lines
7.0 KiB
C#
225 lines
7.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Claims;
|
|
using System.Threading.Tasks;
|
|
using IdentityServer4.Extensions;
|
|
using IdentityServer4.Models;
|
|
using IdentityServer4.Services;
|
|
using Kyoo.Abstractions.Controllers;
|
|
using Kyoo.Abstractions.Models;
|
|
using Kyoo.Abstractions.Models.Exceptions;
|
|
using Kyoo.Authentication.Models;
|
|
using Kyoo.Authentication.Models.DTO;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Kyoo.Authentication.Views
|
|
{
|
|
/// <summary>
|
|
/// The class responsible for login, logout, permissions and claims of a user.
|
|
/// </summary>
|
|
[Route("api/account")]
|
|
[Route("api/accounts")]
|
|
[ApiController]
|
|
public class AccountApi : Controller, IProfileService
|
|
{
|
|
/// <summary>
|
|
/// The repository to handle users.
|
|
/// </summary>
|
|
private readonly IUserRepository _users;
|
|
/// <summary>
|
|
/// A file manager to send profile pictures
|
|
/// </summary>
|
|
private readonly IFileSystem _files;
|
|
/// <summary>
|
|
/// Options about authentication. Those options are monitored and reloads are supported.
|
|
/// </summary>
|
|
private readonly IOptions<AuthenticationOption> _options;
|
|
|
|
|
|
/// <summary>
|
|
/// Create a new <see cref="AccountApi"/> handle to handle login/users requests.
|
|
/// </summary>
|
|
/// <param name="users">The user repository to create and manage users</param>
|
|
/// <param name="files">A file manager to send profile pictures</param>
|
|
/// <param name="options">Authentication options (this may be hot reloaded)</param>
|
|
public AccountApi(IUserRepository users,
|
|
IFileSystem files,
|
|
IOptions<AuthenticationOption> options)
|
|
{
|
|
_users = users;
|
|
_files = files;
|
|
_options = options;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Register a new user and return a OTAC to connect to it.
|
|
/// </summary>
|
|
/// <param name="request">The DTO register request</param>
|
|
/// <returns>A OTAC to connect to this new account</returns>
|
|
[HttpPost("register")]
|
|
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
|
|
{
|
|
User user = request.ToUser();
|
|
user.Permissions = _options.Value.Permissions.NewUser;
|
|
user.Password = PasswordUtils.HashPassword(user.Password);
|
|
user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
|
|
user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
|
|
try
|
|
{
|
|
await _users.Create(user);
|
|
}
|
|
catch (DuplicatedItemException)
|
|
{
|
|
return Conflict(new {Errors = new {Duplicate = new[] {"A user with this name already exists"}}});
|
|
}
|
|
|
|
return Ok(new {Otac = user.ExtraData["otac"]});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return an authentication properties based on a stay login property
|
|
/// </summary>
|
|
/// <param name="stayLogged">Should the user stay logged</param>
|
|
/// <returns>Authentication properties based on a stay login</returns>
|
|
private static AuthenticationProperties StayLogged(bool stayLogged)
|
|
{
|
|
if (!stayLogged)
|
|
return null;
|
|
return new AuthenticationProperties
|
|
{
|
|
IsPersistent = true,
|
|
ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Login the user.
|
|
/// </summary>
|
|
/// <param name="login">The DTO login request</param>
|
|
[HttpPost("login")]
|
|
public async Task<IActionResult> Login([FromBody] LoginRequest login)
|
|
{
|
|
User user = await _users.GetOrDefault(x => x.Username == login.Username);
|
|
|
|
if (user == null)
|
|
return Unauthorized();
|
|
if (!PasswordUtils.CheckPassword(login.Password, user.Password))
|
|
return Unauthorized();
|
|
|
|
await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(login.StayLoggedIn));
|
|
return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use a OTAC to login a user.
|
|
/// </summary>
|
|
/// <param name="otac">The OTAC request</param>
|
|
[HttpPost("otac-login")]
|
|
public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
|
|
{
|
|
// TODO once hstore (Dictionary<string, string> accessor) are supported, use them.
|
|
// We retrieve all users, this is inefficient.
|
|
User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac);
|
|
if (user == null)
|
|
return Unauthorized();
|
|
if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <=
|
|
DateTime.UtcNow)
|
|
{
|
|
return BadRequest(new
|
|
{
|
|
code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."
|
|
});
|
|
}
|
|
|
|
await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(otac.StayLoggedIn));
|
|
return Ok();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sign out an user
|
|
/// </summary>
|
|
[HttpGet("logout")]
|
|
[Authorize]
|
|
public async Task<IActionResult> Logout()
|
|
{
|
|
await HttpContext.SignOutAsync();
|
|
return Ok();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
|
{
|
|
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
|
|
if (user == null)
|
|
return;
|
|
context.IssuedClaims.AddRange(user.GetClaims());
|
|
context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions)));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task IsActiveAsync(IsActiveContext context)
|
|
{
|
|
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
|
|
context.IsActive = user != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the user's profile picture.
|
|
/// </summary>
|
|
/// <param name="slug">The user slug</param>
|
|
/// <returns>The profile picture of the user or 404 if not found</returns>
|
|
[HttpGet("picture/{slug}")]
|
|
public async Task<IActionResult> GetPicture(string slug)
|
|
{
|
|
User user = await _users.GetOrDefault(slug);
|
|
if (user == null)
|
|
return NotFound();
|
|
string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
|
|
return _files.FileResult(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update profile information (email, username, profile picture...)
|
|
/// </summary>
|
|
/// <param name="data">The new information</param>
|
|
/// <returns>The edited user</returns>
|
|
[HttpPut]
|
|
[Authorize]
|
|
public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
|
|
{
|
|
User user = await _users.GetOrDefault(int.Parse(HttpContext.User.GetSubjectId()));
|
|
|
|
if (user == null)
|
|
return Unauthorized();
|
|
if (!string.IsNullOrEmpty(data.Email))
|
|
user.Email = data.Email;
|
|
if (!string.IsNullOrEmpty(data.Username))
|
|
user.Username = data.Username;
|
|
if (data.Picture?.Length > 0)
|
|
{
|
|
string path = _files.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
|
|
await using Stream file = await _files.NewFile(path);
|
|
await data.Picture.CopyToAsync(file);
|
|
}
|
|
return await _users.Edit(user, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get permissions for a non connected user.
|
|
/// </summary>
|
|
/// <returns>The list of permissions of a default user.</returns>
|
|
[HttpGet("permissions")]
|
|
public ActionResult<IEnumerable<string>> GetDefaultPermissions()
|
|
{
|
|
return _options.Value.Permissions.Default ?? Array.Empty<string>();
|
|
}
|
|
}
|
|
} |