Random Bugs (#2531)

This commit is contained in:
Joe Milazzo 2024-01-06 10:33:56 -06:00 committed by GitHub
parent 0c70e80420
commit 4e1c66331f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 232 additions and 178 deletions

View file

@ -7,4 +7,8 @@ export interface InviteUserResponse {
* If an email was sent to the invited user
*/
emailSent: boolean;
}
/**
* When a user has an invalid email and is attempting to perform a flow.
*/
invalidEmail: boolean;
}

View file

@ -1,10 +0,0 @@
export interface UpdateEmailResponse {
/**
* Did the user not have an existing email
*/
hadNoExistingEmail: boolean;
/**
* Was an email sent (ie is this server accessible)
*/
emailSent: boolean;
}

View file

@ -10,12 +10,10 @@ import { EVENTS, MessageHubService } from './message-hub.service';
import { ThemeService } from './theme.service';
import { InviteUserResponse } from '../_models/auth/invite-user-response';
import { UserUpdateEvent } from '../_models/events/user-update-event';
import { UpdateEmailResponse } from '../_models/auth/update-email-response';
import { AgeRating } from '../_models/metadata/age-rating';
import { AgeRestriction } from '../_models/metadata/age-restriction';
import { TextResonse } from '../_types/text-response';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {ToastrService} from "ngx-toastr";
export enum Role {
Admin = 'Admin',
@ -31,7 +29,6 @@ export enum Role {
export class AccountService {
private readonly destroyRef = inject(DestroyRef);
private readonly toastr = inject(ToastrService);
baseUrl = environment.apiUrl;
userKey = 'kavita-user';
@ -192,8 +189,9 @@ export class AccountService {
return this.httpClient.get<boolean>(this.baseUrl + 'account/email-confirmed');
}
migrateUser(model: {email: string, username: string, password: string, sendEmail: boolean}) {
return this.httpClient.post<string>(this.baseUrl + 'account/migrate-email', model, TextResonse);
isEmailValid() {
return this.httpClient.get<string>(this.baseUrl + 'account/is-email-valid', TextResonse)
.pipe(map(res => res == "true"));
}
confirmMigrationEmail(model: {email: string, token: string}) {
@ -247,7 +245,7 @@ export class AccountService {
}
updateEmail(email: string, password: string) {
return this.httpClient.post<UpdateEmailResponse>(this.baseUrl + 'account/update/email', {email, password});
return this.httpClient.post<InviteUserResponse>(this.baseUrl + 'account/update/email', {email, password});
}
updateAgeRestriction(ageRating: AgeRating, includeUnknowns: boolean) {

View file

@ -19,6 +19,9 @@
<div *ngIf="userForm.get('username')?.errors?.required">
{{t('required')}}
</div>
<div *ngIf="userForm.get('username')?.errors?.pattern">
{{t('username-pattern', {characters: allowedCharacters})}}
</div>
</div>
</div>
</div>

View file

@ -12,6 +12,8 @@ import { RoleSelectorComponent } from '../role-selector/role-selector.component'
import { NgIf } from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco";
const AllowedUsernameCharacters = /^[\sa-zA-Z0-9\-._@+/\s]*$/;
@Component({
selector: 'app-edit-user',
templateUrl: './edit-user.component.html',
@ -30,6 +32,8 @@ export class EditUserComponent implements OnInit {
userForm: FormGroup = new FormGroup({});
allowedCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/';
public get email() { return this.userForm.get('email'); }
public get username() { return this.userForm.get('username'); }
public get password() { return this.userForm.get('password'); }
@ -39,7 +43,7 @@ export class EditUserComponent implements OnInit {
ngOnInit(): void {
this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email]));
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required]));
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required, Validators.pattern(AllowedUsernameCharacters)]));
this.userForm.get('email')?.disable();
this.selectedRestriction = this.member.ageRestriction;

View file

@ -39,8 +39,12 @@
<ng-container *ngIf="emailLink !== ''">
<h4>{{t('setup-user-title')}}</h4>
<p>{{t('setup-user-description')}}
</p>
<p>{{t('setup-user-description')}}</p>
@if (inviteError) {
<div class="alert alert-warning" role="alert">
<strong>{{t('notice')}}</strong> {{t('email-not-sent')}}
</div>
}
<a class="email-link" href="{{emailLink}}" target="_blank" rel="noopener noreferrer">{{t('setup-user-account')}}</a>
<app-api-key [title]="t('invite-url-label')" [tooltipText]="t('setup-user-account-tooltip')" [hideData]="false" [showRefresh]="false" [transform]="makeLink"></app-api-key>
</ng-container>

View file

@ -34,6 +34,7 @@ export class InviteUserComponent implements OnInit {
selectedRestriction: AgeRestriction = {ageRating: AgeRating.NotApplicable, includeUnknowns: false};
emailLink: string = '';
invited: boolean = false;
inviteError: boolean = false;
private readonly cdRef = inject(ChangeDetectorRef);
@ -65,14 +66,24 @@ export class InviteUserComponent implements OnInit {
this.emailLink = data.emailLink;
this.isSending = false;
this.invited = true;
this.cdRef.markForCheck();
if (data.invalidEmail) {
this.toastr.info(translate('toasts.email-not-sent'));
this.inviteError = true;
this.cdRef.markForCheck();
return;
}
if (data.emailSent) {
this.toastr.info(translate('toasts.email-sent', {email: email}));
this.modal.close(true);
}
this.cdRef.markForCheck();
}, err => {
// Note to self: If you need to catch an error, do it, but don't toast because interceptor handles that
this.isSending = false;
this.toastr.error(err)
this.cdRef.markForCheck();
});
}

View file

@ -115,7 +115,7 @@ export class ManageUsersComponent implements OnInit {
this.serverService.isServerAccessible().subscribe(canAccess => {
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
if (canAccess) {
this.toastr.info(this.translocoService.translate('toasts.email-sent-to-user', {user: member.username}));
this.toastr.info(this.translocoService.translate('toasts.email-sent', {user: member.username}));
return;
}
await this.confirmService.alert(

View file

@ -1,7 +1,5 @@
import {CommonModule, DOCUMENT} from '@angular/common';
import {
afterNextRender,
AfterRenderPhase, AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@ -172,6 +170,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
}
hasCustomSort() {
if (this.filteringDisabled) return false;
return this.filter?.sortOptions?.sortField != SortField.SortName || !this.filter?.sortOptions.isAscending
|| this.filterSettings?.presetsV2?.sortOptions?.sortField != SortField.SortName || !this.filterSettings?.presetsV2?.sortOptions?.isAscending;
}

View file

@ -318,8 +318,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
const companionHeight = this.companionBar!.nativeElement.offsetHeight;
const navbarHeight = navbar.offsetHeight;
const totalHeight = companionHeight + navbarHeight + 21; //21px to account for padding
console.log('compainionHeight: ', companionHeight)
console.log('navbarHeight: ', navbarHeight)
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
}

View file

@ -73,6 +73,6 @@
<div class="side-nav-overlay" (click)="toggleNavBar()" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async)}"></div>
<div class="bottom" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async),
'hidden': (navService.sideNavVisibility$ | async) === false || (accountService.hasValidLicense$ | async) === true}">
<app-side-nav-item *ngIf="(accountService.hasValidLicense$ | async) === false" [ngClass]="'donate'" icon="fa-heart" [title]="t('donate')" link="https://opencollective.com/kavita" [external]="true"></app-side-nav-item>
<app-side-nav-item *ngIf="(accountService.hasValidLicense$ | async) === false" [ngClass]="'donate'" icon="fa-heart" [ngbTooltip]="t('donate-tooltip')" link="https://wiki.kavitareader.com/en/faq#q-i-want-to-donate-what-are-my-options" [external]="true"></app-side-nav-item>
</div>
</ng-container>

