mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 05:34:21 -04:00
Read Only Accounts (#2658)
This commit is contained in:
parent
4f5bb57085
commit
9c84e19960
@ -382,6 +382,41 @@ public class ReaderServiceTests
|
|||||||
Assert.Equal("2", actualChapter.Range);
|
Assert.Equal("2", actualChapter.Range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//[Fact]
|
||||||
|
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges()
|
||||||
|
{
|
||||||
|
// V1 -> V2
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
var series = new SeriesBuilder("Test")
|
||||||
|
.WithVolume(new VolumeBuilder("1-2")
|
||||||
|
.WithNumber(1)
|
||||||
|
.WithChapter(new ChapterBuilder("1").Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2").Build())
|
||||||
|
.Build())
|
||||||
|
|
||||||
|
.WithVolume(new VolumeBuilder("3-4")
|
||||||
|
.WithNumber(2)
|
||||||
|
.WithChapter(new ChapterBuilder("21").Build())
|
||||||
|
.WithChapter(new ChapterBuilder("22").Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
series.Library = new LibraryBuilder("Test Lib", LibraryType.Manga).Build();
|
||||||
|
|
||||||
|
_context.Series.Add(series);
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||||
|
Assert.Equal("3-4", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats()
|
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats()
|
||||||
{
|
{
|
||||||
|
@ -35,7 +35,13 @@ public static class PolicyConstants
|
|||||||
/// Used to give a user ability to Login to their account
|
/// Used to give a user ability to Login to their account
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LoginRole = "Login";
|
public const string LoginRole = "Login";
|
||||||
|
/// <summary>
|
||||||
|
/// Restricts the ability to manage their account without an admin
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This is used explicitly for Demo Server. Not sure why it would be used in another fashion</remarks>
|
||||||
|
public const string ReadOnlyRole = "Read Only";
|
||||||
|
|
||||||
|
|
||||||
public static readonly ImmutableArray<string> ValidRoles =
|
public static readonly ImmutableArray<string> ValidRoles =
|
||||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole);
|
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole, ReadOnlyRole);
|
||||||
}
|
}
|
||||||
|
@ -77,10 +77,11 @@ public class AccountController : BaseApiController
|
|||||||
[HttpPost("reset-password")]
|
[HttpPost("reset-password")]
|
||||||
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName);
|
|
||||||
|
|
||||||
var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName);
|
var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName);
|
||||||
if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system
|
if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system
|
||||||
|
_logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName);
|
||||||
|
if (User.IsInRole(PolicyConstants.ReadOnlyRole))
|
||||||
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||||
|
|
||||||
if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin))
|
if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin))
|
||||||
@ -319,6 +320,7 @@ public class AccountController : BaseApiController
|
|||||||
public async Task<ActionResult<string>> ResetApiKey()
|
public async Task<ActionResult<string>> ResetApiKey()
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException();
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException();
|
||||||
|
if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
user.ApiKey = HashUtil.ApiKey();
|
user.ApiKey = HashUtil.ApiKey();
|
||||||
|
|
||||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||||
@ -345,7 +347,7 @@ public class AccountController : BaseApiController
|
|||||||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user == null) 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))
|
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||||
@ -450,7 +452,7 @@ public class AccountController : BaseApiController
|
|||||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
|
|
||||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||||
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
if (!await _accountService.CanChangeAgeRestriction(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
|
|
||||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||||
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||||
@ -898,7 +900,7 @@ public class AccountController : BaseApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole or PolicyConstants.ReadOnlyRole))
|
||||||
return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied"));
|
return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied"));
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
|
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
|
||||||
@ -973,6 +975,7 @@ public class AccountController : BaseApiController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
[HttpPost("resend-confirmation-email")]
|
[HttpPost("resend-confirmation-email")]
|
||||||
[EnableRateLimiting("Authentication")]
|
[EnableRateLimiting("Authentication")]
|
||||||
public async Task<ActionResult<InviteUserResponse>> ResendConfirmationSendEmail([FromQuery] int userId)
|
public async Task<ActionResult<InviteUserResponse>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||||
|
@ -128,7 +128,7 @@ public class SettingsController : BaseApiController
|
|||||||
/// Is the minimum information setup for Email to work
|
/// Is the minimum information setup for Email to work
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet("is-email-setup")]
|
[HttpGet("is-email-setup")]
|
||||||
public async Task<ActionResult<bool>> IsEmailSetup()
|
public async Task<ActionResult<bool>> IsEmailSetup()
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A String representation of the volume number. Allows for floats.
|
/// A String representation of the volume number. Allows for floats. Can also include a range (1-2).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>For Books with Series_index, this will map to the Series Index.</remarks>
|
/// <remarks>For Books with Series_index, this will map to the Series Index.</remarks>
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
@ -26,7 +26,7 @@ public interface IAccountService
|
|||||||
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
||||||
Task<bool> HasBookmarkPermission(AppUser? user);
|
Task<bool> HasBookmarkPermission(AppUser? user);
|
||||||
Task<bool> HasDownloadPermission(AppUser? user);
|
Task<bool> HasDownloadPermission(AppUser? user);
|
||||||
Task<bool> HasChangeRestrictionRole(AppUser? user);
|
Task<bool> CanChangeAgeRestriction(AppUser? user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AccountService : IAccountService
|
public class AccountService : IAccountService
|
||||||
@ -128,14 +128,15 @@ public class AccountService : IAccountService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Does the user have Change Restriction permission or admin rights
|
/// Does the user have Change Restriction permission or admin rights and not Read Only
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<bool> HasChangeRestrictionRole(AppUser? user)
|
public async Task<bool> CanChangeAgeRestriction(AppUser? user)
|
||||||
{
|
{
|
||||||
if (user == null) return false;
|
if (user == null) return false;
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
|
if (roles.Contains(PolicyConstants.ReadOnlyRole)) return false;
|
||||||
return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole);
|
return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@ export enum Role {
|
|||||||
ChangePassword = 'Change Password',
|
ChangePassword = 'Change Password',
|
||||||
Bookmark = 'Bookmark',
|
Bookmark = 'Bookmark',
|
||||||
Download = 'Download',
|
Download = 'Download',
|
||||||
ChangeRestriction = 'Change Restriction'
|
ChangeRestriction = 'Change Restriction',
|
||||||
|
ReadOnly = 'Read Only'
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -80,6 +81,10 @@ export class AccountService {
|
|||||||
return user && user.roles.includes(Role.Bookmark);
|
return user && user.roles.includes(Role.Bookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasReadOnlyRole(user: User) {
|
||||||
|
return user && user.roles.includes(Role.ReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
getRoles() {
|
getRoles() {
|
||||||
return this.httpClient.get<string[]>(this.baseUrl + 'account/roles');
|
return this.httpClient.get<string[]>(this.baseUrl + 'account/roles');
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {ChangeDetectorRef, Component, DestroyRef, HostListener, inject, Inject, OnInit} from '@angular/core';
|
import {ChangeDetectorRef, Component, DestroyRef, HostListener, inject, Inject, OnInit} from '@angular/core';
|
||||||
import {NavigationStart, Router, RouterOutlet} from '@angular/router';
|
import {NavigationStart, Router, RouterOutlet} from '@angular/router';
|
||||||
import {map, shareReplay, take} from 'rxjs/operators';
|
import {map, shareReplay, take, tap} from 'rxjs/operators';
|
||||||
import { AccountService } from './_services/account.service';
|
import { AccountService } from './_services/account.service';
|
||||||
import { LibraryService } from './_services/library.service';
|
import { LibraryService } from './_services/library.service';
|
||||||
import { NavService } from './_services/nav.service';
|
import { NavService } from './_services/nav.service';
|
||||||
@ -67,8 +67,21 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Every hour, have the UI check for an update. People seriously stay out of date
|
||||||
|
// interval(60 * 60 * 1000) // 60 minutes in milliseconds
|
||||||
|
// .pipe(
|
||||||
|
// switchMap(() => this.accountService.currentUser$),
|
||||||
|
// filter(u => u !== undefined && this.accountService.hasAdminRole(u)),
|
||||||
|
// switchMap(_ => this.serverService.checkForUpdates())
|
||||||
|
// )
|
||||||
|
// .subscribe();
|
||||||
|
|
||||||
this.transitionState$ = this.accountService.currentUser$.pipe(map((user) => {
|
|
||||||
|
this.transitionState$ = this.accountService.currentUser$.pipe(
|
||||||
|
tap(user => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
map((user) => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
return user.preferences.noTransitions;
|
return user.preferences.noTransitions;
|
||||||
}), takeUntilDestroyed(this.destroyRef));
|
}), takeUntilDestroyed(this.destroyRef));
|
||||||
|
@ -26,23 +26,27 @@ import {translate, TranslocoDirective} from "@ngneat/transloco";
|
|||||||
})
|
})
|
||||||
export class ApiKeyComponent implements OnInit {
|
export class ApiKeyComponent implements OnInit {
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly confirmService = inject(ConfirmService);
|
||||||
|
private readonly accountService = inject(AccountService);
|
||||||
|
private readonly toastr = inject(ToastrService);
|
||||||
|
private readonly clipboard = inject(Clipboard);
|
||||||
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
@Input() title: string = 'API Key';
|
@Input() title: string = 'API Key';
|
||||||
@Input() showRefresh: boolean = true;
|
@Input() showRefresh: boolean = true;
|
||||||
@Input() transform: (val: string) => string = (val: string) => val;
|
@Input() transform: (val: string) => string = (val: string) => val;
|
||||||
@Input() tooltipText: string = '';
|
@Input() tooltipText: string = '';
|
||||||
@Input() hideData = true;
|
@Input() hideData = true;
|
||||||
@ViewChild('apiKey') inputElem!: ElementRef;
|
@ViewChild('apiKey') inputElem!: ElementRef;
|
||||||
key: string = '';
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
|
||||||
|
|
||||||
|
key: string = '';
|
||||||
isDataHidden: boolean = this.hideData;
|
isDataHidden: boolean = this.hideData;
|
||||||
|
|
||||||
get InputType() {
|
get InputType() {
|
||||||
return (this.hideData && this.isDataHidden) ? 'password' : 'text';
|
return (this.hideData && this.isDataHidden) ? 'password' : 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService, private clipboard: Clipboard,
|
|
||||||
private readonly cdRef: ChangeDetectorRef) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||||
@ -53,6 +57,8 @@ export class ApiKeyComponent implements OnInit {
|
|||||||
key = translate('api-key.no-key');
|
key = translate('api-key.no-key');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.showRefresh = !this.accountService.hasReadOnlyRole(user!);
|
||||||
|
|
||||||
if (this.transform != undefined) {
|
if (this.transform != undefined) {
|
||||||
this.key = this.transform(key);
|
this.key = this.transform(key);
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="container-fluid row mb-2">
|
<div class="container-fluid row mb-2">
|
||||||
<div class="col-10 col-sm-11"><h4 id="age-restriction">{{t('age-restriction-label')}}</h4></div>
|
<div class="col-10 col-sm-11"><h4 id="age-restriction">{{t('age-restriction-label')}}</h4></div>
|
||||||
<div class="col-1 text-end">
|
<div class="col-1 text-end">
|
||||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" *ngIf="(hasChangeAgeRestrictionAbility | async)">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" [disabled]="!(hasChangeAgeRestrictionAbility | async)">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,7 +50,7 @@ export class ChangeAgeRestrictionComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.hasChangeAgeRestrictionAbility = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), map(user => {
|
this.hasChangeAgeRestrictionAbility = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), map(user => {
|
||||||
return user !== undefined && (!this.accountService.hasAdminRole(user) && this.accountService.hasChangeAgeRestrictionRole(user));
|
return user !== undefined && !this.accountService.hasReadOnlyRole(user) && (!this.accountService.hasAdminRole(user) && this.accountService.hasChangeAgeRestrictionRole(user));
|
||||||
}));
|
}));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1 text-end">
|
<div class="col-1 text-end">
|
||||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? 'Edit' : 'Cancel'}}</button>
|
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" [disabled]="!canEdit">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,6 +29,7 @@ export class ChangeEmailComponent implements OnInit {
|
|||||||
emailLink: string = '';
|
emailLink: string = '';
|
||||||
emailConfirmed: boolean = true;
|
emailConfirmed: boolean = true;
|
||||||
hasValidEmail: boolean = true;
|
hasValidEmail: boolean = true;
|
||||||
|
canEdit: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
public get email() { return this.form.get('email'); }
|
public get email() { return this.form.get('email'); }
|
||||||
@ -38,8 +39,9 @@ export class ChangeEmailComponent implements OnInit {
|
|||||||
constructor(public accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
constructor(public accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), take(1)).subscribe(user => {
|
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('email', new FormControl(user?.email, [Validators.required, Validators.email]));
|
||||||
this.form.addControl('password', new FormControl('', [Validators.required]));
|
this.form.addControl('password', new FormControl('', [Validators.required]));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="container-fluid row mb-2">
|
<div class="container-fluid row mb-2">
|
||||||
<div class="col-10 col-sm-11"><h4>{{t('password-label')}}</h4></div>
|
<div class="col-10 col-sm-11"><h4>{{t('password-label')}}</h4></div>
|
||||||
<div class="col-1 text-end">
|
<div class="col-1 text-end">
|
||||||
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" *ngIf="(hasChangePasswordAbility | async)">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()" [disabled]="!(hasChangePasswordAbility | async)">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,13 +44,13 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay()).subscribe(user => {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.hasChangePasswordAbility = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), map(user => {
|
this.hasChangePasswordAbility = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), map(user => {
|
||||||
return user !== undefined && (this.accountService.hasAdminRole(user) || this.accountService.hasChangePasswordRole(user));
|
return user !== undefined && !this.accountService.hasReadOnlyRole(user) && (this.accountService.hasAdminRole(user) || this.accountService.hasChangePasswordRole(user));
|
||||||
}));
|
}));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import {take, tap} from 'rxjs/operators';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import {
|
import {
|
||||||
readingDirections,
|
readingDirections,
|
||||||
@ -129,7 +129,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
opdsUrl: string = '';
|
opdsUrl: string = '';
|
||||||
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
||||||
hasActiveLicense = false;
|
hasActiveLicense = false;
|
||||||
|
canEdit = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -142,6 +142,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settingsService.getOpdsEnabled().subscribe(res => {
|
||||||
|
this.opdsEnabled = res;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
this.localizationService.getLocales().subscribe(res => {
|
this.localizationService.getLocales().subscribe(res => {
|
||||||
this.locales = res;
|
this.locales = res;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
@ -149,7 +154,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.accountService.hasValidLicense$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
this.accountService.hasValidLicense$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
||||||
if (res) {
|
if (res) {
|
||||||
this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling});
|
this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling});
|
||||||
this.hasActiveLicense = true;
|
this.hasActiveLicense = true;
|
||||||
@ -169,10 +174,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.settingsService.getOpdsEnabled().subscribe(res => {
|
|
||||||
this.opdsEnabled = res;
|
|
||||||
this.cdRef.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
83
openapi.json
83
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.13.5"
|
"version": "0.7.13.6"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -9560,37 +9560,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/Server/accessible": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Server"
|
|
||||||
],
|
|
||||||
"summary": "Is this server accessible to the outside net",
|
|
||||||
"description": "If the instance has the HostName set, this will return true whether or not it is accessible externally",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"text/plain": {
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/Server/jobs": {
|
"/api/Server/jobs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -10116,6 +10085,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/Settings/test-email-url": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Settings"
|
||||||
|
],
|
||||||
|
"summary": "Sends a test email to see if email settings are hooked up correctly",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/EmailTestResultDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/EmailTestResultDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/EmailTestResultDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/Stats/user/{userId}/read": {
|
"/api/Stats/user/{userId}/read": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -14765,6 +14764,24 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"EmailTestResultDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"successful": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"errorMessage": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"emailAddress": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Represents if Test Email Service URL was successful or not and if any error occured"
|
||||||
|
},
|
||||||
"ExternalRating": {
|
"ExternalRating": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -20222,7 +20239,7 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A String representation of the volume number. Allows for floats.",
|
"description": "A String representation of the volume number. Allows for floats. Can also include a range (1-2).",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user