diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 58478e2f8..86b1ac778 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -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 _logger; private readonly IMapper _mapper; + private readonly IAccountService _accountService; /// public AccountController(UserManager userManager, SignInManager signInManager, ITokenService tokenService, IUnitOfWork unitOfWork, ILogger 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; } /// @@ -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); diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 6081f7d58..049413388 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -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 _userManager; /// - public CollectionController(IUnitOfWork unitOfWork, UserManager userManager) + public CollectionController(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; - _userManager = userManager; } /// @@ -36,7 +32,7 @@ namespace API.Controllers public async Task> 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(); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index d1359a9e5..256dc362d 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -26,7 +26,6 @@ namespace API.Controllers private readonly IUnitOfWork _unitOfWork; private readonly IDownloadService _downloadService; private readonly IDirectoryService _directoryService; - private readonly UserManager _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 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 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 tags; if (isAdmin) diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index acd1b61e8..41d3fcfbd 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -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 _logger; private readonly IUnitOfWork _unitOfWork; private readonly ITaskScheduler _taskScheduler; + private readonly IAccountService _accountService; - public SettingsController(ILogger logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler) + public SettingsController(ILogger 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> GetAuthenticationEnabled() + { + var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + return Ok(settingsDto.EnableAuthentication); + } } } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 2c83a1267..f2f1a34ac 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -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>> GetUserNames() + { + var members = await _unitOfWork.UserRepository.GetMembersAsync(); + return Ok(members.Select(m => m.Username)); + } + + [Authorize] [HttpGet("has-reading-progress")] public async Task> 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> 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> UpdatePreferences(UserPreferencesDto preferencesDto) { diff --git a/API/DTOs/Account/LoginDto.cs b/API/DTOs/Account/LoginDto.cs index 3da1841bf..a21e9868f 100644 --- a/API/DTOs/Account/LoginDto.cs +++ b/API/DTOs/Account/LoginDto.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs index d04c2a03e..1bf598f5d 100644 --- a/API/DTOs/RegisterDto.cs +++ b/API/DTOs/RegisterDto.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 271f7d7a6..c56391778 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -21,5 +21,10 @@ /// Enables OPDS connections to be made to the server. /// public bool EnableOpds { get; set; } + + /// + /// Enables Authentication on the server. Defaults to true. + /// + public bool EnableAuthentication { get; set; } } } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 13642a210..ece1356fd 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -153,6 +153,16 @@ namespace API.Data.Repositories return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); } + public async Task> GetNonAdminUsersAsync() + { + return await _userManager.GetUsersInRoleAsync(PolicyConstants.PlebRole); + } + + public async Task IsUserAdmin(AppUser user) + { + return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); + } + public async Task GetUserRating(int seriesId, int userId) { return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId) diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index f264168ae..7bc438ab5 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -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) diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index fab4a7cba..cbf68f013 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -20,6 +20,8 @@ namespace API.Entities.Enums AllowStatCollection = 6, [Description("EnableOpds")] EnableOpds = 7, + [Description("EnableAuthentication")] + EnableAuthentication = 8 } } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index c08555c69..3d7fde699 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -36,6 +36,7 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index dbd13ab9e..db1736b0b 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -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; } } diff --git a/API/Interfaces/Repositories/IUserRepository.cs b/API/Interfaces/Repositories/IUserRepository.cs index 22bd9dc92..65d943623 100644 --- a/API/Interfaces/Repositories/IUserRepository.cs +++ b/API/Interfaces/Repositories/IUserRepository.cs @@ -15,6 +15,8 @@ namespace API.Interfaces.Repositories public void Delete(AppUser user); Task> GetMembersAsync(); Task> GetAdminUsersAsync(); + Task> GetNonAdminUsersAsync(); + Task IsUserAdmin(AppUser user); Task GetUserRating(int seriesId, int userId); Task GetPreferencesAsync(string username); Task> GetBookmarkDtosForSeries(int userId, int seriesId); diff --git a/API/Interfaces/Services/IAccountService.cs b/API/Interfaces/Services/IAccountService.cs new file mode 100644 index 000000000..e07ce2f79 --- /dev/null +++ b/API/Interfaces/Services/IAccountService.cs @@ -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> ChangeUserPassword(AppUser user, string newPassword); + } +} diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs new file mode 100644 index 000000000..0cc720bb6 --- /dev/null +++ b/API/Services/AccountService.cs @@ -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 _userManager; + private readonly ILogger _logger; + public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; + + public AccountService(UserManager userManager, ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task> 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(); + } + } +} diff --git a/API/Startup.cs b/API/Startup.cs index d69385e23..3e6e5c659 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -78,7 +78,7 @@ namespace API Id = "Bearer" } }, - new string[] { } + Array.Empty() } }); diff --git a/UI/Web/src/app/_services/member.service.ts b/UI/Web/src/app/_services/member.service.ts index 519807bb9..3e59347f7 100644 --- a/UI/Web/src/app/_services/member.service.ts +++ b/UI/Web/src/app/_services/member.service.ts @@ -16,6 +16,10 @@ export class MemberService { return this.httpClient.get(this.baseUrl + 'users'); } + getMemberNames() { + return this.httpClient.get(this.baseUrl + 'users/names'); + } + adminExists() { return this.httpClient.get(this.baseUrl + 'admin/exists'); } diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index 011d53279..a2e682e62 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -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) { diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts index 58f43a2a0..f21b886d1 100644 --- a/UI/Web/src/app/admin/_models/server-settings.ts +++ b/UI/Web/src/app/admin/_models/server-settings.ts @@ -6,4 +6,5 @@ export interface ServerSettings { port: number; allowStatCollection: boolean; enableOpds: boolean; + enableAuthentication: boolean; } diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 796e9fccf..eb7cc14ac 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -42,6 +42,15 @@ +
+ +

