mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -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.Account;
|
||||
using API.Entities;
|
||||
using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -31,13 +31,14 @@ namespace API.Controllers
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<AccountController> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AccountController(UserManager<AppUser> userManager,
|
||||
SignInManager<AppUser> signInManager,
|
||||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||
ILogger<AccountController> logger,
|
||||
IMapper mapper)
|
||||
IMapper mapper, IAccountService accountService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
@ -45,6 +46,7 @@ namespace API.Controllers
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_mapper = mapper;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -61,30 +63,10 @@ namespace API.Controllers
|
||||
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
|
||||
// Validate Password
|
||||
foreach (var validator in _userManager.PasswordValidators)
|
||||
var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
|
||||
if (errors.Any())
|
||||
{
|
||||
var validationResult = await validator.ValidateAsync(_userManager, user, resetPasswordDto.Password);
|
||||
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)));
|
||||
return BadRequest(errors);
|
||||
}
|
||||
|
||||
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
||||
@ -110,6 +92,13 @@ namespace API.Controllers
|
||||
user.UserPreferences ??= new AppUserPreferences();
|
||||
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);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||
@ -166,6 +155,14 @@ namespace API.Controllers
|
||||
|
||||
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
|
||||
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
||||
|
||||
|
@ -2,13 +2,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
@ -19,13 +17,11 @@ namespace API.Controllers
|
||||
public class CollectionController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollectionController(IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
||||
public CollectionController(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -36,7 +32,7 @@ namespace API.Controllers
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
||||
{
|
||||
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)
|
||||
{
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
|
@ -26,7 +26,6 @@ namespace API.Controllers
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDownloadService _downloadService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly IReaderService _readerService;
|
||||
|
||||
@ -41,13 +40,12 @@ namespace API.Controllers
|
||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
|
||||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||
IDirectoryService directoryService, UserManager<AppUser> userManager,
|
||||
ICacheService cacheService, IReaderService readerService)
|
||||
IDirectoryService directoryService, ICacheService cacheService,
|
||||
IReaderService readerService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_downloadService = downloadService;
|
||||
_directoryService = directoryService;
|
||||
_userManager = userManager;
|
||||
_cacheService = cacheService;
|
||||
_readerService = readerService;
|
||||
|
||||
@ -170,7 +168,7 @@ namespace API.Controllers
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var userId = await GetUser(apiKey);
|
||||
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;
|
||||
if (isAdmin)
|
||||
@ -213,7 +211,7 @@ namespace API.Controllers
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var userId = await GetUser(apiKey);
|
||||
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;
|
||||
if (isAdmin)
|
||||
|
@ -8,6 +8,8 @@ using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Converters;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -21,12 +23,14 @@ namespace API.Controllers
|
||||
private readonly ILogger<SettingsController> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
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;
|
||||
_unitOfWork = unitOfWork;
|
||||
_taskScheduler = taskScheduler;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
@ -57,6 +61,7 @@ namespace API.Controllers
|
||||
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
var updateAuthentication = false;
|
||||
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
@ -93,6 +98,13 @@ namespace API.Controllers
|
||||
_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)
|
||||
{
|
||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||
@ -110,12 +122,33 @@ namespace API.Controllers
|
||||
|
||||
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();
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
}
|
||||
|
||||
|
||||
_logger.LogInformation("Server Settings updated");
|
||||
_taskScheduler.ScheduleTasks();
|
||||
return Ok(updateSettingsDto);
|
||||
@ -148,5 +181,12 @@ namespace API.Controllers
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
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
|
||||
{
|
||||
[Authorize]
|
||||
public class UsersController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
@ -39,6 +38,15 @@ namespace API.Controllers
|
||||
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")]
|
||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||
{
|
||||
@ -47,6 +55,7 @@ namespace API.Controllers
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("has-library-access")]
|
||||
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
|
||||
{
|
||||
@ -54,6 +63,7 @@ namespace API.Controllers
|
||||
return Ok(libs.Any(x => x.Id == libraryId));
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("update-preferences")]
|
||||
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
namespace API.DTOs
|
||||
namespace API.DTOs.Account
|
||||
{
|
||||
public class LoginDto
|
||||
{
|
||||
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; }
|
||||
[Required]
|
||||
[StringLength(32, MinimumLength = 6)]
|
||||
public string Password { get; init; }
|
||||
public string Password { get; set; }
|
||||
public bool IsAdmin { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,5 +21,10 @@
|
||||
/// Enables OPDS connections to be made to the server.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.AllowStatCollection, Value = "true"},
|
||||
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
};
|
||||
|
||||
foreach (var defaultSetting in defaultSettings)
|
||||
|
@ -20,6 +20,8 @@ namespace API.Entities.Enums
|
||||
AllowStatCollection = 6,
|
||||
[Description("EnableOpds")]
|
||||
EnableOpds = 7,
|
||||
[Description("EnableAuthentication")]
|
||||
EnableAuthentication = 8
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ namespace API.Extensions
|
||||
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
|
||||
services.AddScoped<IDownloadService, DownloadService>();
|
||||
services.AddScoped<IReaderService, ReaderService>();
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
|
||||
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||
|
||||
|
@ -36,6 +36,9 @@ namespace API.Helpers.Converters
|
||||
case ServerSettingKey.EnableOpds:
|
||||
destination.EnableOpds = bool.Parse(row.Value);
|
||||
break;
|
||||
case ServerSettingKey.EnableAuthentication:
|
||||
destination.EnableAuthentication = bool.Parse(row.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ namespace API.Interfaces.Repositories
|
||||
public void Delete(AppUser user);
|
||||
Task<IEnumerable<MemberDto>> GetMembersAsync();
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<IEnumerable<AppUser>> GetNonAdminUsersAsync();
|
||||
Task<bool> IsUserAdmin(AppUser user);
|
||||
Task<AppUserRating> GetUserRating(int seriesId, int userId);
|
||||
Task<AppUserPreferences> GetPreferencesAsync(string username);
|
||||
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"
|
||||
}
|
||||
},
|
||||
new string[] { }
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -16,6 +16,10 @@ export class MemberService {
|
||||
return this.httpClient.get<Member[]>(this.baseUrl + 'users');
|
||||
}
|
||||
|
||||
getMemberNames() {
|
||||
return this.httpClient.get<string[]>(this.baseUrl + 'users/names');
|
||||
}
|
||||
|
||||
adminExists() {
|
||||
return this.httpClient.get<boolean>(this.baseUrl + 'admin/exists');
|
||||
}
|
||||
|
@ -131,7 +131,9 @@ export class MessageHubService {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -6,4 +6,5 @@ export interface ServerSettings {
|
||||
port: number;
|
||||
allowStatCollection: boolean;
|
||||
enableOpds: boolean;
|
||||
enableAuthentication: boolean;
|
||||
}
|
||||
|
@ -42,6 +42,15 @@
|
||||
</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>
|
||||
<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>
|
||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
|
||||
@ -17,7 +18,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||
taskFrequencies: Array<string> = [];
|
||||
logLevels: Array<string> = [];
|
||||
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService) { }
|
||||
constructor(private settingsService: SettingsService, private toastr: ToastrService, private confirmService: ConfirmService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
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('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [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('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection);
|
||||
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
||||
this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication);
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
async saveSettings() {
|
||||
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.resetForm();
|
||||
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) => {
|
||||
console.error('error: ', err);
|
||||
});
|
||||
|
@ -77,7 +77,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
this.createMemberToggle = true;
|
||||
}
|
||||
|
||||
onMemberCreated(success: boolean) {
|
||||
onMemberCreated(createdUser: User | null) {
|
||||
this.createMemberToggle = false;
|
||||
this.loadMembers();
|
||||
}
|
||||
|
@ -35,4 +35,8 @@ export class SettingsService {
|
||||
getOpdsEnabled() {
|
||||
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 { Routes, RouterModule } from '@angular/router';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
||||
import { LibraryComponent } from './library/library.component';
|
||||
import { NotConnectedComponent } from './not-connected/not-connected.component';
|
||||
import { SeriesDetailComponent } from './series-detail/series-detail.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 { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||
import { DashboardComponent as AdminDashboardComponent } from './admin/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
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', component: HomeComponent},
|
||||
{path: '', component: UserLoginComponent},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
|
||||
@ -62,7 +59,7 @@ const routes: Routes = [
|
||||
},
|
||||
{path: 'login', component: UserLoginComponent},
|
||||
{path: 'no-connection', component: NotConnectedComponent},
|
||||
{path: '**', component: HomeComponent, pathMatch: 'full'}
|
||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -4,10 +4,9 @@ import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
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 { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
||||
import { UserLoginComponent } from './user-login/user-login.component';
|
||||
@ -87,7 +86,6 @@ if (environment.production) {
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HomeComponent,
|
||||
NavHeaderComponent,
|
||||
UserLoginComponent,
|
||||
LibraryComponent,
|
||||
@ -114,6 +112,8 @@ if (environment.production) {
|
||||
NgbNavModule,
|
||||
NgbPaginationModule,
|
||||
|
||||
NgbCollapseModule, // Login
|
||||
|
||||
SharedModule,
|
||||
CarouselModule,
|
||||
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 {
|
||||
// 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">
|
||||
<p>Errors:</p>
|
||||
<ul>
|
||||
@ -10,7 +11,7 @@
|
||||
<input id="username" class="form-control" formControlName="username" type="text">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="registerForm.get('isAdmin')?.value || !authDisabled">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" class="form-control" formControlName="password" type="password">
|
||||
</div>
|
||||
@ -21,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<button class="btn btn-secondary mr-2" type="button" (click)="cancel()">Cancel</button>
|
||||
<button class="btn btn-primary" type="submit">Register</button>
|
||||
<button class="btn btn-secondary mr-2" type="button" (click)="cancel()" *ngIf="!firstTimeFlow">Cancel</button>
|
||||
<button class="btn btn-primary {{firstTimeFlow ? 'alt' : ''}}" type="submit">Register</button>
|
||||
</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 { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { SettingsService } from '../admin/settings.service';
|
||||
import { User } from '../_models/user';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register-member',
|
||||
@ -10,35 +13,42 @@ import { AccountService } from 'src/app/_services/account.service';
|
||||
export class RegisterMemberComponent implements OnInit {
|
||||
|
||||
@Input() firstTimeFlow = false;
|
||||
@Output() created = new EventEmitter<boolean>();
|
||||
/**
|
||||
* Emits the new user created.
|
||||
*/
|
||||
@Output() created = new EventEmitter<User | null>();
|
||||
|
||||
adminExists = false;
|
||||
authDisabled: boolean = false;
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
username: new FormControl('', [Validators.required]),
|
||||
password: new FormControl('', [Validators.required]),
|
||||
password: new FormControl('', []),
|
||||
isAdmin: new FormControl(false, [])
|
||||
});
|
||||
errors: string[] = [];
|
||||
|
||||
constructor(private accountService: AccountService) {
|
||||
constructor(private accountService: AccountService, private settingsService: SettingsService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getAuthenticationEnabled().pipe(take(1)).subscribe(authEnabled => {
|
||||
this.authDisabled = !authEnabled;
|
||||
});
|
||||
if (this.firstTimeFlow) {
|
||||
this.registerForm.get('isAdmin')?.setValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
register() {
|
||||
this.accountService.register(this.registerForm.value).subscribe(resp => {
|
||||
this.created.emit(true);
|
||||
this.accountService.register(this.registerForm.value).subscribe(user => {
|
||||
this.created.emit(user);
|
||||
}, err => {
|
||||
this.errors = err;
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.created.emit(false);
|
||||
this.created.emit(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,28 +1,44 @@
|
||||
<div class="mx-auto login">
|
||||
<div class="card p-3" style="width: 18rem;">
|
||||
<div class="logo-container">
|
||||
<img class="logo" src="assets/images/kavita-book-cropped.png" alt="Kavita logo"/>
|
||||
<h3 class="card-title text-center">Kavita</h3>
|
||||
|
||||
<div class="display: inline-block" *ngIf="firstTimeFlow">
|
||||
<h3 class="card-title text-center">Create an Admin Account</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 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>
|
||||
|
||||
<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>
|
@ -4,6 +4,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -61px; // To offset the navbar
|
||||
height: calc(100vh);
|
||||
min-height: 289px;
|
||||
position: relative;
|
||||
@ -22,24 +23,34 @@
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
margin: 0 auto 15px;
|
||||
|
||||
.logo {
|
||||
display:inline-block;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: $primary-color;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
min-width: 300px;
|
||||
|
||||
&:focus {
|
||||
border: 2px solid white;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.card-title {
|
||||
font-family: 'Spartan', sans-serif;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
vertical-align: middle;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.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 { Router } from '@angular/router';
|
||||
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 { MemberService } from '../_services/member.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
@ -17,27 +19,57 @@ export class UserLoginComponent implements OnInit {
|
||||
model: any = {username: '', password: ''};
|
||||
loginForm: FormGroup = new FormGroup({
|
||||
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 {
|
||||
// Validate that there are users so you can refresh to home. This is important for first installs
|
||||
this.validateAdmin();
|
||||
}
|
||||
|
||||
validateAdmin() {
|
||||
this.navService.hideNavBar();
|
||||
this.memberService.adminExists().subscribe(res => {
|
||||
if (!res) {
|
||||
this.router.navigateByUrl('/home');
|
||||
this.navService.showNavBar();
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.router.navigateByUrl('/library');
|
||||
}
|
||||
});
|
||||
|
||||
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() {
|
||||
if (!this.loginForm.dirty || !this.loginForm.valid) { return; }
|
||||
this.model = {username: this.loginForm.get('username')?.value, password: this.loginForm.get('password')?.value};
|
||||
this.accountService.login(this.model).subscribe(() => {
|
||||
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
|
||||
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', '');
|
||||
this.router.navigateByUrl(pageResume);
|
||||
} 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