mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
* adding back side-nav * Event Widget Update (#1098) * Took care of some notes in the code * Fixed an issue where Extra might get flagged as special too early, if in a word like Extraordinary * Moved Tag cleanup code into Scanner service. Added a SplitQuery to another heavy API. Refactored Scan loop to remove parallelism and use async instead. * Lots of rework on the codebase to support detailed messages and easier management of message sending. Need to take a break on this work. * Progress is being made, but slowly. Code is broken in this commit. * Progress is being made, but slowly. Code is broken in this commit. * Fixed merge issue * Fixed unit tests * CoverUpdate is now hooked into new ProgressEvent structure * Refactored code to remove custom observables and have everything use standard messages$ * Refactored a ton of instances to NotificationProgressEvent style and tons of the UI to respect that too. UI is still a bit buggy, but wholistically the work is done. * Working much better. Sometimes events come in too fast. Currently cover update progress doesn't display on UI * Fixed unit tests * Removed SignalREvent to minimize internal event types. Updated the UI to use progress bars. Finished SiteThemeService. * Merged metadata refresh progress events and changed library scan events to merge cleaner in the UI * Changed RefreshMetadataProgress to CoverUpdateProgress to reflect the event better. * Theme Cleanup (#1089) * Fixed e-ink theme not properly applying correctly * Fixed some seed changes. Changed card checkboxes to use our themed ones * Fixed recently added carousel not going to recently-added page * Fixed an issue where no results found would show when searching for a library name * Cleaned up list a bit, typeahead dropdown still needs work * Added a TODO to streamline series-card component * Removed ng-lazyload-image module since we don't use it. We use lazysizes * Darken card on hover * Fixing accordion focus style * ux pass updates - Fixed typeahead width - Fixed changelog download buttons - Fixed a select - Fixed various input box-shadows - Fixed all anchors to only have underline on hover - Added navtab hover and active effects * more ux pass - Fixed spacing on theme cards - Fixed some light theme issues - Exposed text-muted-color for theme card subtitle color * UX pass fixes - Changed back to bright green for primary on dark theme - Changed fa icon to black on e-ink * Merged changelog component * Fixed anchor buttons text decoration * Changed nav tabs to have a background color instead of open active state * When user is not authenticated, make sure we set default theme (dark) * Cleanup on carousel * Updated Users tab to use small buttons with icons to align with Library tab * Cleaned up brand to not underline, removed default link underline on hover in dropdown and pill tabs * Fixed collection detail posters not rendering Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Tweaked some of the emitting code * Some css, but pretty bad. Robbie please save me * Removed a todo * styling update * Only send filename on FileScanProgress * Some console.log spam cleanup * Various updates * Show events widget activity based on activeEvents * progress bar color updates * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Scanner event hub fix (#1099) * Scanner event hub fix - Fixed an issue where the scanner would error when adding a new series because the series didn't have a library name yet. (develop) * Removing library.type * Bump versions by dotnet-bump-version. * Workflow update to add nightly versions (#1100) # Changed - Changed: Changed automated workflow to release individual nightly versions on dockerhub * Bump versions by dotnet-bump-version. * Updating GA to parse version (#1101) * Bump versions by dotnet-bump-version. * GA Fixes (#1103) **Strictly Repo Changes** # Fixed - Fixed: Fixed an issue where patch version was not being added to docker tag. * Bump versions by dotnet-bump-version. * Fixed specials being misaligned (#1106) # Fixed - Fixed: Fixed issue with specials not being properly aligned (develop) * Bump versions by dotnet-bump-version. * Bugfix/ux pass 2 (#1107) * Adding margin bottom to series detail tabs * Styling tag badges with green on dark - Added 3 new css vars * Removing underline from readmore * Fixing see more to be on one line * adding gutter to see more * Changing queue toasts to info * adding api key tooltip * Updating active accordion on user preference. * Fixing search bar and close btn position * Fixed a bug where entering book reader in dark mode then closing out, would leave you in a broken white state. * Fixed broken wiki links Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * Series Detail Refactor (#1118) * Fixed a bug where reading list and collection's summary wouldn't render newlines * Moved all the logic in the UI for Series Detail into the backend (messy code). We are averaging 400ms max with much optimizations available. Next step is to refactor out of controller and provide unit tests. * Unit tests for CleanSpecialTitle * Laid out foundation for testing major code in SeriesController. * Refactored code so that read doesn't need to be disabled on page load. SeriesId doesn't need the series to actually load. * Removed old property from Volume * Changed tagbadge font size to rem. * Refactored some methods from SeriesController.cs into SeriesService.cs * UpdateRating unit tested * Wrote unit tests for SeriesDetail * Worked up some code where books are rendered only as volumes. However, looks like I will need to use Chapters to better support series_index as floats. * Refactored Series Detail to change Volume Name on Book libraries to have book name and series_index. * Some cleanup on the code * DeleteMultipleSeries test is hard. Going to skip. * Removed some debug code and make all tabs Books for Book library Type * Bump versions by dotnet-bump-version. * Tachiyomi Bugfix (#1119) * Updated the dependencies for .NET 6.0.2 * Fixed a bad prev chapter logic where we would bleed into chapters from last volume instead of specials. * Fixed the get prev chapter code to properly walk the order according to documentation and updated some bad test cases * Updated side nav to float a bit and added user settings to it. * Refactored the code to hide/show sidenav to be more angular and decoupled * Moved Changelog out of admin dashboard and into a dedicated page in user menu. Added a wiki link from user menu * Introduced a side nav item for rendering each item and refactored code to use it. * Added a filter of side nav when there are more than 10 libraries. Added some themeing overrides for side nav. * Cleaned up the template code for side nav item so if there is no link, we don't generate that html directive * Refactored side nav into a module and migrated a few pipes into a pipe module for easy re-use * Added companion bar on reading list and collection. Updated modules to load pages and make side nav items clickable as anchors, so new tab works. * Moved metadata filter into separate component/module and the button in the companion bar. Needs cleanup. * Finished cleanup and refactoring of metadata filter into separate component. Removed filtering from Collections as it doesn't work and wasn't hooked up. * Tweaked the css on carousel component * Added to library detail and series-detail * Fixes and css vars * Stop destroying sidenav, animaton timing * Integrated side nav on the rest of the pages * Navbar now collapses to icons * mobile sidenav start * more mobile fixes * mobile tweaks * light and e-ink theme updates * white and eink dropdown color fixes * plex inspired side-nav * theme fixes * Making spacing more uniform across app * More fixes * fixing spacing on cards * actionable fix for sidenav * no scroll on mobile when sidenav is open * hide sidenav on pages * Adding card spacing * Adding ability to remove sidenav when in a reader * tidying up sidenav toggles * side-nav mobile updates * fixing up other themes * overlay fixes * Cleaned up the code to make the observables have better names. Removed a bunch of pointless subscriptions. Cleaned up methods that werent needed. Added jsdocs to help ensure the understandability of the 2 states for the side nav. * Integrated a highlight effect on side nav. Fixed a ton of places where the nav was being hidden when it shouldn't. * Fixed where active state wasn't working on all urls * misc fixes - smaller hamburger - z-index fixes - active fixes * Revert "Merge branch 'develop' into feature/side-nav-upgrade" This reverts commit 76b0d15a984692874e0cb57e821686ea703144cf, reversing changes made to b3ed55395473aa35577500596a211ad22a42631b. * Fixing edit-series modal spacing * Give the ability to jump to a library from admin manage libraries page * Fixed a bug with highlighting active item on side nav * Moved localized series title to companion bar via subtitle * Removed old title * Fixed a bug where clicking a link would reload the whole app, styling fixes on filter, fixed issue with initial load not setting active state, adjusted styles on active style. * code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
674 lines
28 KiB
C#
674 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
using API.Constants;
|
|
using API.Data;
|
|
using API.Data.Repositories;
|
|
using API.DTOs;
|
|
using API.DTOs.Account;
|
|
using API.DTOs.Email;
|
|
using API.Entities;
|
|
using API.Entities.Enums;
|
|
using API.Errors;
|
|
using API.Extensions;
|
|
using API.Services;
|
|
using AutoMapper;
|
|
using Kavita.Common;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Controllers
|
|
{
|
|
/// <summary>
|
|
/// All Account matters
|
|
/// </summary>
|
|
public class AccountController : BaseApiController
|
|
{
|
|
private readonly UserManager<AppUser> _userManager;
|
|
private readonly SignInManager<AppUser> _signInManager;
|
|
private readonly ITokenService _tokenService;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ILogger<AccountController> _logger;
|
|
private readonly IMapper _mapper;
|
|
private readonly IAccountService _accountService;
|
|
private readonly IEmailService _emailService;
|
|
private readonly IHostEnvironment _environment;
|
|
|
|
/// <inheritdoc />
|
|
public AccountController(UserManager<AppUser> userManager,
|
|
SignInManager<AppUser> signInManager,
|
|
ITokenService tokenService, IUnitOfWork unitOfWork,
|
|
ILogger<AccountController> logger,
|
|
IMapper mapper, IAccountService accountService, IEmailService emailService, IHostEnvironment environment)
|
|
{
|
|
_userManager = userManager;
|
|
_signInManager = signInManager;
|
|
_tokenService = tokenService;
|
|
_unitOfWork = unitOfWork;
|
|
_logger = logger;
|
|
_mapper = mapper;
|
|
_accountService = accountService;
|
|
_emailService = emailService;
|
|
_environment = environment;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update a user's password
|
|
/// </summary>
|
|
/// <param name="resetPasswordDto"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("reset-password")]
|
|
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
|
{
|
|
_logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName);
|
|
var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName);
|
|
|
|
if (resetPasswordDto.UserName != User.GetUsername() && !(User.IsInRole(PolicyConstants.AdminRole) || User.IsInRole(PolicyConstants.ChangePasswordRole)))
|
|
return Unauthorized("You are not permitted to this operation.");
|
|
|
|
var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
|
|
if (errors.Any())
|
|
{
|
|
return BadRequest(errors);
|
|
}
|
|
|
|
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
|
return Ok();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register the first user (admin) on the server. Will not do anything if an admin is already confirmed
|
|
/// </summary>
|
|
/// <param name="registerDto"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("register")]
|
|
public async Task<ActionResult<UserDto>> RegisterFirstUser(RegisterDto registerDto)
|
|
{
|
|
var admins = await _userManager.GetUsersInRoleAsync("Admin");
|
|
if (admins.Count > 0) return BadRequest("Not allowed");
|
|
|
|
try
|
|
{
|
|
var usernameValidation = await _accountService.ValidateUsername(registerDto.Username);
|
|
if (usernameValidation.Any())
|
|
{
|
|
return BadRequest(usernameValidation);
|
|
}
|
|
|
|
var user = new AppUser()
|
|
{
|
|
UserName = registerDto.Username,
|
|
Email = registerDto.Email,
|
|
UserPreferences = new AppUserPreferences
|
|
{
|
|
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
|
},
|
|
ApiKey = HashUtil.ApiKey()
|
|
};
|
|
|
|
var result = await _userManager.CreateAsync(user, registerDto.Password);
|
|
if (!result.Succeeded) return BadRequest(result.Errors);
|
|
|
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
|
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue generating a confirmation token.");
|
|
if (!await ConfirmEmailToken(token, user)) return BadRequest($"There was an issue validating your email: {token}");
|
|
|
|
|
|
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
|
|
if (!roleResult.Succeeded) return BadRequest(result.Errors);
|
|
|
|
return new UserDto
|
|
{
|
|
Username = user.UserName,
|
|
Email = user.Email,
|
|
Token = await _tokenService.CreateToken(user),
|
|
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
|
ApiKey = user.ApiKey,
|
|
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Something went wrong when registering user");
|
|
await _unitOfWork.RollbackAsync();
|
|
}
|
|
|
|
return BadRequest("Something went wrong when registering user");
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Perform a login. Will send JWT Token of the logged in user back.
|
|
/// </summary>
|
|
/// <param name="loginDto"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("login")]
|
|
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
|
{
|
|
var user = await _userManager.Users
|
|
.Include(u => u.UserPreferences)
|
|
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
|
|
|
if (user == null) return Unauthorized("Invalid username");
|
|
|
|
// Check if the user has an email, if not, inform them so they can migrate
|
|
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, loginDto.Password);
|
|
if (string.IsNullOrEmpty(user.Email) && !user.EmailConfirmed && validPassword)
|
|
{
|
|
_logger.LogCritical("User {UserName} does not have an email. Providing a one time migration", user.UserName);
|
|
return Unauthorized(
|
|
"You are missing an email on your account. Please wait while we migrate your account.");
|
|
}
|
|
|
|
if (!validPassword)
|
|
{
|
|
return Unauthorized("Your credentials are not correct");
|
|
}
|
|
|
|
var result = await _signInManager
|
|
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
|
|
|
if (!result.Succeeded)
|
|
{
|
|
return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct.");
|
|
}
|
|
|
|
// Update LastActive on account
|
|
user.LastActive = DateTime.Now;
|
|
user.UserPreferences ??= new AppUserPreferences
|
|
{
|
|
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
|
};
|
|
|
|
_unitOfWork.UserRepository.Update(user);
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
|
|
|
var dto = _mapper.Map<UserDto>(user);
|
|
dto.Token = await _tokenService.CreateToken(user);
|
|
dto.RefreshToken = await _tokenService.CreateRefreshToken(user);
|
|
var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName);
|
|
pref.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
|
dto.Preferences = _mapper.Map<UserPreferencesDto>(pref);
|
|
return dto;
|
|
}
|
|
|
|
[HttpPost("refresh-token")]
|
|
public async Task<ActionResult<TokenRequestDto>> RefreshToken([FromBody] TokenRequestDto tokenRequestDto)
|
|
{
|
|
var token = await _tokenService.ValidateRefreshToken(tokenRequestDto);
|
|
if (token == null)
|
|
{
|
|
return Unauthorized(new { message = "Invalid token" });
|
|
}
|
|
|
|
return Ok(token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get All Roles back. See <see cref="PolicyConstants"/>
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[HttpGet("roles")]
|
|
public ActionResult<IList<string>> GetRoles()
|
|
{
|
|
return typeof(PolicyConstants)
|
|
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
|
.Where(f => f.FieldType == typeof(string))
|
|
.ToDictionary(f => f.Name,
|
|
f => (string) f.GetValue(null)).Values.ToList();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Resets the API Key assigned with a user
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[HttpPost("reset-api-key")]
|
|
public async Task<ActionResult<string>> ResetApiKey()
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
|
|
user.ApiKey = HashUtil.ApiKey();
|
|
|
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok(user.ApiKey);
|
|
}
|
|
|
|
await _unitOfWork.RollbackAsync();
|
|
return BadRequest("Something went wrong, unable to reset key");
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the user account. This can only affect Username, Email (will require confirming), Roles, and Library access.
|
|
/// </summary>
|
|
/// <param name="dto"></param>
|
|
/// <returns></returns>
|
|
[Authorize(Policy = "RequireAdminRole")]
|
|
[HttpPost("update")]
|
|
public async Task<ActionResult> UpdateAccount(UpdateUserDto dto)
|
|
{
|
|
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized("You do not have permission");
|
|
|
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId);
|
|
if (user == null) return BadRequest("User does not exist");
|
|
|
|
// Check if username is changing
|
|
if (!user.UserName.Equals(dto.Username))
|
|
{
|
|
// Validate username change
|
|
var errors = await _accountService.ValidateUsername(dto.Username);
|
|
if (errors.Any()) return BadRequest("Username already taken");
|
|
user.UserName = dto.Username;
|
|
_unitOfWork.UserRepository.Update(user);
|
|
}
|
|
|
|
if (!user.Email.Equals(dto.Email))
|
|
{
|
|
// Validate username change
|
|
var errors = await _accountService.ValidateEmail(dto.Email);
|
|
if (errors.Any()) return BadRequest("Email already registered");
|
|
// NOTE: This needs to be handled differently, like save it in a temp variable in DB until email is validated. For now, I wont allow it
|
|
}
|
|
|
|
// Update roles
|
|
var existingRoles = await _userManager.GetRolesAsync(user);
|
|
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
|
|
if (!hasAdminRole)
|
|
{
|
|
dto.Roles.Add(PolicyConstants.PlebRole);
|
|
}
|
|
if (existingRoles.Except(dto.Roles).Any() || dto.Roles.Except(existingRoles).Any())
|
|
{
|
|
var roles = dto.Roles;
|
|
|
|
var roleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles);
|
|
if (!roleResult.Succeeded) return BadRequest(roleResult.Errors);
|
|
roleResult = await _userManager.AddToRolesAsync(user, roles);
|
|
if (!roleResult.Succeeded) return BadRequest(roleResult.Errors);
|
|
}
|
|
|
|
|
|
var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
|
List<Library> libraries;
|
|
if (hasAdminRole)
|
|
{
|
|
_logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries",
|
|
user.UserName);
|
|
libraries = allLibraries;
|
|
}
|
|
else
|
|
{
|
|
// Remove user from all libraries
|
|
foreach (var lib in allLibraries)
|
|
{
|
|
lib.AppUsers ??= new List<AppUser>();
|
|
lib.AppUsers.Remove(user);
|
|
}
|
|
|
|
libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries)).ToList();
|
|
}
|
|
|
|
foreach (var lib in libraries)
|
|
{
|
|
lib.AppUsers ??= new List<AppUser>();
|
|
lib.AppUsers.Add(user);
|
|
}
|
|
|
|
if (!_unitOfWork.HasChanges()) return Ok();
|
|
if (await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok();
|
|
}
|
|
|
|
await _unitOfWork.RollbackAsync();
|
|
return BadRequest("There was an exception when updating the user");
|
|
}
|
|
|
|
|
|
|
|
[Authorize(Policy = "RequireAdminRole")]
|
|
[HttpPost("invite")]
|
|
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
|
{
|
|
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
if (adminUser == null) return Unauthorized("You need to login");
|
|
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
|
|
|
// Check if there is an existing invite
|
|
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
|
if (emailValidationErrors.Any())
|
|
{
|
|
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
|
if (await _userManager.IsEmailConfirmedAsync(invitedUser))
|
|
return BadRequest($"User is already registered as {invitedUser.UserName}");
|
|
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
|
}
|
|
|
|
// Create a new user
|
|
var user = new AppUser()
|
|
{
|
|
UserName = dto.Email,
|
|
Email = dto.Email,
|
|
ApiKey = HashUtil.ApiKey(),
|
|
UserPreferences = new AppUserPreferences
|
|
{
|
|
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
|
}
|
|
};
|
|
|
|
try
|
|
{
|
|
var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword);
|
|
if (!result.Succeeded) return BadRequest(result.Errors);
|
|
|
|
// Assign Roles
|
|
var roles = dto.Roles;
|
|
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
|
|
if (!hasAdminRole)
|
|
{
|
|
roles.Add(PolicyConstants.PlebRole);
|
|
}
|
|
|
|
foreach (var role in roles)
|
|
{
|
|
if (!PolicyConstants.ValidRoles.Contains(role)) continue;
|
|
var roleResult = await _userManager.AddToRoleAsync(user, role);
|
|
if (!roleResult.Succeeded)
|
|
return
|
|
BadRequest(roleResult.Errors);
|
|
}
|
|
|
|
// Grant access to libraries
|
|
List<Library> libraries;
|
|
if (hasAdminRole)
|
|
{
|
|
_logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries",
|
|
user.UserName);
|
|
libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
|
}
|
|
else
|
|
{
|
|
libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries)).ToList();
|
|
}
|
|
|
|
foreach (var lib in libraries)
|
|
{
|
|
lib.AppUsers ??= new List<AppUser>();
|
|
lib.AppUsers.Add(user);
|
|
}
|
|
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
|
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
|
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue sending email");
|
|
|
|
var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email);
|
|
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
|
if (dto.SendEmail)
|
|
{
|
|
await _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
|
{
|
|
EmailAddress = dto.Email,
|
|
InvitingUser = adminUser.UserName,
|
|
ServerConfirmationLink = emailLink
|
|
});
|
|
}
|
|
return Ok(emailLink);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
_unitOfWork.UserRepository.Delete(user);
|
|
await _unitOfWork.CommitAsync();
|
|
}
|
|
|
|
return BadRequest("There was an error setting up your account. Please check the logs");
|
|
}
|
|
|
|
[HttpPost("confirm-email")]
|
|
public async Task<ActionResult<UserDto>> ConfirmEmail(ConfirmEmailDto dto)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
|
|
|
// Validate Password and Username
|
|
var validationErrors = new List<ApiException>();
|
|
validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username));
|
|
validationErrors.AddRange(await _accountService.ValidatePassword(user, dto.Password));
|
|
|
|
if (validationErrors.Any())
|
|
{
|
|
return BadRequest(validationErrors);
|
|
}
|
|
|
|
|
|
if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
|
|
|
|
user.UserName = dto.Username;
|
|
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
|
|
if (errors.Any())
|
|
{
|
|
return BadRequest(errors);
|
|
}
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
|
|
user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName,
|
|
AppUserIncludes.UserPreferences);
|
|
|
|
// Perform Login code
|
|
return new UserDto
|
|
{
|
|
Username = user.UserName,
|
|
Email = user.Email,
|
|
Token = await _tokenService.CreateToken(user),
|
|
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
|
ApiKey = user.ApiKey,
|
|
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
|
};
|
|
}
|
|
|
|
[AllowAnonymous]
|
|
[HttpPost("confirm-password-reset")]
|
|
public async Task<ActionResult<string>> ConfirmForgotPassword(ConfirmPasswordResetDto dto)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
|
if (user == null)
|
|
{
|
|
return BadRequest("Invalid Details");
|
|
}
|
|
|
|
var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", dto.Token);
|
|
if (!result) return BadRequest("Unable to reset password, your email token is not correct.");
|
|
|
|
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
|
|
return errors.Any() ? BadRequest(errors) : Ok("Password updated");
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Will send user a link to update their password to their email or prompt them if not accessible
|
|
/// </summary>
|
|
/// <param name="email"></param>
|
|
/// <returns></returns>
|
|
[AllowAnonymous]
|
|
[HttpPost("forgot-password")]
|
|
public async Task<ActionResult<string>> ForgotPassword([FromQuery] string email)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
|
|
if (user == null)
|
|
{
|
|
_logger.LogError("There are no users with email: {Email} but user is requesting password reset", email);
|
|
return Ok("An email will be sent to the email if it exists in our database");
|
|
}
|
|
|
|
var emailLink = GenerateEmailLink(await _userManager.GeneratePasswordResetTokenAsync(user), "confirm-reset-password", user.Email);
|
|
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
|
if (await _emailService.CheckIfAccessible(host))
|
|
{
|
|
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
|
{
|
|
EmailAddress = user.Email,
|
|
ServerConfirmationLink = emailLink,
|
|
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
|
});
|
|
return Ok("Email sent");
|
|
}
|
|
|
|
return Ok("Your server is not accessible. The Link to reset your password is in the logs.");
|
|
}
|
|
|
|
[AllowAnonymous]
|
|
[HttpPost("confirm-migration-email")]
|
|
public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
|
if (user == null) return BadRequest("This email is not on system");
|
|
|
|
if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
|
|
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName,
|
|
AppUserIncludes.UserPreferences);
|
|
|
|
// Perform Login code
|
|
return new UserDto
|
|
{
|
|
Username = user.UserName,
|
|
Email = user.Email,
|
|
Token = await _tokenService.CreateToken(user),
|
|
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
|
ApiKey = user.ApiKey,
|
|
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
|
};
|
|
}
|
|
|
|
[HttpPost("resend-confirmation-email")]
|
|
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
|
if (user == null) return BadRequest("User does not exist");
|
|
|
|
if (string.IsNullOrEmpty(user.Email))
|
|
return BadRequest(
|
|
"This user needs to migrate. Have them log out and login to trigger a migration flow");
|
|
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
|
|
|
var emailLink = GenerateEmailLink(await _userManager.GenerateEmailConfirmationTokenAsync(user), "confirm-email", user.Email);
|
|
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
|
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
|
{
|
|
EmailAddress = user.Email,
|
|
Username = user.UserName,
|
|
ServerConfirmationLink = emailLink,
|
|
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
|
});
|
|
|
|
|
|
return Ok(emailLink);
|
|
}
|
|
|
|
private string GenerateEmailLink(string token, string routePart, string email)
|
|
{
|
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
|
var emailLink =
|
|
$"{Request.Scheme}://{host}{Request.PathBase}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
|
return emailLink;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is similar to invite. Essentially we authenticate the user's password then go through invite email flow
|
|
/// </summary>
|
|
/// <param name="dto"></param>
|
|
/// <returns></returns>
|
|
[AllowAnonymous]
|
|
[HttpPost("migrate-email")]
|
|
public async Task<ActionResult<string>> MigrateEmail(MigrateUserEmailDto dto)
|
|
{
|
|
// Check if there is an existing invite
|
|
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
|
if (emailValidationErrors.Any())
|
|
{
|
|
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
|
if (await _userManager.IsEmailConfirmedAsync(invitedUser))
|
|
return BadRequest($"User is already registered as {invitedUser.UserName}");
|
|
|
|
_logger.LogInformation("A user is attempting to login, but hasn't accepted email invite");
|
|
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
|
}
|
|
|
|
|
|
var user = await _userManager.Users
|
|
.Include(u => u.UserPreferences)
|
|
.SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper());
|
|
if (user == null) return BadRequest("Invalid username");
|
|
|
|
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password);
|
|
if (!validPassword) return BadRequest("Your credentials are not correct");
|
|
|
|
try
|
|
{
|
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
|
//if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue sending email");
|
|
user.Email = dto.Email;
|
|
if (!await ConfirmEmailToken(token, user)) return BadRequest("There was a critical error during migration");
|
|
_unitOfWork.UserRepository.Update(user);
|
|
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
//var emailLink = GenerateEmailLink(await _userManager.GenerateEmailConfirmationTokenAsync(user), "confirm-migration-email", user.Email);
|
|
// _logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", dto.Username, emailLink);
|
|
// // Always send an email, even if the user can't click it just to get them conformable with the system
|
|
// await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
|
// {
|
|
// EmailAddress = dto.Email,
|
|
// Username = user.UserName,
|
|
// ServerConfirmationLink = emailLink
|
|
// });
|
|
return Ok();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "There was an issue during email migration. Contact support");
|
|
_unitOfWork.UserRepository.Delete(user);
|
|
await _unitOfWork.CommitAsync();
|
|
}
|
|
|
|
return BadRequest("There was an error setting up your account. Please check the logs");
|
|
}
|
|
|
|
private async Task<bool> ConfirmEmailToken(string token, AppUser user)
|
|
{
|
|
var result = await _userManager.ConfirmEmailAsync(user, token);
|
|
if (!result.Succeeded)
|
|
{
|
|
_logger.LogCritical("Email validation failed");
|
|
if (result.Errors.Any())
|
|
{
|
|
foreach (var error in result.Errors)
|
|
{
|
|
_logger.LogCritical("Email validation error: {Message}", error.Description);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|