mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Create Users Manually (Email still required) (#1381)
* Implemented a manual button to allow users to setup an account, even after they invited. Updated error toast to put "Error" in the title of the toast. * Updated the exception middleware to always send full context instead of generic "Internal Server Error"
This commit is contained in:
parent
63d74ecf9a
commit
1d806bf622
@ -348,6 +348,24 @@ namespace API.Controllers
|
|||||||
return BadRequest("There was an exception when updating the user");
|
return BadRequest("There was an exception when updating the user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests the Invite Url for the UserId. Will return error if user is already validated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="withBaseUrl">Include the "https://ip:port/" in the generated link</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpGet("invite-url")]
|
||||||
|
public async Task<ActionResult<string>> GetInviteUrl(int userId, bool withBaseUrl)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
|
if (user.EmailConfirmed)
|
||||||
|
return BadRequest("User is already confirmed");
|
||||||
|
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||||
|
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
||||||
|
|
||||||
|
return GenerateEmailLink(user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -428,12 +446,10 @@ namespace API.Controllers
|
|||||||
lib.AppUsers.Add(user);
|
lib.AppUsers.Add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _unitOfWork.CommitAsync();
|
|
||||||
|
|
||||||
|
|
||||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue sending email");
|
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue sending email");
|
||||||
|
|
||||||
|
|
||||||
var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email);
|
var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email);
|
||||||
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||||
@ -447,6 +463,11 @@ namespace API.Controllers
|
|||||||
ServerConfirmationLink = emailLink
|
ServerConfirmationLink = emailLink
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.ConfirmationToken = token;
|
||||||
|
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
return Ok(new InviteUserResponse
|
return Ok(new InviteUserResponse
|
||||||
{
|
{
|
||||||
EmailLink = emailLink,
|
EmailLink = emailLink,
|
||||||
@ -486,6 +507,7 @@ namespace API.Controllers
|
|||||||
if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
|
if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
|
||||||
|
|
||||||
user.UserName = dto.Username;
|
user.UserName = dto.Username;
|
||||||
|
user.ConfirmationToken = null;
|
||||||
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
|
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
|
||||||
if (errors.Any())
|
if (errors.Any())
|
||||||
{
|
{
|
||||||
@ -617,12 +639,11 @@ namespace API.Controllers
|
|||||||
return Ok(emailLink);
|
return Ok(emailLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GenerateEmailLink(string token, string routePart, string email)
|
private string GenerateEmailLink(string token, string routePart, string email, bool withHost = true)
|
||||||
{
|
{
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||||
var emailLink =
|
if (withHost) return $"{Request.Scheme}://{host}{Request.PathBase}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||||
$"{Request.Scheme}://{host}{Request.PathBase}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||||
return emailLink;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
1579
API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs
generated
Normal file
1579
API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
API/Data/Migrations/20220717145254_UserConfirmationLink.cs
Normal file
25
API/Data/Migrations/20220717145254_UserConfirmationLink.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class UserConfirmationLink : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ConfirmationToken",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ConfirmationToken",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -60,6 +60,9 @@ namespace API.Data.Migrations
|
|||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConfirmationToken")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<DateTime>("Created")
|
b.Property<DateTime>("Created")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ namespace API.Entities
|
|||||||
/// An API Key to interact with external services, like OPDS
|
/// An API Key to interact with external services, like OPDS
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The confirmation token for the user (invite). This will be set to null after the user confirms.
|
||||||
|
/// </summary>
|
||||||
|
public string ConfirmationToken { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -35,9 +35,9 @@ namespace API.Middleware
|
|||||||
context.Response.ContentType = "application/json";
|
context.Response.ContentType = "application/json";
|
||||||
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
|
||||||
var response = _env.IsDevelopment()
|
var errorMessage = string.IsNullOrEmpty(ex.Message) ? "Internal Server Error" : ex.Message;
|
||||||
? new ApiException(context.Response.StatusCode, ex.Message, ex.StackTrace)
|
|
||||||
: new ApiException(context.Response.StatusCode, "Internal Server Error", ex.StackTrace);
|
var response = new ApiException(context.Response.StatusCode, errorMessage, ex.StackTrace);
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
@ -85,9 +85,9 @@ export class ErrorInterceptor implements HttpInterceptor {
|
|||||||
this.toastr.error('There was an issue downloading this file or you do not have permissions', error.status);
|
this.toastr.error('There was an issue downloading this file or you do not have permissions', error.status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.toastr.error(error.error, error.status);
|
this.toastr.error(error.error, error.status + ' Error');
|
||||||
} else {
|
} else {
|
||||||
this.toastr.error(error.statusText === 'OK' ? error.error : error.statusText, error.status);
|
this.toastr.error(error.statusText === 'OK' ? error.error : error.statusText, error.status + ' Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,15 @@ export class AccountService implements OnDestroy {
|
|||||||
return this.httpClient.post<User>(this.baseUrl + 'account/confirm-email', model);
|
return this.httpClient.post<User>(this.baseUrl + 'account/confirm-email', model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a user id, returns a full url for setting up the user account
|
||||||
|
* @param userId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getInviteUrl(userId: number, withBaseUrl: boolean = true) {
|
||||||
|
return this.httpClient.get<string>(this.baseUrl + 'account/invite-url?userId=' + userId + '&withBaseUrl=' + withBaseUrl, {responseType: 'text' as 'json'});
|
||||||
|
}
|
||||||
|
|
||||||
getDecodedToken(token: string) {
|
getDecodedToken(token: string) {
|
||||||
return JSON.parse(atob(token.split('.')[1]));
|
return JSON.parse(atob(token.split('.')[1]));
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
|
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(invite)">Cancel</button>
|
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(invite)">Cancel</button>
|
||||||
<button class="btn btn-secondary btn-sm" (click)="resendEmail(invite)">Resend</button>
|
<button class="btn btn-secondary btn-sm me-2" (click)="resendEmail(invite)">Resend</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" (click)="setup(invite)">Setup</button>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { take } from 'rxjs/operators';
|
import { catchError, take } from 'rxjs/operators';
|
||||||
import { MemberService } from 'src/app/_services/member.service';
|
import { MemberService } from 'src/app/_services/member.service';
|
||||||
import { Member } from 'src/app/_models/member';
|
import { Member } from 'src/app/_models/member';
|
||||||
import { User } from 'src/app/_models/user';
|
import { User } from 'src/app/_models/user';
|
||||||
@ -13,6 +13,7 @@ import { MessageHubService } from 'src/app/_services/message-hub.service';
|
|||||||
import { InviteUserComponent } from '../invite-user/invite-user.component';
|
import { InviteUserComponent } from '../invite-user/invite-user.component';
|
||||||
import { EditUserComponent } from '../edit-user/edit-user.component';
|
import { EditUserComponent } from '../edit-user/edit-user.component';
|
||||||
import { ServerService } from 'src/app/_services/server.service';
|
import { ServerService } from 'src/app/_services/server.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-manage-users',
|
selector: 'app-manage-users',
|
||||||
@ -34,7 +35,8 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||||||
private toastr: ToastrService,
|
private toastr: ToastrService,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
public messageHub: MessageHubService,
|
public messageHub: MessageHubService,
|
||||||
private serverService: ServerService) {
|
private serverService: ServerService,
|
||||||
|
private router: Router) {
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe((user) => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe((user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.loggedInUsername = user.username;
|
this.loggedInUsername = user.username;
|
||||||
@ -122,7 +124,6 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resendEmail(member: Member) {
|
resendEmail(member: Member) {
|
||||||
|
|
||||||
this.serverService.isServerAccessible().subscribe(canAccess => {
|
this.serverService.isServerAccessible().subscribe(canAccess => {
|
||||||
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
|
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
|
||||||
if (canAccess) {
|
if (canAccess) {
|
||||||
@ -134,7 +135,15 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(member: Member) {
|
||||||
|
this.accountService.getInviteUrl(member.id, false).subscribe(url => {
|
||||||
|
console.log('Url: ', url);
|
||||||
|
if (url) {
|
||||||
|
this.router.navigateByUrl(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePassword(member: Member) {
|
updatePassword(member: Member) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user