Misc Updates (#665)

* Do not allow non-admins to change their passwords when authentication is disabled

* Clean up the login page so that input field text is black

* cleanup some resizing when typing a password and having a lot of users

* Changed the LastActive for a user to not just be login, but also when they open an already authenticated session.
This commit is contained in:
Joseph Milazzo 2021-10-13 11:13:55 -07:00 committed by GitHub
parent 40ea4235fe
commit 70f324669b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 57 additions and 48 deletions

View File

@ -140,7 +140,7 @@ namespace API.Controllers
} }
} }
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);
try try
{ {

View File

@ -35,15 +35,6 @@ namespace API.Data.Repositories
return _mapper.Map<ServerSettingDto>(settings); return _mapper.Map<ServerSettingDto>(settings);
} }
public ServerSettingDto GetSettingsDto()
{
var settings = _context.ServerSetting
.Select(x => x)
.AsNoTracking()
.ToList();
return _mapper.Map<ServerSettingDto>(settings);
}
public Task<ServerSetting> GetSettingAsync(ServerSettingKey key) public Task<ServerSetting> GetSettingAsync(ServerSettingKey key)
{ {
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key); return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);

View File

@ -10,7 +10,6 @@ namespace API.Interfaces.Repositories
{ {
void Update(ServerSetting settings); void Update(ServerSetting settings);
Task<ServerSettingDto> GetSettingsDtoAsync(); Task<ServerSettingDto> GetSettingsDtoAsync();
ServerSettingDto GetSettingsDto();
Task<ServerSetting> GetSettingAsync(ServerSettingKey key); Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
Task<IEnumerable<ServerSetting>> GetSettingsAsync(); Task<IEnumerable<ServerSetting>> GetSettingsAsync();

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Interfaces; using API.Interfaces;
@ -27,7 +28,7 @@ namespace API.SignalR.Presence
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
} }
public Task UserConnected(string username, string connectionId) public async Task UserConnected(string username, string connectionId)
{ {
lock (OnlineUsers) lock (OnlineUsers)
{ {
@ -41,7 +42,10 @@ namespace API.SignalR.Presence
} }
} }
return Task.CompletedTask; // Update the last active for the user
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
user.LastActive = DateTime.Now;
await _unitOfWork.CommitAsync();
} }
public Task UserDisconnected(string username, string connectionId) public Task UserDisconnected(string username, string connectionId)

View File

@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { ServerSettings } from './_models/server-settings'; import { ServerSettings } from './_models/server-settings';
@ -37,6 +38,8 @@ export class SettingsService {
} }
getAuthenticationEnabled() { getAuthenticationEnabled() {
return this.http.get<boolean>(this.baseUrl + 'settings/authentication-enabled', {responseType: 'text' as 'json'}); return this.http.get<string>(this.baseUrl + 'settings/authentication-enabled', {responseType: 'text' as 'json'}).pipe(map((res: string) => {
return res === 'true';
}));
} }
} }

View File

@ -1,6 +1,6 @@
<div class="mx-auto login"> <div class="mx-auto login">
<ng-container *ngIf="isLoaded"> <ng-container *ngIf="isLoaded">
<div class="display: inline-block" *ngIf="firstTimeFlow"> <div class="display: inline-block" *ngIf="firstTimeFlow">
<h3 class="card-title text-center">Create an Admin Account</h3> <h3 class="card-title text-center">Create an Admin Account</h3>
<div class="card p-3"> <div class="card p-3">
@ -12,7 +12,7 @@
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow"> <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"> <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"> <ng-container *ngFor="let member of memberNames">
<div class="col align-self-center card p-3 m-3" style="width: 12rem;"> <div class="col align-self-center card p-3 m-3" style="max-width: 12rem;">
<span tabindex="0" (click)="select(member)" a11y-click="13,32"> <span tabindex="0" (click)="select(member)" a11y-click="13,32">
<div class="logo-container"> <div class="logo-container">
<h3 class="card-title text-center">{{member | sentenceCase}}</h3> <h3 class="card-title text-center">{{member | sentenceCase}}</h3>

View File

