using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using IdentityServer4.Models; using IdentityServer4.Services; using Kyoo.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; namespace Kyoo.Api { public class RegisterRequest { public string Email { get; set; } public string Username { get; set; } public string Password { get; set; } } public class LoginRequest { public string Username { get; set; } public string Password { get; set; } public bool StayLoggedIn { get; set; } } public class OtacRequest { public string Otac { get; set; } public bool StayLoggedIn { get; set; } } public class AccountData { [FromForm(Name = "email")] public string Email { get; set; } [FromForm(Name = "username")] public string Username { get; set; } [FromForm(Name = "picture")] public IFormFile Picture { get; set; } } [ApiController] public class AccountUiController : Controller { [HttpGet("login")] public IActionResult Index() { return new PhysicalFileResult(Path.GetFullPath("login/login.html"), "text/html"); } [HttpGet("login/{*file}")] public IActionResult Index(string file) { string path = Path.Combine(Path.GetFullPath("login/"), file); if (!System.IO.File.Exists(path)) return NotFound(); FileExtensionContentTypeProvider provider = new FileExtensionContentTypeProvider(); if (!provider.TryGetContentType(path, out string contentType)) contentType = "text/plain"; return new PhysicalFileResult(path, contentType); } } [Route("api/[controller]")] [ApiController] public class AccountController : Controller, IProfileService { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IConfiguration _configuration; private readonly string _picturePath; public AccountController(UserManager userManager, SignInManager siginInManager, IConfiguration configuration) { _userManager = userManager; _signInManager = siginInManager; _picturePath = configuration.GetValue("profilePicturePath"); _configuration = configuration; if (!Path.IsPathRooted(_picturePath)) _picturePath = Path.GetFullPath(_picturePath); } [HttpPost("register")] public async Task Register([FromBody] RegisterRequest user) { if (!ModelState.IsValid) return BadRequest(user); if (user.Username.Length < 4) return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}}); if (!new EmailAddressAttribute().IsValid(user.Email)) return BadRequest(new[] {new {code = "email", description = "Email must be valid."}}); User account = new User {UserName = user.Username, Email = user.Email}; IdentityResult result = await _userManager.CreateAsync(account, user.Password); if (!result.Succeeded) return BadRequest(result.Errors); string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1)); await _userManager.UpdateAsync(account); await _userManager.AddClaimAsync(account, new Claim( "permissions", _configuration.GetValue("newUserPermissions"))); return Ok(new {otac}); } [HttpPost("login")] public async Task Login([FromBody] LoginRequest login) { if (!ModelState.IsValid) return BadRequest(login); SignInResult result = await _signInManager .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false); if (result.Succeeded) return Ok(); return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}}); } [HttpPost("otac-login")] public async Task OtacLogin([FromBody] OtacRequest otac) { if (!ModelState.IsValid) return BadRequest(otac); User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac); if (user == null) return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}}); if (user.OTACExpires <= DateTime.UtcNow) return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}}); await _signInManager.SignInAsync(user, otac.StayLoggedIn); return Ok(); } [HttpGet("logout")] [Authorize] public async Task Logout() { await _signInManager.SignOutAsync(); return Ok(); } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { User user = await _userManager.GetUserAsync(context.Subject); if (user != null) { List claims = new List { new Claim("email", user.Email), new Claim("username", user.UserName), new Claim("picture", $"api/account/picture/{user.UserName}") }; Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); if (perms != null) claims.Add(perms); context.IssuedClaims.AddRange(claims); } } public async Task IsActiveAsync(IsActiveContext context) { User user = await _userManager.GetUserAsync(context.Subject); context.IsActive = user != null; } [HttpGet("picture/{username}")] public async Task GetPicture(string username) { User user = await _userManager.FindByNameAsync(username); if (user == null) return BadRequest(); string path = Path.Combine(_picturePath, user.Id); if (!System.IO.File.Exists(path)) return NotFound(); return new PhysicalFileResult(path, "image/png"); } [HttpPost("update")] [Authorize] public async Task Update([FromForm] AccountData data) { User user = await _userManager.GetUserAsync(HttpContext.User); 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 = Path.Combine(_picturePath, user.Id); await using FileStream file = System.IO.File.Create(path); await data.Picture.CopyToAsync(file); } await _userManager.UpdateAsync(user); return Ok(); } [HttpGet("default-permissions")] public ActionResult> GetDefaultPermissions() { return _configuration.GetValue("defaultPermissions").Split(","); } } }