mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Polish before Release 2 (#3723)
Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
parent
67d7d8467e
commit
4453482d93
@ -6,11 +6,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.12" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.12" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.13" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -52,7 +52,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@ -70,11 +70,11 @@
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
@ -92,15 +92,15 @@
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.8.0.113526">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="22.0.12" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.4" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.3" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
|
@ -138,6 +138,12 @@ public class AccountController : BaseApiController
|
||||
return BadRequest(usernameValidation);
|
||||
}
|
||||
|
||||
// If Email is empty, default to the username
|
||||
if (string.IsNullOrEmpty(registerDto.Email))
|
||||
{
|
||||
registerDto.Email = registerDto.Username;
|
||||
}
|
||||
|
||||
var user = new AppUserBuilder(registerDto.Username, registerDto.Email,
|
||||
await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||
|
||||
@ -352,10 +358,11 @@ public class AccountController : BaseApiController
|
||||
/// <param name="dto"></param>
|
||||
/// <returns>Returns just if the email was sent or server isn't reachable</returns>
|
||||
[HttpPost("update/email")]
|
||||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
||||
public async Task<ActionResult<InviteUserResponse>> UpdateEmail(UpdateEmailDto? dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole))
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||
@ -364,12 +371,13 @@ public class AccountController : BaseApiController
|
||||
// Validate this user's password
|
||||
if (! await _userManager.CheckPasswordAsync(user, dto.Password))
|
||||
{
|
||||
_logger.LogCritical("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
|
||||
_logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
}
|
||||
|
||||
// Validate no other users exist with this email
|
||||
if (user.Email!.Equals(dto.Email)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
if (user.Email!.Equals(dto.Email))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
// Check if email is used by another user
|
||||
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
@ -386,8 +394,10 @@ public class AccountController : BaseApiController
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
|
||||
}
|
||||
|
||||
var isValidEmailAddress = _emailService.IsValidEmail(user.Email);
|
||||
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email);
|
||||
var shouldEmailUser = serverSettings.IsEmailSetup() || !isValidEmailAddress;
|
||||
|
||||
user.EmailConfirmed = !shouldEmailUser;
|
||||
user.ConfirmationToken = token;
|
||||
await _userManager.UpdateAsync(user);
|
||||
@ -401,7 +411,8 @@ public class AccountController : BaseApiController
|
||||
return Ok(new InviteUserResponse
|
||||
{
|
||||
EmailLink = string.Empty,
|
||||
EmailSent = false
|
||||
EmailSent = false,
|
||||
InvalidEmail = !isValidEmailAddress
|
||||
});
|
||||
}
|
||||
|
||||
@ -409,7 +420,7 @@ public class AccountController : BaseApiController
|
||||
// Send a confirmation email
|
||||
try
|
||||
{
|
||||
if (!_emailService.IsValidEmail(user.Email))
|
||||
if (!isValidEmailAddress)
|
||||
{
|
||||
_logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email);
|
||||
return Ok(new InviteUserResponse
|
||||
@ -441,7 +452,8 @@ public class AccountController : BaseApiController
|
||||
return Ok(new InviteUserResponse
|
||||
{
|
||||
EmailLink = string.Empty,
|
||||
EmailSent = true
|
||||
EmailSent = true,
|
||||
InvalidEmail = !isValidEmailAddress
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
#nullable enable
|
||||
|
||||
public class RegisterDto
|
||||
{
|
||||
@ -9,7 +10,7 @@ public class RegisterDto
|
||||
/// <summary>
|
||||
/// An email to register with. Optional. Provides Forgot Password functionality
|
||||
/// </summary>
|
||||
public string Email { get; init; } = default!;
|
||||
public string? Email { get; set; } = default!;
|
||||
[Required]
|
||||
[StringLength(256, MinimumLength = 6)]
|
||||
public string Password { get; set; } = default!;
|
||||
|
@ -510,7 +510,7 @@ public static class SeriesFilter
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => s.Metadata.Tags == null || s.Metadata.Tags.Count == 0);
|
||||
return queryable.Where(s => s.Metadata.Tags.Count == 0);
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
@ -707,7 +707,7 @@ public static class SeriesFilter
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => collectionSeries.All(c => c != s.Id));
|
||||
return queryable.Where(s => s.Collections.Count == 0);
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
|
@ -9,12 +9,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Cronos" Version="0.9.0" />
|
||||
<PackageReference Include="Cronos" Version="0.10.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageReference Include="Flurl.Http" Version="4.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.8.0.113526">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
8
UI/Web/package-lock.json
generated
8
UI/Web/package-lock.json
generated
@ -39,7 +39,7 @@
|
||||
"luxon": "^3.6.1",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
"ng-select2-component": "^17.2.3",
|
||||
"ng-select2-component": "^17.2.4",
|
||||
"ngx-color-picker": "^19.0.0",
|
||||
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
@ -7412,9 +7412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ng-select2-component": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.3.tgz",
|
||||
"integrity": "sha512-JNik7OWqya4ERuqlfnYiJHkaqyZtHqUhATIZ9yUxmadWWNIn8I3Lwa7qt0KtPpR01O9HJC0PtHXhvev88Cju2A==",
|
||||
"version": "17.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.4.tgz",
|
||||
"integrity": "sha512-pfRQg1gY1NsQkBNAYYeSYJjejKwz1z+9bKWor8/8toCNbvh9TYMOKpcz3FrNvhR6v/Hto/quddajaxjD81TOgg==",
|
||||
"dependencies": {
|
||||
"ngx-infinite-scroll": ">=18.0.0 || >=19.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
|
@ -47,7 +47,7 @@
|
||||
"luxon": "^3.6.1",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
"ng-select2-component": "^17.2.3",
|
||||
"ng-select2-component": "^17.2.4",
|
||||
"ngx-color-picker": "^19.0.0",
|
||||
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
|
@ -1,22 +1,20 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {DestroyRef, inject, Injectable } from '@angular/core';
|
||||
import {catchError, Observable, of, ReplaySubject, shareReplay, throwError} from 'rxjs';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {DestroyRef, inject, Injectable} from '@angular/core';
|
||||
import {Observable, of, ReplaySubject, shareReplay} from 'rxjs';
|
||||
import {filter, map, switchMap, tap} from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Preferences } from '../_models/preferences/preferences';
|
||||
import { User } from '../_models/user';
|
||||
import { Router } from '@angular/router';
|
||||
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 { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRestriction } from '../_models/metadata/age-restriction';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import {environment} from 'src/environments/environment';
|
||||
import {Preferences} from '../_models/preferences/preferences';
|
||||
import {User} from '../_models/user';
|
||||
import {Router} from '@angular/router';
|
||||
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 {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 {Action} from "./action-factory.service";
|
||||
import {CoverImageSize} from "../admin/_models/cover-image-size";
|
||||
import {LicenseInfo} from "../_models/kavitaplus/license-info";
|
||||
import {LicenseService} from "./license.service";
|
||||
import {LocalizationService} from "./localization.service";
|
||||
|
||||
@ -132,7 +130,7 @@ export class AccountService {
|
||||
}
|
||||
|
||||
hasChangeAgeRestrictionRole(user: User) {
|
||||
return user && user.roles.includes(Role.ChangeRestriction);
|
||||
return user && !user.roles.includes(Role.Admin) && user.roles.includes(Role.ChangeRestriction);
|
||||
}
|
||||
|
||||
hasDownloadRole(user: User) {
|
||||
@ -199,9 +197,9 @@ export class AccountService {
|
||||
if (this.currentUser) {
|
||||
// BUG: StopHubConnection has a promise in it, this needs to be async
|
||||
// But that really messes everything up
|
||||
this.messageHub.stopHubConnection();
|
||||
this.messageHub.createHubConnection(this.currentUser);
|
||||
if (!isSameUser) {
|
||||
this.messageHub.stopHubConnection();
|
||||
this.messageHub.createHubConnection(this.currentUser);
|
||||
this.licenseService.hasValidLicense().subscribe();
|
||||
}
|
||||
this.startRefreshTokenTimer();
|
||||
|
@ -17,12 +17,8 @@
|
||||
{{formControl.value | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<div class="input-group">
|
||||
<input id="settings-hostname" aria-describedby="hostname-validations" class="form-control" formControlName="hostName" type="text"
|
||||
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
|
||||
</div>
|
||||
<input id="settings-hostname" aria-describedby="hostname-validations" class="form-control" formControlName="hostName" type="text"
|
||||
[class.is-invalid]="formControl.invalid && !formControl.untouched" appEnterBlur>
|
||||
|
||||
@if (formControl.errors; as errors) {
|
||||
<div id="hostname-validations" class="invalid-feedback" style="display: inline-block">
|
||||
@ -69,7 +65,11 @@
|
||||
{{formControl.value | defaultValue}}
|
||||
</ng-template>
|
||||
<ng-template #edit>
|
||||
<input type="text" class="form-control" formControlName="host" id="settings-host" appEnterBlur />
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" formControlName="host" id="settings-host" appEnterBlur />
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-10 mb-2">
|
||||
@if (formGroup.get('comparison')?.value !== FilterComparison.IsEmpty) {
|
||||
@if (IsEmptySelected) {
|
||||
@if (predicateType$ | async; as predicateType) {
|
||||
@switch (predicateType) {
|
||||
@case (PredicateType.Text) {
|
||||
|
@ -170,6 +170,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
private readonly mangaFormatPipe = new MangaFormatPipe(this.translocoService);
|
||||
private readonly ageRatingPipe = new AgeRatingPipe();
|
||||
|
||||
get IsEmptySelected() {
|
||||
return parseInt(this.formGroup.get('comparison')?.value + '', 10) !== FilterComparison.IsEmpty;
|
||||
}
|
||||
|
||||
|
||||
get UiLabel(): FilterRowUi | null {
|
||||
const field = parseInt(this.formGroup.get('input')!.value, 10) as FilterField;
|
||||
@ -348,6 +352,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.formGroup.get('filterValue')?.patchValue('');
|
||||
this.formGroup.get('comparison')?.patchValue(StringComparisons[0]);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -363,10 +368,13 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
|
||||
this.validComparisons$.next([...new Set(comps)]);
|
||||
this.predicateType$.next(PredicateType.Number);
|
||||
|
||||
if (this.loaded) {
|
||||
this.formGroup.get('filterValue')?.patchValue(0);
|
||||
this.formGroup.get('comparison')?.patchValue(NumberComparisons[0]);
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -383,6 +391,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.formGroup.get('filterValue')?.patchValue(false);
|
||||
this.formGroup.get('comparison')?.patchValue(DateComparisons[0]);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -400,6 +409,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.formGroup.get('filterValue')?.patchValue(false);
|
||||
this.formGroup.get('comparison')?.patchValue(BooleanComparisons[0]);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -421,15 +431,15 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.formGroup.get('filterValue')?.patchValue(0);
|
||||
this.formGroup.get('comparison')?.patchValue(comps[0]);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
onDateSelect(_: NgbDate) {
|
||||
this.propagateFilterUpdate();
|
||||
}
|
||||
|
||||
updateIfDateFilled() {
|
||||
this.propagateFilterUpdate();
|
||||
}
|
||||
|
@ -8,11 +8,13 @@
|
||||
<label for="username" class="form-label">{{t('username-label')}}</label>
|
||||
<input id="username" class="form-control custom-input" formControlName="username" type="text" autocomplete="username"
|
||||
[class.is-invalid]="registerForm.get('username')?.invalid && registerForm.get('username')?.touched" aria-describedby="username-validations">
|
||||
<div id="username-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('username')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
@if (registerForm.dirty || registerForm.touched) {
|
||||
<div id="username-validations" class="invalid-feedback">
|
||||
@if (registerForm.get('username')?.errors?.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 text-start">
|
||||
@ -22,16 +24,18 @@
|
||||
<span class="visually-hidden" id="email-help">
|
||||
<ng-container [ngTemplateOutlet]="emailTooltip"></ng-container>
|
||||
</span>
|
||||
<input class="form-control custom-input" type="email" inputmode="email" id="email" autocomplete="email" formControlName="email" required aria-describedby="email-help"
|
||||
<input class="form-control custom-input" type="email" inputmode="email" id="email" autocomplete="email" formControlName="email" aria-describedby="email-help"
|
||||
[class.is-invalid]="registerForm.get('email')?.invalid && registerForm.get('email')?.touched">
|
||||
<div id="email-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
@if (registerForm.dirty || registerForm.touched) {
|
||||
<div id="email-validations" class="invalid-feedback">
|
||||
@if (registerForm.get('email')?.errors?.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
} @else if (registerForm.get('email')?.errors?.email) {
|
||||
<div>{{t('valid-email')}}</div>
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="registerForm.get('email')?.errors?.email">
|
||||
{{t('valid-email')}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3 text-start">
|
||||
|
@ -1,13 +1,13 @@
|
||||
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { SplashContainerComponent } from '../splash-container/splash-container.component';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {Router} from '@angular/router';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {MemberService} from 'src/app/_services/member.service';
|
||||
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
import {SplashContainerComponent} from '../splash-container/splash-container.component';
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {NavService} from "../../../_services/nav.service";
|
||||
|
||||
@ -15,25 +15,28 @@ import {NavService} from "../../../_services/nav.service";
|
||||
* This is exclusively used to register the first user on the server and nothing else
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SplashContainerComponent, ReactiveFormsModule, NgIf, NgbTooltip, NgTemplateOutlet, TranslocoDirective]
|
||||
selector: 'app-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.scss'],
|
||||
imports: [SplashContainerComponent, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RegisterComponent {
|
||||
|
||||
private readonly navService = inject(NavService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly memberService = inject(MemberService);
|
||||
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
email: new FormControl('', [Validators.required]),
|
||||
username: new FormControl('', [Validators.required]),
|
||||
email: new FormControl('', []),
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(256),
|
||||
Validators.minLength(6), Validators.pattern("^.{6,256}$")]),
|
||||
});
|
||||
|
||||
private readonly navService = inject(NavService);
|
||||
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private memberService: MemberService) {
|
||||
constructor() {
|
||||
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
|
@ -2,9 +2,16 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild, ElementRef, EventEmitter, HostListener,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input, OnChanges, Output, SimpleChange, SimpleChanges,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChange,
|
||||
SimpleChanges,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
@ -28,6 +35,7 @@ import {AbstractControl} from "@angular/forms";
|
||||
export class SettingItemComponent implements OnChanges {
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
|
||||
@Input({required:true}) title: string = '';
|
||||
@Input() editLabel: string | undefined = undefined;
|
||||
@ -98,11 +106,10 @@ export class SettingItemComponent implements OnChanges {
|
||||
if (!this.canEdit) return;
|
||||
if (this.control != null && this.control.invalid) return;
|
||||
|
||||
console.log('isEditMode', this.isEditMode, 'currentValue', change.currentValue);
|
||||
this.isEditMode = change.currentValue;
|
||||
//this.editMode.emit(this.isEditMode);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +121,27 @@ export class SettingItemComponent implements OnChanges {
|
||||
|
||||
this.isEditMode = !this.isEditMode;
|
||||
this.editMode.emit(this.isEditMode);
|
||||
this.focusInput();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
focusInput() {
|
||||
if (this.isEditMode) {
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
const inputElem = this.findFirstInput();
|
||||
if (inputElem) {
|
||||
inputElem.focus();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
private findFirstInput(): HTMLInputElement | null {
|
||||
const nativeInputs = [...this.elementRef.nativeElement.querySelectorAll('input'), ...this.elementRef.nativeElement.querySelectorAll('select'), ...this.elementRef.nativeElement.querySelectorAll('textarea')];
|
||||
if (nativeInputs.length === 0) return null;
|
||||
|
||||
return nativeInputs[0];
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +239,12 @@ export class SideNavComponent implements OnInit {
|
||||
}
|
||||
|
||||
async reorderDrop($event: CdkDragDrop<any, any, SideNavStream>) {
|
||||
// Don't allow dropping on non SideNav items
|
||||
const fixedSideNavItems = 3;
|
||||
if ($event.currentIndex < fixedSideNavItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = $event.item.data;
|
||||
// Offset the home, back, and customize button
|
||||
this.navService.updateSideNavStreamPosition(stream.name, stream.id, stream.order, $event.currentIndex - 3).subscribe({
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ng-container *transloco="let t; read:'change-age-restriction'">
|
||||
@if (user) {
|
||||
<app-setting-item [title]="t('age-restriction-label')" [canEdit]="accountService.hasChangeAgeRestrictionRole(user) || accountService.hasAdminRole(user)">
|
||||
<app-setting-item [title]="t('age-restriction-label')" [canEdit]="accountService.hasChangeAgeRestrictionRole(user)">
|
||||
<ng-template #view>
|
||||
<span class="col-12" [ngClass]="{'disabled': !accountService.hasChangeAgeRestrictionRole(user) && !accountService.hasAdminRole(user)}">{{user.ageRestriction.ageRating | ageRating }}
|
||||
@if (user.ageRestriction.ageRating !== AgeRating.NotApplicable && user.ageRestriction.includeUnknowns) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
<ng-container *transloco="let t; read:'change-email'">
|
||||
|
||||
<app-setting-item [title]="t('email-title')" [canEdit]="canEdit">
|
||||
<ng-template #extra>
|
||||
<app-setting-item [title]="t('email-title')" [canEdit]="canEdit" [isEditMode]="isEditMode" (editMode)="updateEditMode($event)">
|
||||
<ng-template #view>
|
||||
<span>{{user?.email}}</span>
|
||||
@if(emailConfirmed) {
|
||||
<i class="fa-solid fa-circle-check ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('email-confirmed')"></i>
|
||||
<span class="visually-hidden">{{t('email-confirmed')}}</span>
|
||||
@ -11,10 +12,6 @@
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #view>
|
||||
<span>{{user?.email}}</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #edit>
|
||||
@if (errors.length > 0) {
|
||||
<div class="alert alert-danger" role="alert">
|
||||
|
@ -1,55 +1,58 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {shareReplay} from 'rxjs';
|
||||
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 {ApiKeyComponent} from '../api-key/api-key.component';
|
||||
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ScrobbleProviderNamePipe} from "../../_pipes/scrobble-provider-name.pipe";
|
||||
import {SettingTitleComponent} from "../../settings/_components/setting-title/setting-title.component";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-email',
|
||||
templateUrl: './change-email.component.html',
|
||||
styleUrls: ['./change-email.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgbTooltip, ReactiveFormsModule, ApiKeyComponent, TranslocoDirective, SettingItemComponent]
|
||||
selector: 'app-change-email',
|
||||
templateUrl: './change-email.component.html',
|
||||
styleUrls: ['./change-email.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgbTooltip, ReactiveFormsModule, ApiKeyComponent, TranslocoDirective, SettingItemComponent]
|
||||
})
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
|
||||
form: FormGroup = new FormGroup({});
|
||||
user: User | undefined = undefined;
|
||||
errors: string[] = [];
|
||||
isViewMode: boolean = true;
|
||||
isEditMode: boolean = false;
|
||||
emailLink: string = '';
|
||||
emailConfirmed: boolean = true;
|
||||
hasValidEmail: boolean = true;
|
||||
canEdit: boolean = false;
|
||||
|
||||
|
||||
public get email() { return this.form.get('email'); }
|
||||
protected get email() { return this.form.get('email'); }
|
||||
|
||||
makeLink: (val: string) => string = (val: string) => {return this.emailLink};
|
||||
|
||||
constructor(public accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay()).subscribe(user => {
|
||||
this.user = user;
|
||||
this.user = user!;
|
||||
this.canEdit = !this.accountService.hasReadOnlyRole(user!);
|
||||
this.form.addControl('email', new FormControl(user?.email, [Validators.required, Validators.email]));
|
||||
this.form.addControl('password', new FormControl('', [Validators.required]));
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.accountService.isEmailConfirmed().subscribe((confirmed) => {
|
||||
this.emailConfirmed = confirmed;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.accountService.isEmailValid().subscribe(isValid => {
|
||||
this.hasValidEmail = isValid;
|
||||
this.cdRef.markForCheck();
|
||||
@ -68,42 +71,31 @@ export class ChangeEmailComponent implements OnInit {
|
||||
|
||||
const model = this.form.value;
|
||||
this.errors = [];
|
||||
|
||||
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) {
|
||||
} else if (updateEmailResponse.emailSent) {
|
||||
this.toastr.success(translate('toasts.email-sent-to'));
|
||||
} else {
|
||||
this.toastr.success(translate('toasts.change-email-no-email'));
|
||||
this.accountService.refreshAccount().subscribe(user => {
|
||||
this.user = user;
|
||||
this.form.get('email')?.setValue(this.user?.email);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
this.isViewMode = true;
|
||||
this.resetForm();
|
||||
this.accountService.refreshAccount().subscribe(user => {
|
||||
this.user = user;
|
||||
this.resetForm();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.isEditMode = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, err => {
|
||||
this.errors = err;
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
}
|
||||
|
||||
toggleViewMode() {
|
||||
this.isViewMode = !this.isViewMode;
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
updateEditMode(mode: boolean) {
|
||||
this.isViewMode = !mode;
|
||||
updateEditMode(val: boolean) {
|
||||
this.isEditMode = val;
|
||||
this.cdRef.markForCheck();
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<ng-container *transloco="let t; read:'change-password'">
|
||||
<app-setting-item [title]="t('password-label')" [canEdit]="canEdit">
|
||||
<app-setting-item [title]="t('password-label')" [canEdit]="canEdit" [isEditMode]="isEditMode" (editMode)="updateEditMode($event)">
|
||||
<ng-template #view>
|
||||
<span class="col-12">***************</span>
|
||||
</ng-template>
|
||||
|
@ -7,16 +7,13 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { map, Observable, of, shareReplay } from 'rxjs';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {map, Observable, of, shareReplay} from 'rxjs';
|
||||
import {User} from 'src/app/_models/user';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {SettingTitleComponent} from "../../settings/_components/setting-title/setting-title.component";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
|
||||
@Component({
|
||||
@ -39,7 +36,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
observableHandles: Array<any> = [];
|
||||
passwordsMatch = false;
|
||||
resetPasswordErrors: string[] = [];
|
||||
isViewMode: boolean = true;
|
||||
isEditMode: boolean = false;
|
||||
canEdit: boolean = false;
|
||||
|
||||
|
||||
@ -90,7 +87,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
this.observableHandles.push(this.accountService.resetPassword(this.user?.username, model.confirmPassword, model.oldPassword).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.password-updated'));
|
||||
this.resetPasswordForm();
|
||||
this.isViewMode = true;
|
||||
this.isEditMode = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, err => {
|
||||
this.resetPasswordErrors = err;
|
||||
@ -99,7 +96,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
updateEditMode(mode: boolean) {
|
||||
this.isViewMode = !mode;
|
||||
this.isEditMode = mode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user