@ -8,6 +8,8 @@
height: calc(100vh); height: calc(100vh);
min-height: 289px; min-height: 289px;
position: relative; position: relative;
width: 100vw;
max-width: 100vw;
&::before { &::before {
@ -80,4 +82,5 @@
input { input {
background-color: #fff !important; background-color: #fff !important;
color: black;
} }

View File

@ -47,7 +47,7 @@ export class UserLoginComponent implements OnInit {
this.settingsService.getAuthenticationEnabled().pipe(take(1)).subscribe((enabled: boolean) => { this.settingsService.getAuthenticationEnabled().pipe(take(1)).subscribe((enabled: boolean) => {
// There is a bug where this is coming back as a string not a boolean. // There is a bug where this is coming back as a string not a boolean.
this.authDisabled = enabled + '' === 'false'; this.authDisabled = !enabled;
if (this.authDisabled) { if (this.authDisabled) {
this.loginForm.get('password')?.setValidators([]); this.loginForm.get('password')?.setValidators([]);

View File

@ -175,37 +175,42 @@
</div> </div>
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<p>Change your Password</p> <ng-container *ngIf="isAuthenticationEnabled || isAdmin; else authDisabled">
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0"> <p>Change your Password</p>
<div *ngFor="let error of resetPasswordErrors">{{error}}</div> <div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
</div> <div *ngFor="let error of resetPasswordErrors">{{error}}</div>
<form [formGroup]="passwordChangeForm"> </div>
<div class="form-group"> <form [formGroup]="passwordChangeForm">
<label for="new-password">New Password</label> <div class="form-group">
<input class="form-control" type="password" id="new-password" formControlName="password" required> <label for="new-password">New Password</label>
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched"> <input class="form-control" type="password" id="new-password" formControlName="password" required>
<div *ngIf="password?.errors?.required"> <div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
This field is required <div *ngIf="password?.errors?.required">
This field is required
</div>
</div> </div>
</div> </div>
</div> <div class="form-group">
<div class="form-group"> <label for="confirm-password">Confirm Password</label>
<label for="confirm-password">Confirm Password</label> <input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="password-validations" required>
<input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="password-validations" required> <div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched"> <div *ngIf="!passwordsMatch">
<div *ngIf="!passwordsMatch"> Passwords must match
Passwords must match </div>
</div> <div *ngIf="confirmPassword?.errors?.required">
<div *ngIf="confirmPassword?.errors?.required"> This field is required
This field is required </div>
</div> </div>
</div> </div>
</div> <div class="float-right mb-3">
<div class="float-right mb-3"> <button type="button" class="btn btn-secondary mr-2" aria-describedby="password-panel" (click)="resetPasswordForm()">Reset</button>
<button type="button" class="btn btn-secondary mr-2" aria-describedby="password-panel" (click)="resetPasswordForm()">Reset</button> <button type="submit" class="btn btn-primary" aria-describedby="password-panel" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">Save</button>
<button type="submit" class="btn btn-primary" aria-describedby="password-panel" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">Save</button> </div>
</div> </form>
</form> </ng-container>
<ng-template #authDisabled>
<p class="text-warning">Authentication is disabled on this server. A password is not required to authenticate.</p>
</ng-template>
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
<ngb-panel id="api-panel" title="OPDS"> <ngb-panel id="api-panel" title="OPDS">

View File

@ -11,8 +11,6 @@ import { AccountService } from 'src/app/_services/account.service';
import { NavService } from 'src/app/_services/nav.service'; import { NavService } from 'src/app/_services/nav.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { SettingsService } from 'src/app/admin/settings.service'; import { SettingsService } from 'src/app/admin/settings.service';
import { keyframes } from '@angular/animations';
import { environment } from 'src/environments/environment';
@Component({ @Component({
selector: 'app-user-preferences', selector: 'app-user-preferences',
@ -29,6 +27,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
settingsForm: FormGroup = new FormGroup({}); settingsForm: FormGroup = new FormGroup({});
passwordChangeForm: FormGroup = new FormGroup({}); passwordChangeForm: FormGroup = new FormGroup({});
user: User | undefined = undefined; user: User | undefined = undefined;
isAdmin: boolean = false;
isAuthenticationEnabled: boolean = true;
passwordsMatch = false; passwordsMatch = false;
resetPasswordErrors: string[] = []; resetPasswordErrors: string[] = [];
@ -75,7 +75,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.settingsService.getOpdsEnabled().subscribe(res => { this.settingsService.getOpdsEnabled().subscribe(res => {
this.opdsEnabled = res; this.opdsEnabled = res;
}) });
this.settingsService.getAuthenticationEnabled().subscribe(res => {
this.isAuthenticationEnabled = res;
});
} }
ngOnInit(): void { ngOnInit(): void {
@ -83,6 +86,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => { this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => {
if (user) { if (user) {
this.user = user; this.user = user;
this.isAdmin = this.accountService.hasAdminRole(user);
if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) { if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) {
this.user.preferences.bookReaderFontFamily = 'default'; this.user.preferences.bookReaderFontFamily = 'default';