By disabling authentication, all non-admin users will be able to login by just their username. No password will be required to authenticate.

+
+ + +
+
+

Reoccuring Tasks

  diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index d4d93a87c..ab5a71c0c 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -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 = []; logLevels: Array = []; - 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); }); diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index b76a45d2e..c1982f130 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -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(); } diff --git a/UI/Web/src/app/admin/settings.service.ts b/UI/Web/src/app/admin/settings.service.ts index 1c7f717c7..f3ab0c3f7 100644 --- a/UI/Web/src/app/admin/settings.service.ts +++ b/UI/Web/src/app/admin/settings.service.ts @@ -35,4 +35,8 @@ export class SettingsService { getOpdsEnabled() { return this.http.get(this.baseUrl + 'settings/opds-enabled', {responseType: 'text' as 'json'}); } + + getAuthenticationEnabled() { + return this.http.get(this.baseUrl + 'settings/authentication-enabled', {responseType: 'text' as 'json'}); + } } diff --git a/UI/Web/src/app/app-routing.module.ts b/UI/Web/src/app/app-routing.module.ts index e65fd5ee3..5ddee3f2b 100644 --- a/UI/Web/src/app/app-routing.module.ts +++ b/UI/Web/src/app/app-routing.module.ts @@ -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({ diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index 9a029e665..f68ef4341 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -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, diff --git a/UI/Web/src/app/home/home.component.html b/UI/Web/src/app/home/home.component.html deleted file mode 100644 index 9b23d9c1b..000000000 --- a/UI/Web/src/app/home/home.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
- -

Please create an admin account for yourself to start your reading journey.

- -
-
- - - - - diff --git a/UI/Web/src/app/home/home.component.scss b/UI/Web/src/app/home/home.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/home/home.component.ts b/UI/Web/src/app/home/home.component.ts deleted file mode 100644 index 7c474cf82..000000000 --- a/UI/Web/src/app/home/home.component.ts +++ /dev/null @@ -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'); - } - } -} diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index ef2937fb2..843006726 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -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'); } }); diff --git a/UI/Web/src/app/register-member/register-member.component.html b/UI/Web/src/app/register-member/register-member.component.html index 155b0e9a9..84a7983a8 100644 --- a/UI/Web/src/app/register-member/register-member.component.html +++ b/UI/Web/src/app/register-member/register-member.component.html @@ -1,3 +1,4 @@ +

Errors:

    @@ -10,7 +11,7 @@
-
+
@@ -21,7 +22,7 @@
- - + +
- \ No newline at end of file + diff --git a/UI/Web/src/app/register-member/register-member.component.scss b/UI/Web/src/app/register-member/register-member.component.scss index e69de29bb..74ab28f75 100644 --- a/UI/Web/src/app/register-member/register-member.component.scss +++ b/UI/Web/src/app/register-member/register-member.component.scss @@ -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%); +} \ No newline at end of file diff --git a/UI/Web/src/app/register-member/register-member.component.ts b/UI/Web/src/app/register-member/register-member.component.ts index 9b839b3aa..8a705700c 100644 --- a/UI/Web/src/app/register-member/register-member.component.ts +++ b/UI/Web/src/app/register-member/register-member.component.ts @@ -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(); + /** + * Emits the new user created. + */ + @Output() created = new EventEmitter(); 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); } } diff --git a/UI/Web/src/app/user-login/user-login.component.html b/UI/Web/src/app/user-login/user-login.component.html index 553522c81..de8be9137 100644 --- a/UI/Web/src/app/user-login/user-login.component.html +++ b/UI/Web/src/app/user-login/user-login.component.html @@ -1,28 +1,44 @@