mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 21:18:13 -05:00
passwordless signin for billing portal
This commit is contained in:
parent
9ed1ae9567
commit
3d9d193bda
@ -5,8 +5,6 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<RootNamespace>Bit.Billing</RootNamespace>
|
||||
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -15,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -22,4 +21,9 @@
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\images\" />
|
||||
<Folder Include="wwwroot\scripts\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
15
src/Billing/Controllers/HomeController.cs
Normal file
15
src/Billing/Controllers/HomeController.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Billing.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
[Authorize]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
48
src/Billing/Controllers/LoginController.cs
Normal file
48
src/Billing/Controllers/LoginController.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Billing.Controllers
|
||||
{
|
||||
public class LoginController : Controller
|
||||
{
|
||||
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public LoginController(
|
||||
PasswordlessSignInManager<IdentityUser> signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(LoginModel model)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(model.Email);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Confirm(string email, string token)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(email, token, false);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
}
|
||||
}
|
11
src/Billing/Models/LoginModel.cs
Normal file
11
src/Billing/Models/LoginModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Billing.Models
|
||||
{
|
||||
public class LoginModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
@ -11,6 +11,10 @@ using Serilog.Events;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Billing.Utilities;
|
||||
using Bit.Core.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Bit.Billing
|
||||
{
|
||||
@ -42,7 +46,28 @@ namespace Bit.Billing
|
||||
services.AddScoped<CurrentContext>();
|
||||
|
||||
// Identity
|
||||
services.AddCustomIdentityServices(globalSettings);
|
||||
services.AddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
|
||||
services.AddIdentity<IdentityUser, Core.Models.Table.Role>()
|
||||
.AddUserStore<ReadOnlyIdentityUserStore>()
|
||||
.AddRoleStore<RoleStore>()
|
||||
.AddDefaultTokenProviders();
|
||||
services.TryAddScoped<PasswordlessSignInManager<IdentityUser>, PasswordlessSignInManager<IdentityUser>>();
|
||||
|
||||
services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromMinutes(15);
|
||||
});
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.LogoutPath = "/";
|
||||
options.AccessDeniedPath = "/login";
|
||||
options.Cookie.Name = "BitwardenBilling";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
|
||||
options.ReturnUrlParameter = "returnUrl";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
// Services
|
||||
services.AddBaseServices();
|
||||
@ -55,6 +80,7 @@ namespace Bit.Billing
|
||||
{
|
||||
config.Filters.Add(new ExceptionHandlerFilterAttribute());
|
||||
});
|
||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
@ -71,10 +97,9 @@ namespace Bit.Billing
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
// Default Middleware
|
||||
app.UseDefaultMiddleware(env);
|
||||
|
||||
app.UseMvc();
|
||||
app.UseAuthentication();
|
||||
app.UseStaticFiles();
|
||||
app.UseMvcWithDefaultRoute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
src/Billing/Views/Home/Index.cshtml
Normal file
6
src/Billing/Views/Home/Index.cshtml
Normal file
@ -0,0 +1,6 @@
|
||||
@{
|
||||
ViewData["Title"] = "Index";
|
||||
}
|
||||
|
||||
<h2>Index</h2>
|
||||
|
21
src/Billing/Views/Login/Index.cshtml
Normal file
21
src/Billing/Views/Login/Index.cshtml
Normal file
@ -0,0 +1,21 @@
|
||||
@{
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
|
||||
@model LoginModel
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-8 col-xs-offset-2">
|
||||
<h1 class="text-center">Login</h1>
|
||||
<form asp-action="" method="post" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="col-sm-3">Email Address: </label>
|
||||
<div class="col-sm-9">
|
||||
<input asp-for="Email" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<div asp-validation-summary="All"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
15
src/Billing/Views/Shared/Error.cshtml
Normal file
15
src/Billing/Views/Shared/Error.cshtml
Normal file
@ -0,0 +1,15 @@
|
||||
@model ErrorViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
|
||||
</p>
|
42
src/Billing/Views/Shared/_Layout.cshtml
Normal file
42
src/Billing/Views/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] | Bitwarden Billing Portal</title>
|
||||
|
||||
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="~/styles/site.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Bitwarden Billing</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li><a asp-area="" asp-controller="Login" asp-action="Index">Login</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container body-content">
|
||||
@RenderBody()
|
||||
<hr />
|
||||
<footer>
|
||||
<p>© 2018 - 8bit Solutions LLC</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"></script>
|
||||
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
3
src/Billing/Views/_ViewImports.cshtml
Normal file
3
src/Billing/Views/_ViewImports.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@using Bit.Billing
|
||||
@using Bit.Billing.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
3
src/Billing/Views/_ViewStart.cshtml
Normal file
3
src/Billing/Views/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
35
src/Billing/wwwroot/styles/site.css
Normal file
35
src/Billing/wwwroot/styles/site.css
Normal file
@ -0,0 +1,35 @@
|
||||
body {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Wrapping element */
|
||||
/* Set some basic padding to keep content from hitting the edges */
|
||||
.body-content {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/* Carousel */
|
||||
.carousel-caption p {
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Make .svg files in the carousel display properly in older browsers */
|
||||
.carousel-inner .item img[src$=".svg"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* QR code generator */
|
||||
#qrCode {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
/* Hide/rearrange for smaller screens */
|
||||
@media screen and (max-width: 767px) {
|
||||
/* Hide captions */
|
||||
.carousel-caption {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="2.0.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="1.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.AzureDocumentDB" Version="3.8.0" />
|
||||
|
90
src/Core/Identity/PasswordlessSignInManager.cs
Normal file
90
src/Core/Identity/PasswordlessSignInManager.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Core.Identity
|
||||
{
|
||||
public class PasswordlessSignInManager<TUser> : SignInManager<TUser> where TUser : class
|
||||
{
|
||||
public const string PasswordlessSignInPurpose = "PasswordlessSignIn";
|
||||
|
||||
public PasswordlessSignInManager(UserManager<TUser> userManager,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
IUserClaimsPrincipalFactory<TUser> claimsFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
ILogger<SignInManager<TUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes)
|
||||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(string email)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(email);
|
||||
if(user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
var token = await UserManager.GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
PasswordlessSignInPurpose);
|
||||
|
||||
// TODO: send email
|
||||
var encodedToken = WebUtility.UrlEncode(token);
|
||||
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(TUser user, string token, bool isPersistent)
|
||||
{
|
||||
if(user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var attempt = await CheckPasswordlessSignInAsync(user, token);
|
||||
return attempt.Succeeded ?
|
||||
await SignInOrTwoFactorAsync(user, isPersistent, bypassTwoFactor: true) : attempt;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(string email, string token, bool isPersistent)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(email);
|
||||
if(user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
return await PasswordlessSignInAsync(user, token, isPersistent);
|
||||
}
|
||||
|
||||
public virtual async Task<SignInResult> CheckPasswordlessSignInAsync(TUser user, string token)
|
||||
{
|
||||
if(user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var error = await PreSignInCheck(user);
|
||||
if(error != null)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
if(await UserManager.VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
PasswordlessSignInPurpose, token))
|
||||
{
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
Logger.LogWarning(2, "User {userId} failed to provide the correct token.",
|
||||
await UserManager.GetUserIdAsync(user));
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
144
src/Core/Identity/ReadOnlyIdentityUserStore.cs
Normal file
144
src/Core/Identity/ReadOnlyIdentityUserStore.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Identity
|
||||
{
|
||||
public class ReadOnlyIdentityUserStore :
|
||||
IUserStore<IdentityUser>,
|
||||
IUserEmailStore<IdentityUser>,
|
||||
IUserSecurityStampStore<IdentityUser>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ReadOnlyIdentityUserStore(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public Task<IdentityResult> CreateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> DeleteAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(normalizedEmail);
|
||||
return user?.ToIdentityUser();
|
||||
}
|
||||
|
||||
public async Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if(!Guid.TryParse(userId, out var userIdGuid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userIdGuid);
|
||||
return user?.ToIdentityUser();
|
||||
}
|
||||
|
||||
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return await FindByEmailAsync(normalizedUserName, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> GetEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<bool> GetEmailConfirmedAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.EmailConfirmed);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetUserIdAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Id);
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task SetEmailAsync(IdentityUser user, string email,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.NormalizedEmail = normalizedEmail;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.NormalizedUserName = normalizedName;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(IdentityUser user, string userName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.SecurityStamp);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Models.Table
|
||||
{
|
||||
@ -163,5 +164,20 @@ namespace Bit.Core.Models.Table
|
||||
|
||||
return paymentService;
|
||||
}
|
||||
|
||||
public IdentityUser ToIdentityUser()
|
||||
{
|
||||
return new IdentityUser
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Email = Email,
|
||||
NormalizedEmail = Email,
|
||||
EmailConfirmed = EmailVerified,
|
||||
UserName = Email,
|
||||
NormalizedUserName = Email,
|
||||
TwoFactorEnabled = TwoFactorIsEnabled(),
|
||||
SecurityStamp = SecurityStamp
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user