mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Invitation process improvements (#6188)
* Server: Make sending email optional when adding user Closes #6158. * Generate custom invite token and store it in user blob Closes btcpayserver/app/#46. * QR code for user invite Closes #6157. * Text fix
This commit is contained in:
@@ -46,5 +46,6 @@ namespace BTCPayServer.Data
|
|||||||
public bool ShowInvoiceStatusChangeHint { get; set; }
|
public bool ShowInvoiceStatusChangeHint { get; set; }
|
||||||
public string ImageUrl { get; set; }
|
public string ImageUrl { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public string InvitationToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ namespace BTCPayServer.Tests
|
|||||||
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
||||||
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||||
s.ClickPagePrimary();
|
s.ClickPagePrimary();
|
||||||
var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text;
|
var url = s.Driver.FindElement(By.Id("InvitationUrl")).GetAttribute("data-text");
|
||||||
|
|
||||||
s.Logout();
|
s.Logout();
|
||||||
s.Driver.Navigate().GoToUrl(url);
|
s.Driver.Navigate().GoToUrl(url);
|
||||||
|
|||||||
@@ -812,7 +812,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _userManager.FindByInvitationTokenAsync(userId, Uri.UnescapeDataString(code));
|
var user = await _userManager.FindByInvitationTokenAsync<ApplicationUser>(userId, Uri.UnescapeDataString(code));
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -827,6 +827,9 @@ namespace BTCPayServer.Controllers
|
|||||||
RequestUri = Request.GetAbsoluteRootUri()
|
RequestUri = Request.GetAbsoluteRootUri()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// unset used token
|
||||||
|
await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||||
|
|
||||||
if (requiresEmailConfirmation)
|
if (requiresEmailConfirmation)
|
||||||
{
|
{
|
||||||
return await RedirectToConfirmEmail(user);
|
return await RedirectToConfirmEmail(user);
|
||||||
|
|||||||
@@ -58,18 +58,28 @@ namespace BTCPayServer.Controllers
|
|||||||
.Skip(model.Skip)
|
.Skip(model.Skip)
|
||||||
.Take(model.Count)
|
.Take(model.Count)
|
||||||
.ToListAsync())
|
.ToListAsync())
|
||||||
.Select(u => new UsersViewModel.UserViewModel
|
.Select(u =>
|
||||||
{
|
{
|
||||||
Name = u.GetBlob()?.Name,
|
var blob = u.GetBlob();
|
||||||
ImageUrl = u.GetBlob()?.ImageUrl,
|
return new UsersViewModel.UserViewModel
|
||||||
Email = u.Email,
|
{
|
||||||
Id = u.Id,
|
Name = blob?.Name,
|
||||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
ImageUrl = blob?.ImageUrl,
|
||||||
Approved = u.RequiresApproval ? u.Approved : null,
|
Email = u.Email,
|
||||||
Created = u.Created,
|
Id = u.Id,
|
||||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
InvitationUrl =
|
||||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
string.IsNullOrEmpty(blob?.InvitationToken)
|
||||||
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
? null
|
||||||
|
: _linkGenerator.InvitationLink(u.Id, blob.InvitationToken, Request.Scheme,
|
||||||
|
Request.Host, Request.PathBase),
|
||||||
|
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||||
|
Approved = u.RequiresApproval ? u.Approved : null,
|
||||||
|
Created = u.Created,
|
||||||
|
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||||
|
Disabled = u.LockoutEnabled && u.LockoutEnd != null &&
|
||||||
|
DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
||||||
|
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
return View(model);
|
return View(model);
|
||||||
@@ -88,6 +98,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Email = user.Email,
|
Email = user.Email,
|
||||||
Name = blob?.Name,
|
Name = blob?.Name,
|
||||||
|
InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _linkGenerator.InvitationLink(user.Id, blob.InvitationToken, Request.Scheme, Request.Host, Request.PathBase),
|
||||||
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
|
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
|
||||||
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||||
Approved = user.RequiresApproval ? user.Approved : null,
|
Approved = user.RequiresApproval ? user.Approved : null,
|
||||||
@@ -200,16 +211,20 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("server/users/new")]
|
[HttpGet("server/users/new")]
|
||||||
public IActionResult CreateUser()
|
public async Task<IActionResult> CreateUser()
|
||||||
{
|
{
|
||||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
await PrepareCreateUserViewData();
|
||||||
return View();
|
var vm = new RegisterFromAdminViewModel
|
||||||
|
{
|
||||||
|
SendInvitationEmail = ViewData["CanSendEmail"] is true
|
||||||
|
};
|
||||||
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("server/users/new")]
|
[HttpPost("server/users/new")]
|
||||||
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
||||||
{
|
{
|
||||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
await PrepareCreateUserViewData();
|
||||||
if (!_Options.CheatMode)
|
if (!_Options.CheatMode)
|
||||||
model.IsAdmin = false;
|
model.IsAdmin = false;
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
@@ -236,6 +251,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var tcs = new TaskCompletionSource<Uri>();
|
var tcs = new TaskCompletionSource<Uri>();
|
||||||
var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
|
var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
|
||||||
|
var sendEmail = model.SendInvitationEmail && ViewData["CanSendEmail"] is true;
|
||||||
|
|
||||||
_eventAggregator.Publish(new UserRegisteredEvent
|
_eventAggregator.Publish(new UserRegisteredEvent
|
||||||
{
|
{
|
||||||
@@ -243,23 +259,23 @@ namespace BTCPayServer.Controllers
|
|||||||
Kind = UserRegisteredEventKind.Invite,
|
Kind = UserRegisteredEventKind.Invite,
|
||||||
User = user,
|
User = user,
|
||||||
InvitedByUser = currentUser,
|
InvitedByUser = currentUser,
|
||||||
|
SendInvitationEmail = sendEmail,
|
||||||
Admin = model.IsAdmin,
|
Admin = model.IsAdmin,
|
||||||
CallbackUrlGenerated = tcs
|
CallbackUrlGenerated = tcs
|
||||||
});
|
});
|
||||||
|
|
||||||
var callbackUrl = await tcs.Task;
|
var callbackUrl = await tcs.Task;
|
||||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
var info = sendEmail
|
||||||
var info = settings.IsComplete()
|
? "An invitation email has been sent. You may alternatively"
|
||||||
? "An invitation email has been sent.<br/>You may alternatively"
|
: "An invitation email has not been sent. You need to";
|
||||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
|
||||||
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
{
|
{
|
||||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
AllowDismiss = false,
|
AllowDismiss = false,
|
||||||
Html = $"Account successfully created. {info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
Html = $"Account successfully created. {info} share this link with them:<br/>{callbackUrl}"
|
||||||
});
|
});
|
||||||
return RedirectToAction(nameof(ListUsers));
|
return RedirectToAction(nameof(User), new { userId = user.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var error in result.Errors)
|
foreach (var error in result.Errors)
|
||||||
@@ -391,6 +407,13 @@ namespace BTCPayServer.Controllers
|
|||||||
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
|
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
|
||||||
return RedirectToAction(nameof(ListUsers));
|
return RedirectToAction(nameof(ListUsers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PrepareCreateUserViewData()
|
||||||
|
{
|
||||||
|
var emailSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||||
|
ViewData["CanSendEmail"] = emailSettings.IsComplete();
|
||||||
|
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RegisterFromAdminViewModel
|
public class RegisterFromAdminViewModel
|
||||||
@@ -415,5 +438,8 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
[Display(Name = "Email confirmed?")]
|
[Display(Name = "Email confirmed?")]
|
||||||
public bool EmailConfirmed { get; set; }
|
public bool EmailConfirmed { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Send invitation email")]
|
||||||
|
public bool SendInvitationEmail { get; set; } = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class UserRegisteredEvent
|
|||||||
public UserRegisteredEventKind Kind { get; set; } = UserRegisteredEventKind.Registration;
|
public UserRegisteredEventKind Kind { get; set; } = UserRegisteredEventKind.Registration;
|
||||||
public Uri RequestUri { get; set; }
|
public Uri RequestUri { get; set; }
|
||||||
public ApplicationUser InvitedByUser { get; set; }
|
public ApplicationUser InvitedByUser { get; set; }
|
||||||
|
public bool SendInvitationEmail { get; set; }
|
||||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,11 +73,12 @@ public class UserEventHostedService(
|
|||||||
emailSender = await emailSenderFactory.GetEmailSender();
|
emailSender = await emailSenderFactory.GetEmailSender();
|
||||||
if (isInvite)
|
if (isInvite)
|
||||||
{
|
{
|
||||||
code = await userManager.GenerateInvitationTokenAsync(user);
|
code = await userManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||||
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||||
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||||
|
|
||||||
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
|
if (ev.SendInvitationEmail)
|
||||||
|
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
|
||||||
}
|
}
|
||||||
else if (requiresEmailConfirmation)
|
else if (requiresEmailConfirmation)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
[Display(Name = "Invitation URL")]
|
||||||
|
public string InvitationUrl { get; set; }
|
||||||
[Display(Name = "Image")]
|
[Display(Name = "Image")]
|
||||||
public IFormFile ImageFile { get; set; }
|
public IFormFile ImageFile { get; set; }
|
||||||
public string ImageUrl { get; set; }
|
public string ImageUrl { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
@@ -19,15 +21,35 @@ namespace BTCPayServer
|
|||||||
return await userManager.FindByIdAsync(idOrEmail);
|
return await userManager.FindByIdAsync(idOrEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string> GenerateInvitationTokenAsync<TUser>(this UserManager<TUser> userManager, TUser user) where TUser : class
|
public static async Task<string?> GenerateInvitationTokenAsync<TUser>(this UserManager<ApplicationUser> userManager, string userId) where TUser : class
|
||||||
{
|
{
|
||||||
return await userManager.GenerateUserTokenAsync(user, InvitationTokenProviderOptions.ProviderName, InvitationPurpose);
|
var token = Guid.NewGuid().ToString("n")[..12];
|
||||||
|
return await userManager.SetInvitationTokenAsync<TUser>(userId, token) ? token : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<TUser?> FindByInvitationTokenAsync<TUser>(this UserManager<TUser> userManager, string userId, string token) where TUser : class
|
public static async Task<bool> UnsetInvitationTokenAsync<TUser>(this UserManager<ApplicationUser> userManager, string userId) where TUser : class
|
||||||
|
{
|
||||||
|
return await userManager.SetInvitationTokenAsync<TUser>(userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> SetInvitationTokenAsync<TUser>(this UserManager<ApplicationUser> userManager, string userId, string? token) where TUser : class
|
||||||
{
|
{
|
||||||
var user = await userManager.FindByIdAsync(userId);
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
var isValid = user is not null && await userManager.VerifyUserTokenAsync(user, InvitationTokenProviderOptions.ProviderName, InvitationPurpose, token);
|
if (user == null) return false;
|
||||||
|
var blob = user.GetBlob() ?? new UserBlob();
|
||||||
|
blob.InvitationToken = token;
|
||||||
|
user.SetBlob(blob);
|
||||||
|
await userManager.UpdateAsync(user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ApplicationUser?> FindByInvitationTokenAsync<TUser>(this UserManager<ApplicationUser> userManager, string userId, string token) where TUser : class
|
||||||
|
{
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
var isValid = user is not null && (
|
||||||
|
user.GetBlob()?.InvitationToken == token ||
|
||||||
|
// backwards-compatibility with old tokens
|
||||||
|
await userManager.VerifyUserTokenAsync(user, InvitationTokenProviderOptions.ProviderName, InvitationPurpose, token));
|
||||||
return isValid ? user : null;
|
return isValid ? user : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
@using BTCPayServer.TagHelpers
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@model BTCPayServer.Controllers.RegisterFromAdminViewModel
|
@model BTCPayServer.Controllers.RegisterFromAdminViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(ServerNavPages.Users, "Create account");
|
ViewData.SetActivePage(ServerNavPages.Users, "Create account");
|
||||||
|
var canSendEmail = ViewData["CanSendEmail"] is true;
|
||||||
}
|
}
|
||||||
|
|
||||||
<form method="post" asp-action="CreateUser">
|
<form method="post" asp-action="CreateUser">
|
||||||
@@ -55,6 +58,17 @@
|
|||||||
<span asp-validation-for="EmailConfirmed" class="text-danger"></span>
|
<span asp-validation-for="EmailConfirmed" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<input asp-for="SendInvitationEmail" type="checkbox" class="btcpay-toggle me-3" disabled="@(canSendEmail ? null : "disabled")" />
|
||||||
|
<div>
|
||||||
|
<label asp-for="SendInvitationEmail" class="form-check-label"></label>
|
||||||
|
<span asp-validation-for="SendInvitationEmail" class="text-danger"></span>
|
||||||
|
@if (!canSendEmail)
|
||||||
|
{
|
||||||
|
<div class="text-secondary">Your email server has not been configured. <a asp-controller="UIServer" asp-action="Emails">Please configure it first.</a></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
{ Disabled: true } => ("Disabled", "danger"),
|
{ Disabled: true } => ("Disabled", "danger"),
|
||||||
{ Approved: false } => ("Pending Approval", "warning"),
|
{ Approved: false } => ("Pending Approval", "warning"),
|
||||||
{ EmailConfirmed: false } => ("Pending Email Verification", "warning"),
|
{ EmailConfirmed: false } => ("Pending Email Verification", "warning"),
|
||||||
|
{ InvitationUrl: not null } => ("Pending Invitation", "warning"),
|
||||||
_ => ("Active", "success")
|
_ => ("Active", "success")
|
||||||
};
|
};
|
||||||
var detailsId = $"user_details_{user.Id}";
|
var detailsId = $"user_details_{user.Id}";
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>@user.Stores.Count() Store@(user.Stores.Count() == 1 ? "" : "s")</td>
|
<td class="@(user.Stores.Any() ? null : "text-muted")">@user.Stores.Count() Store@(user.Stores.Count() == 1 ? "" : "s")</td>
|
||||||
<td>@user.Created?.ToBrowserDate()</td>
|
<td>@user.Created?.ToBrowserDate()</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="user-status badge bg-@status.Item2">@status.Item1</span>
|
<span class="user-status badge bg-@status.Item2">@status.Item1</span>
|
||||||
@@ -82,11 +83,11 @@
|
|||||||
{
|
{
|
||||||
<a asp-action="ApproveUser" asp-route-userId="@user.Id" asp-route-approved="true" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Approve user" data-description="This will approve the user <strong>@Html.Encode(user.Email)</strong>." data-confirm="Approve">Approve</a>
|
<a asp-action="ApproveUser" asp-route-userId="@user.Id" asp-route-approved="true" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Approve user" data-description="This will approve the user <strong>@Html.Encode(user.Email)</strong>." data-confirm="Approve">Approve</a>
|
||||||
}
|
}
|
||||||
|
<a asp-action="User" asp-route-userId="@user.Id" class="user-edit">Edit</a>
|
||||||
@if (status.Item2 != "warning")
|
@if (status.Item2 != "warning")
|
||||||
{
|
{
|
||||||
<a asp-action="ToggleUser" asp-route-userId="@user.Id" asp-route-enable="@user.Disabled">@(user.Disabled ? "Enable" : "Disable")</a>
|
<a asp-action="ToggleUser" asp-route-userId="@user.Id" asp-route-enable="@user.Disabled">@(user.Disabled ? "Enable" : "Disable")</a>
|
||||||
}
|
}
|
||||||
<a asp-action="User" asp-route-userId="@user.Id" class="user-edit">Edit</a>
|
|
||||||
<a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
<a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -100,7 +101,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr id="@detailsId" class="user-details-row collapse">
|
<tr id="@detailsId" class="user-details-row collapse">
|
||||||
<td colspan="6" class="border-top-0">
|
<td colspan="6" class="border-top-0">
|
||||||
@if (user.Stores.Any())
|
@if (!string.IsNullOrEmpty(user.InvitationUrl))
|
||||||
|
{
|
||||||
|
<div class="payment-box m-0">
|
||||||
|
<div class="qr-container">
|
||||||
|
<vc:qr-code data="@user.InvitationUrl" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mt-3">
|
||||||
|
<div class="form-floating">
|
||||||
|
<vc:truncate-center text="@user.InvitationUrl" padding="15" elastic="true" classes="form-control-plaintext"/>
|
||||||
|
<label>Invitation URL</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (user.Stores.Any())
|
||||||
{
|
{
|
||||||
<ul class="mb-0 p-0">
|
<ul class="mb-0 p-0">
|
||||||
@foreach (var store in user.Stores)
|
@foreach (var store in user.Stores)
|
||||||
@@ -118,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-secondary">No stores</span>
|
<span class="text-secondary">No stores</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -21,6 +21,22 @@
|
|||||||
<button id="page-primary" name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
<button id="page-primary" name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<partial name="_StatusMessage" />
|
<partial name="_StatusMessage" />
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Model.InvitationUrl))
|
||||||
|
{
|
||||||
|
<div class="payment-box mx-0 mb-5">
|
||||||
|
<div class="qr-container">
|
||||||
|
<vc:qr-code data="@Model.InvitationUrl" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mt-3">
|
||||||
|
<div class="form-floating">
|
||||||
|
<vc:truncate-center text="@Model.InvitationUrl" padding="15" elastic="true" classes="form-control-plaintext" id="InvitationUrl"/>
|
||||||
|
<label for="InvitationUrl">Invitation URL</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Name" class="form-label"></label>
|
<label asp-for="Name" class="form-label"></label>
|
||||||
<input asp-for="Name" class="form-control" />
|
<input asp-for="Name" class="form-control" />
|
||||||
|
|||||||
Reference in New Issue
Block a user