Allow an admin to override a user's email address.
This commit is contained in:
parent
0407d75d91
commit
d522bccf86
6 changed files with 112 additions and 80 deletions
|
|
@ -509,6 +509,21 @@ public class AccountController : BaseApiController
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if email is changing for a non-admin user
|
||||||
|
var isUpdatingAnotherAccount = user.Id != adminUser.Id;
|
||||||
|
if (isUpdatingAnotherAccount && !string.IsNullOrEmpty(dto.Email) && user.Email != dto.Email)
|
||||||
|
{
|
||||||
|
// Validate username change
|
||||||
|
var errors = await _accountService.ValidateEmail(dto.Email);
|
||||||
|
if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "email-taken"));
|
||||||
|
|
||||||
|
user.Email = dto.Email;
|
||||||
|
user.EmailConfirmed = true; // When an admin performs the flow, we assume the email address is able to receive data
|
||||||
|
|
||||||
|
await _userManager.UpdateNormalizedEmailAsync(user);
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
// Update roles
|
// Update roles
|
||||||
var existingRoles = await _userManager.GetRolesAsync(user);
|
var existingRoles = await _userManager.GetRolesAsync(user);
|
||||||
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
|
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
|
||||||
|
|
@ -612,8 +627,7 @@ public class AccountController : BaseApiController
|
||||||
if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied"));
|
if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied"));
|
||||||
|
|
||||||
dto.Email = dto.Email.Trim();
|
dto.Email = dto.Email.Trim();
|
||||||
if (string.IsNullOrEmpty(dto.Email))
|
if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await _localizationService.Translate(userId, "invalid-payload"));
|
||||||
return BadRequest(await _localizationService.Translate(userId, "invalid-payload"));
|
|
||||||
|
|
||||||
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
||||||
|
|
||||||
|
|
@ -623,7 +637,7 @@ public class AccountController : BaseApiController
|
||||||
{
|
{
|
||||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser.UserName));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName));
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,8 @@ public record UpdateUserDto
|
||||||
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AgeRestrictionDto AgeRestriction { get; init; } = default!;
|
public AgeRestrictionDto AgeRestriction { get; init; } = default!;
|
||||||
|
/// <summary>
|
||||||
|
/// Email of the user
|
||||||
|
/// </summary>
|
||||||
|
public string? Email { get; set; } = default!;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"age-restriction-update": "There was an error updating the age restriction",
|
"age-restriction-update": "There was an error updating the age restriction",
|
||||||
"no-user": "User does not exist",
|
"no-user": "User does not exist",
|
||||||
"username-taken": "Username already taken",
|
"username-taken": "Username already taken",
|
||||||
|
"email-taken": "Email already in use",
|
||||||
"user-already-confirmed": "User is already confirmed",
|
"user-already-confirmed": "User is already confirmed",
|
||||||
"generic-user-update": "There was an exception when updating the user",
|
"generic-user-update": "There was an exception when updating the user",
|
||||||
"manual-setup-fail": "Manual setup is unable to be completed. Please cancel and recreate the invite",
|
"manual-setup-fail": "Manual setup is unable to be completed. Please cancel and recreate the invite",
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,12 @@ public class AccountService : IAccountService
|
||||||
public async Task<IEnumerable<ApiException>> ValidateEmail(string email)
|
public async Task<IEnumerable<ApiException>> ValidateEmail(string email)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
|
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
|
||||||
if (user == null) return Array.Empty<ApiException>();
|
if (user == null) return [];
|
||||||
|
|
||||||
return new List<ApiException>()
|
return
|
||||||
{
|
[
|
||||||
new ApiException(400, "Email is already registered")
|
new ApiException(400, "Email is already registered")
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,90 @@
|
||||||
<ng-container *transloco="let t; read: 'edit-user'">
|
<ng-container *transloco="let t; read: 'edit-user'">
|
||||||
<div class="modal-container">
|
<div class="modal-container">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="modal-basic-title">{{t('edit')}} {{member.username | sentenceCase}}</h5>
|
<h5 class="modal-title" id="modal-basic-title">{{t('edit')}} {{member.username | sentenceCase}}</h5>
|
||||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()">
|
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()">
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="modal-body scrollable-modal">
|
|
||||||
|
|
||||||
<form [formGroup]="userForm">
|
|
||||||
<h4>{{t('account-detail-title')}}</h4>
|
|
||||||
<div class="row g-0 mb-2">
|
|
||||||
<div class="col-md-6 col-sm-12 pe-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">{{t('username')}}</label>
|
|
||||||
<input id="username" class="form-control" formControlName="username" type="text"
|
|
||||||
[class.is-invalid]="userForm.get('username')?.invalid && userForm.get('username')?.touched" aria-describedby="username-validations">
|
|
||||||
<div id="username-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
|
||||||
<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>
|
|
||||||
<div class="col-md-6 col-sm-12">
|
|
||||||
<div class="mb-3" style="width:100%">
|
|
||||||
<label for="email" class="form-label">{{t('email')}}</label>
|
|
||||||
<input class="form-control" inputmode="email" type="email" id="email"
|
|
||||||
[class.is-invalid]="userForm.get('email')?.invalid && userForm.get('email')?.touched"
|
|
||||||
formControlName="email" aria-describedby="email-validations">
|
|
||||||
<div id="email-validations" class="invalid-feedback"
|
|
||||||
*ngIf="userForm.dirty || userForm.touched">
|
|
||||||
<div *ngIf="userForm.get('email')?.errors?.required">
|
|
||||||
{{t('required')}}
|
|
||||||
</div>
|
|
||||||
<div *ngIf="userForm.get('email')?.errors?.email">
|
|
||||||
{{t('not-valid-email')}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mb-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [isAdmin]="hasAdminRoleSelected" [member]="member"></app-restriction-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mb-3">
|
|
||||||
<div class="col-md-6 pe-4">
|
|
||||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<app-library-selector (selected)="updateLibrarySelection($event)" [member]="member"></app-library-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
|
||||||
{{t('cancel')}}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="isSaving || !userForm.valid">
|
|
||||||
<span *ngIf="isSaving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
||||||
<span>{{isSaving ? t('saving') : t('update')}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body scrollable-modal">
|
||||||
|
<form [formGroup]="userForm">
|
||||||
|
<h4>{{t('account-detail-title')}}</h4>
|
||||||
|
<div class="row g-0 mb-2">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-4">
|
||||||
|
@if(userForm.get('username'); as formControl) {
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">{{t('username')}}</label>
|
||||||
|
<input id="username" class="form-control" formControlName="username" type="text"
|
||||||
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" aria-describedby="username-validations">
|
||||||
|
@if(formControl.dirty || !formControl.untouched) {
|
||||||
|
<div id="username-validations" class="invalid-feedback">
|
||||||
|
@if (formControl.errors; as errors) {
|
||||||
|
<div>
|
||||||
|
@if (errors.required) {
|
||||||
|
{{t('required')}}
|
||||||
|
} @else if (errors.pattern) {
|
||||||
|
{{t('username-pattern', {characters: allowedCharacters})}}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
@if(userForm.get('email'); as formControl) {
|
||||||
|
<div class="mb-3" style="width:100%">
|
||||||
|
<label for="email" class="form-label">{{t('email')}}</label>
|
||||||
|
<input id="email" class="form-control" formControlName="email" type="text"
|
||||||
|
[class.is-invalid]="formControl.invalid && !formControl.untouched" aria-describedby="email-validations">
|
||||||
|
@if(formControl.dirty || !formControl.untouched) {
|
||||||
|
<div id="email-validations" class="invalid-feedback">
|
||||||
|
@if (formControl.errors; as errors) {
|
||||||
|
<div>
|
||||||
|
@if (errors.required) {
|
||||||
|
{{t('required')}}
|
||||||
|
} @else if (errors.email) {
|
||||||
|
{{t('not-valid-email')}}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<app-restriction-selector (selected)="updateRestrictionSelection($event)" [isAdmin]="hasAdminRoleSelected" [member]="member"></app-restriction-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mb-3">
|
||||||
|
<div class="col-md-6 pe-4">
|
||||||
|
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<app-library-selector (selected)="updateLibrarySelection($event)" [member]="member"></app-library-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||||
|
{{t('cancel')}}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="isSaving || !userForm.valid">
|
||||||
|
@if (isSaving) {
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
}
|
||||||
|
<span>{{isSaving ? t('saving') : t('update')}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export class EditUserComponent implements OnInit {
|
||||||
this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email]));
|
this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email]));
|
||||||
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required, Validators.pattern(AllowedUsernameCharacters)]));
|
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required, Validators.pattern(AllowedUsernameCharacters)]));
|
||||||
|
|
||||||
this.userForm.get('email')?.disable();
|
|
||||||
this.selectedRestriction = this.member.ageRestriction;
|
this.selectedRestriction = this.member.ageRestriction;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue