mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 13:44:31 -04:00
Disable Authentication & Login Page Rework (#619)
* Implemented the ability to disable authentication on a server instance. Admins will require authentication, but non-admin accounts can be setup without any password requirements. * WIP for new login page. * Reworked code to handle disabled auth better. First time user flow is moved into the user login component. * Removed debug code * Removed home component, shakeout testing is complete. * remove a file accidently committed * Fixed a code smell from last PR * Code smells
This commit is contained in:
parent
83d76982f4
commit
a5b6bf1b52
@ -7,10 +7,10 @@ using API.Constants;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Account;
|
using API.DTOs.Account;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Errors;
|
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
|
using API.Services;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@ -31,13 +31,14 @@ namespace API.Controllers
|
|||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ILogger<AccountController> _logger;
|
private readonly ILogger<AccountController> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public AccountController(UserManager<AppUser> userManager,
|
public AccountController(UserManager<AppUser> userManager,
|
||||||
SignInManager<AppUser> signInManager,
|
SignInManager<AppUser> signInManager,
|
||||||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||||
ILogger<AccountController> logger,
|
ILogger<AccountController> logger,
|
||||||
IMapper mapper)
|
IMapper mapper, IAccountService accountService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
@ -45,6 +46,7 @@ namespace API.Controllers
|
|||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
_accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -61,30 +63,10 @@ namespace API.Controllers
|
|||||||
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
||||||
return Unauthorized("You are not permitted to this operation.");
|
return Unauthorized("You are not permitted to this operation.");
|
||||||
|
|
||||||
// Validate Password
|
var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
|
||||||
foreach (var validator in _userManager.PasswordValidators)
|
if (errors.Any())
|
||||||
{
|
{
|
||||||
var validationResult = await validator.ValidateAsync(_userManager, user, resetPasswordDto.Password);
|
return BadRequest(errors);
|
||||||
if (!validationResult.Succeeded)
|
|
||||||
{
|
|
||||||
return BadRequest(
|
|
||||||
validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _userManager.RemovePasswordAsync(user);
|
|
||||||
if (!result.Succeeded)
|
|
||||||
{
|
|
||||||
_logger.LogError("Could not update password");
|
|
||||||
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
|
|
||||||
if (!result.Succeeded)
|
|
||||||
{
|
|
||||||
_logger.LogError("Could not update password");
|
|
||||||
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
||||||
@ -110,6 +92,13 @@ namespace API.Controllers
|
|||||||
user.UserPreferences ??= new AppUserPreferences();
|
user.UserPreferences ??= new AppUserPreferences();
|
||||||
user.ApiKey = HashUtil.ApiKey();
|
user.ApiKey = HashUtil.ApiKey();
|
||||||
|
|
||||||
|
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
if (!settings.EnableAuthentication && !registerDto.IsAdmin)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("User {UserName} is being registered as non-admin with no server authentication. Using default password.", registerDto.Username);
|
||||||
|
registerDto.Password = AccountService.DefaultPassword;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _userManager.CreateAsync(user, registerDto.Password);
|
var result = await _userManager.CreateAsync(user, registerDto.Password);
|
||||||
|
|
||||||
if (!result.Succeeded) return BadRequest(result.Errors);
|
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||||
@ -166,6 +155,14 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (user == null) return Unauthorized("Invalid username");
|
if (user == null) return Unauthorized("Invalid username");
|
||||||
|
|
||||||
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
|
||||||
|
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
if (!settings.EnableAuthentication && !isAdmin)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("User {UserName} is logging in with authentication disabled", loginDto.Username);
|
||||||
|
loginDto.Password = AccountService.DefaultPassword;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _signInManager
|
var result = await _signInManager
|
||||||
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
||||||
|
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
@ -19,13 +17,11 @@ namespace API.Controllers
|
|||||||
public class CollectionController : BaseApiController
|
public class CollectionController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly UserManager<AppUser> _userManager;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CollectionController(IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
public CollectionController(IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_userManager = userManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -36,7 +32,7 @@ namespace API.Controllers
|
|||||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
{
|
{
|
||||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||||
|
@ -26,7 +26,6 @@ namespace API.Controllers
|
|||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly UserManager<AppUser> _userManager;
|
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
|
|
||||||
@ -41,13 +40,12 @@ namespace API.Controllers
|
|||||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
|
|
||||||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||||
IDirectoryService directoryService, UserManager<AppUser> userManager,
|
IDirectoryService directoryService, ICacheService cacheService,
|
||||||
ICacheService cacheService, IReaderService readerService)
|
IReaderService readerService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_downloadService = downloadService;
|
_downloadService = downloadService;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
_userManager = userManager;
|
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_readerService = readerService;
|
_readerService = readerService;
|
||||||
|
|
||||||
@ -170,7 +168,7 @@ namespace API.Controllers
|
|||||||
return BadRequest("OPDS is not enabled on this server");
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
var userId = await GetUser(apiKey);
|
var userId = await GetUser(apiKey);
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
|
||||||
|
|
||||||
IEnumerable <CollectionTagDto> tags;
|
IEnumerable <CollectionTagDto> tags;
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
@ -213,7 +211,7 @@ namespace API.Controllers
|
|||||||
return BadRequest("OPDS is not enabled on this server");
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
var userId = await GetUser(apiKey);
|
var userId = await GetUser(apiKey);
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
|
||||||
|
|
||||||
IEnumerable <CollectionTagDto> tags;
|
IEnumerable <CollectionTagDto> tags;
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
|
@ -8,6 +8,8 @@ using API.Entities.Enums;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using API.Services;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Kavita.Common.Extensions;
|
using Kavita.Common.Extensions;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -21,12 +23,14 @@ namespace API.Controllers
|
|||||||
private readonly ILogger<SettingsController> _logger;
|
private readonly ILogger<SettingsController> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler)
|
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IAccountService accountService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_taskScheduler = taskScheduler;
|
_taskScheduler = taskScheduler;
|
||||||
|
_accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
@ -57,6 +61,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
// We do not allow CacheDirectory changes, so we will ignore.
|
// We do not allow CacheDirectory changes, so we will ignore.
|
||||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||||
|
var updateAuthentication = false;
|
||||||
|
|
||||||
foreach (var setting in currentSettings)
|
foreach (var setting in currentSettings)
|
||||||
{
|
{
|
||||||
@ -93,6 +98,13 @@ namespace API.Controllers
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.EnableAuthentication && updateSettingsDto.EnableAuthentication + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.EnableAuthentication + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
updateAuthentication = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||||
@ -110,12 +122,33 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
|
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync())
|
try
|
||||||
{
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
if (updateAuthentication)
|
||||||
|
{
|
||||||
|
var users = await _unitOfWork.UserRepository.GetNonAdminUsersAsync();
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
var errors = await _accountService.ChangeUserPassword(user, AccountService.DefaultPassword);
|
||||||
|
if (!errors.Any()) continue;
|
||||||
|
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
return BadRequest(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Server authentication changed. Updated all non-admins to default password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
return BadRequest("There was a critical issue. Please try again.");
|
return BadRequest("There was a critical issue. Please try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_logger.LogInformation("Server Settings updated");
|
_logger.LogInformation("Server Settings updated");
|
||||||
_taskScheduler.ScheduleTasks();
|
_taskScheduler.ScheduleTasks();
|
||||||
return Ok(updateSettingsDto);
|
return Ok(updateSettingsDto);
|
||||||
@ -148,5 +181,12 @@ namespace API.Controllers
|
|||||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
return Ok(settingsDto.EnableOpds);
|
return Ok(settingsDto.EnableOpds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("authentication-enabled")]
|
||||||
|
public async Task<ActionResult<bool>> GetAuthenticationEnabled()
|
||||||
|
{
|
||||||
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
return Ok(settingsDto.EnableAuthentication);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
public class UsersController : BaseApiController
|
public class UsersController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
@ -39,6 +38,15 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.UserRepository.GetMembersAsync());
|
return Ok(await _unitOfWork.UserRepository.GetMembersAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("names")]
|
||||||
|
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUserNames()
|
||||||
|
{
|
||||||
|
var members = await _unitOfWork.UserRepository.GetMembersAsync();
|
||||||
|
return Ok(members.Select(m => m.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[HttpGet("has-reading-progress")]
|
[HttpGet("has-reading-progress")]
|
||||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||||
{
|
{
|
||||||
@ -47,6 +55,7 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[HttpGet("has-library-access")]
|
[HttpGet("has-library-access")]
|
||||||
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
|
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
|
||||||
{
|
{
|
||||||
@ -54,6 +63,7 @@ namespace API.Controllers
|
|||||||
return Ok(libs.Any(x => x.Id == libraryId));
|
return Ok(libs.Any(x => x.Id == libraryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[HttpPost("update-preferences")]
|
[HttpPost("update-preferences")]
|
||||||
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
namespace API.DTOs
|
namespace API.DTOs.Account
|
||||||
{
|
{
|
||||||
public class LoginDto
|
public class LoginDto
|
||||||
{
|
{
|
||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
public string Password { get; init; }
|
public string Password { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ namespace API.DTOs
|
|||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(32, MinimumLength = 6)]
|
[StringLength(32, MinimumLength = 6)]
|
||||||
public string Password { get; init; }
|
public string Password { get; set; }
|
||||||
public bool IsAdmin { get; init; }
|
public bool IsAdmin { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,10 @@
|
|||||||
/// Enables OPDS connections to be made to the server.
|
/// Enables OPDS connections to be made to the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableOpds { get; set; }
|
public bool EnableOpds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables Authentication on the server. Defaults to true.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAuthentication { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,6 +153,16 @@ namespace API.Data.Repositories
|
|||||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AppUser>> GetNonAdminUsersAsync()
|
||||||
|
{
|
||||||
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.PlebRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsUserAdmin(AppUser user)
|
||||||
|
{
|
||||||
|
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<AppUserRating> GetUserRating(int seriesId, int userId)
|
public async Task<AppUserRating> GetUserRating(int seriesId, int userId)
|
||||||
{
|
{
|
||||||
return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
|
return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
|
||||||
|
@ -49,6 +49,7 @@ namespace API.Data
|
|||||||
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
|
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
|
||||||
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||||
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||||
|
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var defaultSetting in defaultSettings)
|
foreach (var defaultSetting in defaultSettings)
|
||||||
|
@ -20,6 +20,8 @@ namespace API.Entities.Enums
|
|||||||
AllowStatCollection = 6,
|
AllowStatCollection = 6,
|
||||||
[Description("EnableOpds")]
|
[Description("EnableOpds")]
|
||||||
EnableOpds = 7,
|
EnableOpds = 7,
|
||||||
|
[Description("EnableAuthentication")]
|
||||||
|
EnableAuthentication = 8
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ namespace API.Extensions
|
|||||||
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
|
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
|
||||||
services.AddScoped<IDownloadService, DownloadService>();
|
services.AddScoped<IDownloadService, DownloadService>();
|
||||||
services.AddScoped<IReaderService, ReaderService>();
|
services.AddScoped<IReaderService, ReaderService>();
|
||||||
|
services.AddScoped<IAccountService, AccountService>();
|
||||||
|
|
||||||
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||||
|
|
||||||
|
@ -36,6 +36,9 @@ namespace API.Helpers.Converters
|
|||||||
case ServerSettingKey.EnableOpds:
|
case ServerSettingKey.EnableOpds:
|
||||||
destination.EnableOpds = bool.Parse(row.Value);
|
destination.EnableOpds = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.EnableAuthentication:
|
||||||
|
destination.EnableAuthentication = bool.Parse(row.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ namespace API.Interfaces.Repositories
|
|||||||
public void Delete(AppUser user);
|
public void Delete(AppUser user);
|
||||||
Task<IEnumerable<MemberDto>> GetMembersAsync();
|
Task<IEnumerable<MemberDto>> GetMembersAsync();
|
||||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||||
|
Task<IEnumerable<AppUser>> GetNonAdminUsersAsync();
|
||||||
|
Task<bool> IsUserAdmin(AppUser user);
|
||||||
Task<AppUserRating> GetUserRating(int seriesId, int userId);
|
Task<AppUserRating> GetUserRating(int seriesId, int userId);
|
||||||
Task<AppUserPreferences> GetPreferencesAsync(string username);
|
Task<AppUserPreferences> GetPreferencesAsync(string username);
|
||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||||
|
12
API/Interfaces/Services/IAccountService.cs
Normal file
12
API/Interfaces/Services/IAccountService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Errors;
|
||||||
|
|
||||||
|
namespace API.Interfaces.Services
|
||||||
|
{
|
||||||
|
public interface IAccountService
|
||||||
|
{
|
||||||
|
Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword);
|
||||||
|
}
|
||||||
|
}
|
53
API/Services/AccountService.cs
Normal file
53
API/Services/AccountService.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Errors;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Services
|
||||||
|
{
|
||||||
|
public class AccountService : IAccountService
|
||||||
|
{
|
||||||
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
private readonly ILogger<AccountService> _logger;
|
||||||
|
public const string DefaultPassword = "[k.2@RZ!mxCQkJzE";
|
||||||
|
|
||||||
|
public AccountService(UserManager<AppUser> userManager, ILogger<AccountService> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword)
|
||||||
|
{
|
||||||
|
foreach (var validator in _userManager.PasswordValidators)
|
||||||
|
{
|
||||||
|
var validationResult = await validator.ValidateAsync(_userManager, user, newPassword);
|
||||||
|
if (!validationResult.Succeeded)
|
||||||
|
{
|
||||||
|
return validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ApiException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,7 +78,7 @@ namespace API
|
|||||||
Id = "Bearer"
|
Id = "Bearer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new string[] { }
|
Array.Empty<string>()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -16,6 +16,10 @@ export class MemberService {
|
|||||||
return this.httpClient.get<Member[]>(this.baseUrl + 'users');
|
return this.httpClient.get<Member[]>(this.baseUrl + 'users');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMemberNames() {
|
||||||
|
return this.httpClient.get<string[]>(this.baseUrl + 'users/names');
|
||||||
|
}
|
||||||
|
|
||||||
adminExists() {
|
adminExists() {
|
||||||
return this.httpClient.get<boolean>(this.baseUrl + 'admin/exists');
|
return this.httpClient.get<boolean>(this.baseUrl + 'admin/exists');
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,9 @@ export class MessageHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopHubConnection() {
|
stopHubConnection() {
|
||||||
this.hubConnection.stop().catch(err => console.error(err));
|
if (this.hubConnection) {
|
||||||
|
this.hubConnection.stop().catch(err => console.error(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(methodName: string, body?: any) {
|
sendMessage(methodName: string, body?: any) {
|
||||||
|
@ -6,4 +6,5 @@ export interface ServerSettings {
|
|||||||
port: number;
|
port: number;
|
||||||
allowStatCollection: boolean;
|
allowStatCollection: boolean;
|
||||||
enableOpds: boolean;
|
enableOpds: boolean;
|
||||||
|
enableAuthentication: boolean;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="authentication" aria-describedby="authentication-info">Authentication</label>
|
||||||
|
<p class="accent" id="authentication-info">By disabling authentication, all non-admin users will be able to login by just their username. No password will be required to authenticate.</p>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="authentication" type="checkbox" aria-label="User Authentication" class="form-check-input" formControlName="enableAuthentication">
|
||||||
|
<label for="authentication" class="form-check-label">Enable Authentication</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>Reoccuring Tasks</h4>
|
<h4>Reoccuring Tasks</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="settings-tasks-scan">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
<label for="settings-tasks-scan">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
import { ServerSettings } from '../_models/server-settings';
|
import { ServerSettings } from '../_models/server-settings';
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
taskFrequencies: Array<string> = [];
|
taskFrequencies: Array<string> = [];
|
||||||
logLevels: Array<string> = [];
|
logLevels: Array<string> = [];
|
||||||
|
|
||||||
constructor(private settingsService: SettingsService, private toastr: ToastrService) { }
|
constructor(private settingsService: SettingsService, private toastr: ToastrService, private confirmService: ConfirmService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => {
|
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => {
|
||||||
@ -35,6 +36,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
|
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
|
||||||
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
||||||
this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required]));
|
this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required]));
|
||||||
|
this.settingsForm.addControl('enableAuthentication', new FormControl(this.serverSettings.enableAuthentication, [Validators.required]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,15 +48,28 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel);
|
this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel);
|
||||||
this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection);
|
this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection);
|
||||||
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
||||||
|
this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
async saveSettings() {
|
||||||
const modelSettings = this.settingsForm.value;
|
const modelSettings = this.settingsForm.value;
|
||||||
|
|
||||||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
|
if (this.settingsForm.get('enableAuthentication')?.value === false) {
|
||||||
|
if (!await this.confirmService.confirm('Disabling Authentication opens your server up to unauthorized access and possible hacking. Are you sure you want to continue with this?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const informUserAfterAuthenticationEnabled = this.settingsForm.get('enableAuthentication')?.value && !this.serverSettings.enableAuthentication;
|
||||||
|
|
||||||
|
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||||
this.serverSettings = settings;
|
this.serverSettings = settings;
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.toastr.success('Server settings updated');
|
this.toastr.success('Server settings updated');
|
||||||
|
|
||||||
|
if (informUserAfterAuthenticationEnabled) {
|
||||||
|
await this.confirmService.alert('You have just re-enabled authentication. All non-admin users have been re-assigned a password of "[k.2@RZ!mxCQkJzE". This is a publicly known password. Please change their users passwords or request them to.');
|
||||||
|
}
|
||||||
}, (err: any) => {
|
}, (err: any) => {
|
||||||
console.error('error: ', err);
|
console.error('error: ', err);
|
||||||
});
|
});
|
||||||
|
@ -77,7 +77,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||||||
this.createMemberToggle = true;
|
this.createMemberToggle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberCreated(success: boolean) {
|
onMemberCreated(createdUser: User | null) {
|
||||||
this.createMemberToggle = false;
|
this.createMemberToggle = false;
|
||||||
this.loadMembers();
|
this.loadMembers();
|
||||||
}
|
}
|
||||||
|
@ -35,4 +35,8 @@ export class SettingsService {
|
|||||||
getOpdsEnabled() {
|
getOpdsEnabled() {
|
||||||
return this.http.get<boolean>(this.baseUrl + 'settings/opds-enabled', {responseType: 'text' as 'json'});
|
return this.http.get<boolean>(this.baseUrl + 'settings/opds-enabled', {responseType: 'text' as 'json'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAuthenticationEnabled() {
|
||||||
|
return this.http.get<boolean>(this.baseUrl + 'settings/authentication-enabled', {responseType: 'text' as 'json'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import { HomeComponent } from './home/home.component';
|
|
||||||
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
||||||
import { LibraryComponent } from './library/library.component';
|
|
||||||
import { NotConnectedComponent } from './not-connected/not-connected.component';
|
import { NotConnectedComponent } from './not-connected/not-connected.component';
|
||||||
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
||||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
||||||
@ -10,13 +8,12 @@ import { UserLoginComponent } from './user-login/user-login.component';
|
|||||||
import { AuthGuard } from './_guards/auth.guard';
|
import { AuthGuard } from './_guards/auth.guard';
|
||||||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
import { DashboardComponent as AdminDashboardComponent } from './admin/dashboard/dashboard.component';
|
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
|
||||||
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', component: HomeComponent},
|
{path: '', component: UserLoginComponent},
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
|
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
|
||||||
@ -62,7 +59,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{path: 'login', component: UserLoginComponent},
|
{path: 'login', component: UserLoginComponent},
|
||||||
{path: 'no-connection', component: NotConnectedComponent},
|
{path: 'no-connection', component: NotConnectedComponent},
|
||||||
{path: '**', component: HomeComponent, pathMatch: 'full'}
|
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -4,10 +4,9 @@ import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
|||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { HomeComponent } from './home/home.component';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
||||||
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
||||||
import { UserLoginComponent } from './user-login/user-login.component';
|
import { UserLoginComponent } from './user-login/user-login.component';
|
||||||
@ -87,7 +86,6 @@ if (environment.production) {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
HomeComponent,
|
|
||||||
NavHeaderComponent,
|
NavHeaderComponent,
|
||||||
UserLoginComponent,
|
UserLoginComponent,
|
||||||
LibraryComponent,
|
LibraryComponent,
|
||||||
@ -114,6 +112,8 @@ if (environment.production) {
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
|
|
||||||
|
NgbCollapseModule, // Login
|
||||||
|
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CarouselModule,
|
CarouselModule,
|
||||||
TypeaheadModule,
|
TypeaheadModule,
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<ng-container *ngIf="firstTimeFlow">
|
|
||||||
<p>Please create an admin account for yourself to start your reading journey.</p>
|
|
||||||
<app-register-member (created)="onAdminCreated($event)" [firstTimeFlow]="firstTimeFlow"></app-register-member>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { MemberService } from '../_services/member.service';
|
|
||||||
import { AccountService } from '../_services/account.service';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-home',
|
|
||||||
templateUrl: './home.component.html',
|
|
||||||
styleUrls: ['./home.component.scss']
|
|
||||||
})
|
|
||||||
export class HomeComponent implements OnInit {
|
|
||||||
|
|
||||||
firstTimeFlow = false;
|
|
||||||
model: any = {};
|
|
||||||
registerForm: FormGroup = new FormGroup({
|
|
||||||
username: new FormControl('', [Validators.required]),
|
|
||||||
password: new FormControl('', [Validators.required])
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(public accountService: AccountService, private memberService: MemberService, private router: Router, private titleService: Title) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
|
|
||||||
this.memberService.adminExists().subscribe(adminExists => {
|
|
||||||
this.firstTimeFlow = !adminExists;
|
|
||||||
|
|
||||||
if (this.firstTimeFlow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.titleService.setTitle('Kavita');
|
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
|
||||||
if (user) {
|
|
||||||
this.router.navigateByUrl('/library');
|
|
||||||
} else {
|
|
||||||
this.router.navigateByUrl('/login');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onAdminCreated(success: boolean) {
|
|
||||||
if (success) {
|
|
||||||
this.router.navigateByUrl('/login');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -325,7 +325,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If no user, we can't render
|
// If no user, we can't render
|
||||||
this.router.navigateByUrl('/home');
|
this.router.navigateByUrl('/login');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
<div class="text-danger" *ngIf="errors.length > 0">
|
<div class="text-danger" *ngIf="errors.length > 0">
|
||||||
<p>Errors:</p>
|
<p>Errors:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -10,7 +11,7 @@
|
|||||||
<input id="username" class="form-control" formControlName="username" type="text">
|
<input id="username" class="form-control" formControlName="username" type="text">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" *ngIf="registerForm.get('isAdmin')?.value || !authDisabled">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input id="password" class="form-control" formControlName="password" type="password">
|
<input id="password" class="form-control" formControlName="password" type="password">
|
||||||
</div>
|
</div>
|
||||||
@ -21,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
<button class="btn btn-secondary mr-2" type="button" (click)="cancel()">Cancel</button>
|
<button class="btn btn-secondary mr-2" type="button" (click)="cancel()" *ngIf="!firstTimeFlow">Cancel</button>
|
||||||
<button class="btn btn-primary" type="submit">Register</button>
|
<button class="btn btn-primary {{firstTimeFlow ? 'alt' : ''}}" type="submit">Register</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
.alt {
|
||||||
|
background-color: #424c72;
|
||||||
|
border-color: #444f75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alt:hover {
|
||||||
|
background-color: #3b4466;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alt:focus {
|
||||||
|
background-color: #343c59;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgb(68 79 117 / 50%);
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
import { SettingsService } from '../admin/settings.service';
|
||||||
|
import { User } from '../_models/user';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-register-member',
|
selector: 'app-register-member',
|
||||||
@ -10,35 +13,42 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||||||
export class RegisterMemberComponent implements OnInit {
|
export class RegisterMemberComponent implements OnInit {
|
||||||
|
|
||||||
@Input() firstTimeFlow = false;
|
@Input() firstTimeFlow = false;
|
||||||
@Output() created = new EventEmitter<boolean>();
|
/**
|
||||||
|
* Emits the new user created.
|
||||||
|
*/
|
||||||
|
@Output() created = new EventEmitter<User | null>();
|
||||||
|
|
||||||
adminExists = false;
|
adminExists = false;
|
||||||
|
authDisabled: boolean = false;
|
||||||
registerForm: FormGroup = new FormGroup({
|
registerForm: FormGroup = new FormGroup({
|
||||||
username: new FormControl('', [Validators.required]),
|
username: new FormControl('', [Validators.required]),
|
||||||
password: new FormControl('', [Validators.required]),
|
password: new FormControl('', []),
|
||||||
isAdmin: new FormControl(false, [])
|
isAdmin: new FormControl(false, [])
|
||||||
});
|
});
|
||||||
errors: string[] = [];
|
errors: string[] = [];
|
||||||
|
|
||||||
constructor(private accountService: AccountService) {
|
constructor(private accountService: AccountService, private settingsService: SettingsService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.settingsService.getAuthenticationEnabled().pipe(take(1)).subscribe(authEnabled => {
|
||||||
|
this.authDisabled = !authEnabled;
|
||||||
|
});
|
||||||
if (this.firstTimeFlow) {
|
if (this.firstTimeFlow) {
|
||||||
this.registerForm.get('isAdmin')?.setValue(true);
|
this.registerForm.get('isAdmin')?.setValue(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
register() {
|
register() {
|
||||||
this.accountService.register(this.registerForm.value).subscribe(resp => {
|
this.accountService.register(this.registerForm.value).subscribe(user => {
|
||||||
this.created.emit(true);
|
this.created.emit(user);
|
||||||
}, err => {
|
}, err => {
|
||||||
this.errors = err;
|
this.errors = err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.created.emit(false);
|
this.created.emit(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,44 @@
|
|||||||
<div class="mx-auto login">
|
<div class="mx-auto login">
|
||||||
<div class="card p-3" style="width: 18rem;">
|
|
||||||
<div class="logo-container">
|
<div class="display: inline-block" *ngIf="firstTimeFlow">
|
||||||
<img class="logo" src="assets/images/kavita-book-cropped.png" alt="Kavita logo"/>
|
<h3 class="card-title text-center">Create an Admin Account</h3>
|
||||||
<h3 class="card-title text-center">Kavita</h3>
|
<div class="card p-3">
|
||||||
|
<p>Please create an admin account for yourself to start your reading journey.</p>
|
||||||
|
<app-register-member (created)="onAdminCreated($event)" [firstTimeFlow]="firstTimeFlow"></app-register-member>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-text">
|
|
||||||
<form [formGroup]="loginForm" (ngSubmit)="login()">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Username</label>
|
|
||||||
<input class="form-control" formControlName="username" id="username" type="text" autofocus>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input class="form-control" formControlName="password" id="password" type="password">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="float-right">
|
|
||||||
<button class="btn btn-primary alt" type="submit">Login</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow">
|
||||||
|
<div class="row row-cols-4 row-cols-md-4 row-cols-sm-2 row-cols-xs-2">
|
||||||
|
<ng-container *ngFor="let member of memberNames">
|
||||||
|
<div class="col align-self-center card p-3 m-3" style="width: 12rem;">
|
||||||
|
<span tabindex="0" (click)="select(member)" a11y-click="13,32">
|
||||||
|
<div class="logo-container">
|
||||||
|
<h3 class="card-title text-center">{{member | titlecase}}</h3>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="card-text" #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed[member]" (keyup.enter)="$event.stopPropagation()">
|
||||||
|
<div class="form-group" style="display: none;">
|
||||||
|
<label for="username--{{member}}">Username</label>
|
||||||
|
<input class="form-control" formControlName="username" id="username--{{member}}" type="text" [readonly]="true">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password--{{member}}">Password</label>
|
||||||
|
<input class="form-control" formControlName="password" id="password--{{member}}" type="password" autofocus>
|
||||||
|
<div *ngIf="authDisabled" class="invalid-feedback">
|
||||||
|
Authentication is disabled. Only type password if this is an admin account.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="float-right">
|
||||||
|
<button class="btn btn-primary alt" type="submit--{{member}}">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -4,6 +4,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-top: -61px; // To offset the navbar
|
||||||
height: calc(100vh);
|
height: calc(100vh);
|
||||||
min-height: 289px;
|
min-height: 289px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -22,24 +23,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
margin: 0 auto 15px;
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-top: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: $primary-color;
|
background-color: $primary-color;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid white;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-family: 'Spartan', sans-serif;
|
font-family: 'Spartan', sans-serif;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 10px;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-text {
|
.card-text {
|
||||||
@ -66,3 +77,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: inline-block;
|
||||||
|
color: #343c59;
|
||||||
|
}
|
@ -2,7 +2,9 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { first } from 'rxjs/operators';
|
import { first, take } from 'rxjs/operators';
|
||||||
|
import { SettingsService } from '../admin/settings.service';
|
||||||
|
import { User } from '../_models/user';
|
||||||
import { AccountService } from '../_services/account.service';
|
import { AccountService } from '../_services/account.service';
|
||||||
import { MemberService } from '../_services/member.service';
|
import { MemberService } from '../_services/member.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../_services/nav.service';
|
||||||
@ -17,27 +19,57 @@ export class UserLoginComponent implements OnInit {
|
|||||||
model: any = {username: '', password: ''};
|
model: any = {username: '', password: ''};
|
||||||
loginForm: FormGroup = new FormGroup({
|
loginForm: FormGroup = new FormGroup({
|
||||||
username: new FormControl('', [Validators.required]),
|
username: new FormControl('', [Validators.required]),
|
||||||
password: new FormControl('', [Validators.required])
|
password: new FormControl('', [Validators.required])
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private accountService: AccountService, private router: Router, private memberService: MemberService, private toastr: ToastrService, private navService: NavService) { }
|
memberNames: Array<string> = [];
|
||||||
|
isCollapsed: {[key: string]: boolean} = {};
|
||||||
|
authDisabled: boolean = false;
|
||||||
|
/**
|
||||||
|
* If there are no admins on the server, this will enable the registration to kick in.
|
||||||
|
*/
|
||||||
|
firstTimeFlow: boolean = true;
|
||||||
|
|
||||||
|
constructor(private accountService: AccountService, private router: Router, private memberService: MemberService,
|
||||||
|
private toastr: ToastrService, private navService: NavService, private settingsService: SettingsService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Validate that there are users so you can refresh to home. This is important for first installs
|
this.navService.showNavBar();
|
||||||
this.validateAdmin();
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
}
|
if (user) {
|
||||||
|
this.router.navigateByUrl('/library');
|
||||||
validateAdmin() {
|
|
||||||
this.navService.hideNavBar();
|
|
||||||
this.memberService.adminExists().subscribe(res => {
|
|
||||||
if (!res) {
|
|
||||||
this.router.navigateByUrl('/home');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settingsService.getAuthenticationEnabled().pipe(take(1)).subscribe((enabled: boolean) => {
|
||||||
|
// There is a bug where this is coming back as a string not a boolean.
|
||||||
|
this.authDisabled = enabled + '' === 'false';
|
||||||
|
if (this.authDisabled) {
|
||||||
|
this.loginForm.get('password')?.setValidators([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.memberService.getMemberNames().pipe(take(1)).subscribe(members => {
|
||||||
|
this.memberNames = members;
|
||||||
|
const isOnlyOne = this.memberNames.length === 1;
|
||||||
|
this.memberNames.forEach(name => this.isCollapsed[name] = !isOnlyOne);
|
||||||
|
this.firstTimeFlow = members.length === 0;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdminCreated(user: User | null) {
|
||||||
|
if (user != null) {
|
||||||
|
this.firstTimeFlow = false;
|
||||||
|
this.isCollapsed[user.username] = true;
|
||||||
|
this.select(user.username);
|
||||||
|
this.memberNames.push(user.username);
|
||||||
|
} else {
|
||||||
|
this.toastr.error('There was an issue creating the new user. Please refresh and try again.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
if (!this.loginForm.dirty || !this.loginForm.valid) { return; }
|
|
||||||
this.model = {username: this.loginForm.get('username')?.value, password: this.loginForm.get('password')?.value};
|
this.model = {username: this.loginForm.get('username')?.value, password: this.loginForm.get('password')?.value};
|
||||||
this.accountService.login(this.model).subscribe(() => {
|
this.accountService.login(this.model).subscribe(() => {
|
||||||
this.loginForm.reset();
|
this.loginForm.reset();
|
||||||
@ -45,7 +77,7 @@ export class UserLoginComponent implements OnInit {
|
|||||||
|
|
||||||
// Check if user came here from another url, else send to library route
|
// Check if user came here from another url, else send to library route
|
||||||
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
|
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
|
||||||
if (pageResume && pageResume !== '/no-connection') {
|
if (pageResume && pageResume !== '/no-connection' && pageResume !== '/login') {
|
||||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||||
this.router.navigateByUrl(pageResume);
|
this.router.navigateByUrl(pageResume);
|
||||||
} else {
|
} else {
|
||||||
@ -62,4 +94,20 @@ export class UserLoginComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select(member: string) {
|
||||||
|
|
||||||
|
this.loginForm.get('username')?.setValue(member);
|
||||||
|
|
||||||
|
this.isCollapsed[member] = !this.isCollapsed[member];
|
||||||
|
this.collapseAllButName(member);
|
||||||
|
// ?! Scroll to the newly opened element?
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseAllButName(name: string) {
|
||||||
|
Object.keys(this.isCollapsed).forEach(key => {
|
||||||
|
if (key !== name) {
|
||||||
|
this.isCollapsed[key] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user