using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web; using API.Constants; using API.Data; using API.Entities; using API.Errors; using Kavita.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services; public interface IAccountService { Task> ChangeUserPassword(AppUser user, string newPassword); Task> ValidatePassword(AppUser user, string password); Task> ValidateUsername(string username); Task> ValidateEmail(string email); Task HasBookmarkPermission(AppUser? user); Task HasDownloadPermission(AppUser? user); Task HasChangeRestrictionRole(AppUser? user); Task CheckIfAccessible(HttpRequest request); Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true); } public class AccountService : IAccountService { private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly IHostEnvironment _environment; private readonly IEmailService _emailService; public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; private const string LocalHost = "localhost:4200"; public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, IHostEnvironment environment, IEmailService emailService) { _userManager = userManager; _logger = logger; _unitOfWork = unitOfWork; _environment = environment; _emailService = emailService; } /// /// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name. /// /// /// public async Task CheckIfAccessible(HttpRequest request) { var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host); } public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) { var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); var basePart = $"{request.Scheme}://{host}{request.PathBase}/"; if (!string.IsNullOrEmpty(serverSettings.HostName)) { basePart = serverSettings.HostName; if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) { basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - 1); } } if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}".Replace("//", "/"); return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}".Replace("//", "/"); } public async Task> ChangeUserPassword(AppUser user, string newPassword) { var passwordValidationIssues = (await ValidatePassword(user, newPassword)).ToList(); if (passwordValidationIssues.Any()) return passwordValidationIssues; var result = await _userManager.RemovePasswordAsync(user); if (!result.Succeeded) { _logger.LogError("Could not update password"); return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); } result = await _userManager.AddPasswordAsync(user, newPassword); if (!result.Succeeded) { _logger.LogError("Could not update password"); return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); } return new List(); } public async Task> ValidatePassword(AppUser user, string password) { foreach (var validator in _userManager.PasswordValidators) { var validationResult = await validator.ValidateAsync(_userManager, user, password); if (!validationResult.Succeeded) { return validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)); } } return Array.Empty(); } public async Task> ValidateUsername(string username) { if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == username.ToUpper())) { return new List() { new ApiException(400, "Username is already taken") }; } return Array.Empty(); } public async Task> ValidateEmail(string email) { var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); if (user == null) return Array.Empty(); return new List() { new ApiException(400, "Email is already registered") }; } /// /// Does the user have the Bookmark permission or admin rights /// /// /// public async Task HasBookmarkPermission(AppUser? user) { if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole); } /// /// Does the user have the Download permission or admin rights /// /// /// public async Task HasDownloadPermission(AppUser? user) { if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); } /// /// Does the user have Change Restriction permission or admin rights /// /// /// public async Task HasChangeRestrictionRole(AppUser? user) { if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole); } }