View file

@ -7,7 +7,7 @@ import {
OnInit
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {distinctUntilChanged, filter, map, take, tap} from 'rxjs/operators';
import { ImportCblModalComponent } from 'src/app/reading-list/_modals/import-cbl-modal/import-cbl-modal.component';
import { ImageService } from 'src/app/_services/image.service';
@ -34,7 +34,7 @@ import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.en
@Component({
selector: 'app-side-nav',
standalone: true,
imports: [CommonModule, SideNavItemComponent, CardActionablesComponent, FilterPipe, FormsModule, TranslocoDirective, SentenceCasePipe],
imports: [CommonModule, SideNavItemComponent, CardActionablesComponent, FilterPipe, FormsModule, TranslocoDirective, SentenceCasePipe, NgbTooltip],
templateUrl: './side-nav.component.html',
styleUrls: ['./side-nav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -29,14 +29,22 @@
<div *ngFor="let error of errors">{{error}}</div>
</div>
<form [formGroup]="form">
@if(!hasValidEmail) {
<div class="alert alert-warning" role="alert">
{{t('has-invalid-email')}}
</div>
}
<div class="mb-3">
<label for="email" class="form-label visually-hidden">{{t('email-label')}}</label>
<input class="form-control custom-input" type="email" id="email" formControlName="email"
[class.is-invalid]="form.get('email')?.invalid && form.get('email')?.touched">
<div id="email-validations" class="invalid-feedback" *ngIf="form.dirty || form.touched">
<div id="email-validations" class="invalid-feedback" *ngIf="form.get('email')?.errors">
<div *ngIf="form.get('email')?.errors?.required">
{{t('required-field')}}
</div>
<div *ngIf="form.get('email')?.errors?.email">
{{t('valid-email')}}
</div>
</div>
</div>

View file

@ -2,13 +2,12 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, injec
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {shareReplay, take} from 'rxjs';
import {UpdateEmailResponse} from 'src/app/_models/auth/update-email-response';
import {User} from 'src/app/_models/user';
import {AccountService} from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { ApiKeyComponent } from '../api-key/api-key.component';
import { NgbTooltip, NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import { NgIf, NgFor } from '@angular/common';
import {NgIf, NgFor, JsonPipe} from '@angular/common';
import {translate, TranslocoDirective} from "@ngneat/transloco";
@Component({
@ -17,17 +16,20 @@ import {translate, TranslocoDirective} from "@ngneat/transloco";
styleUrls: ['./change-email.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, NgbTooltip, NgbCollapse, NgFor, ReactiveFormsModule, ApiKeyComponent, TranslocoDirective]
imports: [NgIf, NgbTooltip, NgbCollapse, NgFor, ReactiveFormsModule, ApiKeyComponent, TranslocoDirective, JsonPipe]
})
export class ChangeEmailComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
form: FormGroup = new FormGroup({});
user: User | undefined = undefined;
errors: string[] = [];
isViewMode: boolean = true;
emailLink: string = '';
emailConfirmed: boolean = true;
private readonly destroyRef = inject(DestroyRef);
hasValidEmail: boolean = true;
public get email() { return this.form.get('email'); }
@ -45,6 +47,10 @@ export class ChangeEmailComponent implements OnInit {
this.emailConfirmed = confirmed;
this.cdRef.markForCheck();
});
this.accountService.isEmailValid().subscribe(isValid => {
this.hasValidEmail = isValid;
this.cdRef.markForCheck();
});
});
}
@ -59,13 +65,14 @@ export class ChangeEmailComponent implements OnInit {
const model = this.form.value;
this.errors = [];
this.accountService.updateEmail(model.email, model.password).subscribe((updateEmailResponse: UpdateEmailResponse) => {
this.accountService.updateEmail(model.email, model.password).subscribe(updateEmailResponse => {
if (updateEmailResponse.invalidEmail) {
this.toastr.success(translate('toasts.email-sent-to-no-existing', {email: model.email}));
}
if (updateEmailResponse.emailSent) {
if (updateEmailResponse.hadNoExistingEmail) {
this.toastr.success(translate('toasts.email-sent-to-no-existing', {email: model.email}));
} else {
this.toastr.success(translate('toasts.email-sent-to'));
}
this.toastr.success(translate('toasts.email-sent-to'));
} else {
this.toastr.success(translate('toasts.change-email-private'));
}

View file

@ -250,9 +250,11 @@
"setup-user-account": "Setup user's account",
"invite-url-label": "Invite Url",
"invite-url-tooltip": "Copy this and paste in a new tab",
"has-invalid-email": "It looks like you do not have a valid email set. Change email will require the admin to send you a link to complete this action.",
"permission-error": "You do not have permission to change your email. Reach out to the admin of the server.",
"required-field": "{{validation.required-field}}",
"valid-email": "{{validation.valid-email}}",
"reset": "{{common.reset}}",
"edit": "{{common.edit}}",
"cancel": "{{common.cancel}}",
@ -562,7 +564,9 @@
"invite-url-label": "Invite Url",
"invite": "Invite",
"inviting": "Inviting…",
"cancel": "{{common.cancel}}"
"cancel": "{{common.cancel}}",
"email-not-sent": "{{toasts.email-not-sent}}",
"notice": "{{manage-settings.notice}}"
},
"library-selector": {
@ -772,6 +776,7 @@
"all-series": "All Series",
"clear": "{{common.clear}}",
"donate": "Donate",
"donate-tooltip": "You can remove this by subscribing to Kavita+",
"back": "Back",
"more": "More"
},
@ -1252,6 +1257,7 @@
"delete-user-alt": "Delete User {{user}}",
"edit-user-tooltip": "Edit",
"edit-user-alt": "Edit User {{user}}",
"username-pattern": "Username can only contain the following characters and whitespace: {{characters}}",
"resend-invite-tooltip": "Resend Invite",
"resend-invite-alt": "Resend Invite {{user}}",
"setup-user-tooltip": "Setup User",
@ -1924,7 +1930,6 @@
"no-updates": "No updates available",
"confirm-delete-user": "Are you sure you want to delete this user?",
"user-deleted": "{{user}} has been deleted",
"email-sent-to-user": "Email sent to {{user}}",
"click-email-link": "Please click this link to confirm your email. You must confirm to be able to login.",
"series-added-to-collection": "Series added to {{collectionName}} collection",
"no-series-collection-warning": "Warning! No series are selected, saving will delete the Collection. Are you sure you want to continue?",
@ -1955,6 +1960,7 @@
"file-send-to": "File(s) emailed to {{name}}",
"theme-missing": "The active theme no longer exists. Please refresh the page.",
"email-sent": "Email sent to {{email}}",
"email-not-sent": "Email on file is not a valid email and can not be sent. A link has been dumped in logs. The admin can provide this link to complete flow.",
"k+-license-saved": "License Key saved, but it is not valid. Click check to revalidate the subscription. First time registration may take a min to propagate.",
"k+-unlocked": "Kavita+ unlocked!",
"k+-error": "There was an error when activating your license. Please try again.",
@ -1976,7 +1982,7 @@
"library-created": "Library created successfully. A scan has been started.",
"anilist-token-updated": "AniList Token has been updated",
"age-restriction-updated": "Age Restriction has been updated",
"email-sent-to-no-existing": "An email has been sent to {{email}} for confirmation.",
"email-sent-to-no-existing": "Existing email is not valid. A link has been dumped to logs. Ask admin for link to complete email change.",
"email-sent-to": "An email has been sent to your old email address for confirmation.",
"change-email-private": "The server is not publicly accessible. Ask the admin to fetch your confirmation link from the logs",
"device-updated": "Device updated",