mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Can ask user to confirm email
This commit is contained in:
@@ -28,6 +28,7 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
StoreRepository storeRepository;
|
StoreRepository storeRepository;
|
||||||
RoleManager<IdentityRole> _RoleManager;
|
RoleManager<IdentityRole> _RoleManager;
|
||||||
|
SettingsRepository _SettingsRepository;
|
||||||
|
|
||||||
public AccountController(
|
public AccountController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
@@ -35,6 +36,7 @@ namespace BTCPayServer.Controllers
|
|||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
SignInManager<ApplicationUser> signInManager,
|
SignInManager<ApplicationUser> signInManager,
|
||||||
IEmailSender emailSender,
|
IEmailSender emailSender,
|
||||||
|
SettingsRepository settingsRepository,
|
||||||
ILogger<AccountController> logger)
|
ILogger<AccountController> logger)
|
||||||
{
|
{
|
||||||
this.storeRepository = storeRepository;
|
this.storeRepository = storeRepository;
|
||||||
@@ -43,11 +45,14 @@ namespace BTCPayServer.Controllers
|
|||||||
_emailSender = emailSender;
|
_emailSender = emailSender;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_RoleManager = roleManager;
|
_RoleManager = roleManager;
|
||||||
|
_SettingsRepository = settingsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TempData]
|
[TempData]
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
@@ -66,21 +71,36 @@ namespace BTCPayServer.Controllers
|
|||||||
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
||||||
{
|
{
|
||||||
ViewData["ReturnUrl"] = returnUrl;
|
ViewData["ReturnUrl"] = returnUrl;
|
||||||
if (ModelState.IsValid)
|
if(ModelState.IsValid)
|
||||||
{
|
{
|
||||||
|
// Require the user to have a confirmed email before they can log on.
|
||||||
|
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||||
|
if(user != null)
|
||||||
|
{
|
||||||
|
if(user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty,
|
||||||
|
"You must have a confirmed email to log in.");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
// This doesn't count login failures towards account lockout
|
// This doesn't count login failures towards account lockout
|
||||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
|
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User logged in.");
|
_logger.LogInformation("User logged in.");
|
||||||
return RedirectToLocal(returnUrl);
|
return RedirectToLocal(returnUrl);
|
||||||
}
|
}
|
||||||
if (result.RequiresTwoFactor)
|
if(result.RequiresTwoFactor)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
|
return RedirectToAction(nameof(LoginWith2fa), new
|
||||||
|
{
|
||||||
|
returnUrl,
|
||||||
|
model.RememberMe
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (result.IsLockedOut)
|
if(result.IsLockedOut)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User account locked out.");
|
_logger.LogWarning("User account locked out.");
|
||||||
return RedirectToAction(nameof(Lockout));
|
return RedirectToAction(nameof(Lockout));
|
||||||
@@ -103,7 +123,7 @@ namespace BTCPayServer.Controllers
|
|||||||
// Ensure the user has gone through the username & password screen first
|
// Ensure the user has gone through the username & password screen first
|
||||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||||
|
|
||||||
if (user == null)
|
if(user == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||||
}
|
}
|
||||||
@@ -119,13 +139,13 @@ namespace BTCPayServer.Controllers
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
|
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if(!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||||
if (user == null)
|
if(user == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
@@ -134,12 +154,12 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||||
|
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
||||||
return RedirectToLocal(returnUrl);
|
return RedirectToLocal(returnUrl);
|
||||||
}
|
}
|
||||||
else if (result.IsLockedOut)
|
else if(result.IsLockedOut)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||||
return RedirectToAction(nameof(Lockout));
|
return RedirectToAction(nameof(Lockout));
|
||||||
@@ -158,7 +178,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
// Ensure the user has gone through the username & password screen first
|
// Ensure the user has gone through the username & password screen first
|
||||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||||
if (user == null)
|
if(user == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||||
}
|
}
|
||||||
@@ -173,13 +193,13 @@ namespace BTCPayServer.Controllers
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
|
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if(!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||||
if (user == null)
|
if(user == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||||
}
|
}
|
||||||
@@ -188,12 +208,12 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||||
|
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
||||||
return RedirectToLocal(returnUrl);
|
return RedirectToLocal(returnUrl);
|
||||||
}
|
}
|
||||||
if (result.IsLockedOut)
|
if(result.IsLockedOut)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||||
return RedirectToAction(nameof(Lockout));
|
return RedirectToAction(nameof(Lockout));
|
||||||
@@ -227,11 +247,12 @@ namespace BTCPayServer.Controllers
|
|||||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
|
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
|
||||||
{
|
{
|
||||||
ViewData["ReturnUrl"] = returnUrl;
|
ViewData["ReturnUrl"] = returnUrl;
|
||||||
if (ModelState.IsValid)
|
if(ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||||
|
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
|
||||||
var result = await _userManager.CreateAsync(user, model.Password);
|
var result = await _userManager.CreateAsync(user, model.Password);
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User created a new account with password.");
|
_logger.LogInformation("User created a new account with password.");
|
||||||
|
|
||||||
@@ -247,10 +268,18 @@ namespace BTCPayServer.Controllers
|
|||||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||||
RegisteredUserId = user.Id;
|
RegisteredUserId = user.Id;
|
||||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||||
|
if(!policies.RequiresConfirmedEmail)
|
||||||
|
{
|
||||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||||
_logger.LogInformation("User created a new account with password.");
|
|
||||||
return RedirectToLocal(returnUrl);
|
return RedirectToLocal(returnUrl);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["StatusMessage"] = "Account created, please confirm your email";
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
_logger.LogInformation("User created a new account with password.");
|
||||||
|
}
|
||||||
AddErrors(result);
|
AddErrors(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +309,10 @@ namespace BTCPayServer.Controllers
|
|||||||
public IActionResult ExternalLogin(string provider, string returnUrl = null)
|
public IActionResult ExternalLogin(string provider, string returnUrl = null)
|
||||||
{
|
{
|
||||||
// Request a redirect to the external login provider.
|
// Request a redirect to the external login provider.
|
||||||
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
|
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new
|
||||||
|
{
|
||||||
|
returnUrl
|
||||||
|
});
|
||||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||||
return Challenge(properties, provider);
|
return Challenge(properties, provider);
|
||||||
}
|
}
|
||||||
@@ -289,25 +321,25 @@ namespace BTCPayServer.Controllers
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
|
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
|
||||||
{
|
{
|
||||||
if (remoteError != null)
|
if(remoteError != null)
|
||||||
{
|
{
|
||||||
ErrorMessage = $"Error from external provider: {remoteError}";
|
ErrorMessage = $"Error from external provider: {remoteError}";
|
||||||
return RedirectToAction(nameof(Login));
|
return RedirectToAction(nameof(Login));
|
||||||
}
|
}
|
||||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||||
if (info == null)
|
if(info == null)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(Login));
|
return RedirectToAction(nameof(Login));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign in the user with this external login provider if the user already has a login.
|
// Sign in the user with this external login provider if the user already has a login.
|
||||||
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
|
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
|
||||||
return RedirectToLocal(returnUrl);
|
return RedirectToLocal(returnUrl);
|
||||||
}
|
}
|
||||||
if (result.IsLockedOut)
|
if(result.IsLockedOut)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(Lockout));
|
return RedirectToAction(nameof(Lockout));
|
||||||
}
|
}
|
||||||
@@ -326,20 +358,20 @@ namespace BTCPayServer.Controllers
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
|
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
|
||||||
{
|
{
|
||||||
if (ModelState.IsValid)
|
if(ModelState.IsValid)
|
||||||
{
|
{
|
||||||
// Get the information about the user from the external login provider
|
// Get the information about the user from the external login provider
|
||||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||||
if (info == null)
|
if(info == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException("Error loading external login information during confirmation.");
|
throw new ApplicationException("Error loading external login information during confirmation.");
|
||||||
}
|
}
|
||||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||||
var result = await _userManager.CreateAsync(user);
|
var result = await _userManager.CreateAsync(user);
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
result = await _userManager.AddLoginAsync(user, info);
|
result = await _userManager.AddLoginAsync(user, info);
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||||
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
|
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
|
||||||
@@ -357,12 +389,12 @@ namespace BTCPayServer.Controllers
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> ConfirmEmail(string userId, string code)
|
public async Task<IActionResult> ConfirmEmail(string userId, string code)
|
||||||
{
|
{
|
||||||
if (userId == null || code == null)
|
if(userId == null || code == null)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||||
}
|
}
|
||||||
var user = await _userManager.FindByIdAsync(userId);
|
var user = await _userManager.FindByIdAsync(userId);
|
||||||
if (user == null)
|
if(user == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
||||||
}
|
}
|
||||||
@@ -382,10 +414,10 @@ namespace BTCPayServer.Controllers
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||||
{
|
{
|
||||||
if (ModelState.IsValid)
|
if(ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||||
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
if(user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||||
{
|
{
|
||||||
// Don't reveal that the user does not exist or is not confirmed
|
// Don't reveal that the user does not exist or is not confirmed
|
||||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||||
@@ -415,7 +447,7 @@ namespace BTCPayServer.Controllers
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public IActionResult ResetPassword(string code = null)
|
public IActionResult ResetPassword(string code = null)
|
||||||
{
|
{
|
||||||
if (code == null)
|
if(code == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException("A code must be supplied for password reset.");
|
throw new ApplicationException("A code must be supplied for password reset.");
|
||||||
}
|
}
|
||||||
@@ -428,18 +460,18 @@ namespace BTCPayServer.Controllers
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if(!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||||
if (user == null)
|
if(user == null)
|
||||||
{
|
{
|
||||||
// Don't reveal that the user does not exist
|
// Don't reveal that the user does not exist
|
||||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||||
}
|
}
|
||||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||||
if (result.Succeeded)
|
if(result.Succeeded)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||||
}
|
}
|
||||||
@@ -465,7 +497,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
private void AddErrors(IdentityResult result)
|
private void AddErrors(IdentityResult result)
|
||||||
{
|
{
|
||||||
foreach (var error in result.Errors)
|
foreach(var error in result.Errors)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
ModelState.AddModelError(string.Empty, error.Description);
|
||||||
}
|
}
|
||||||
@@ -473,7 +505,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
private IActionResult RedirectToLocal(string returnUrl)
|
private IActionResult RedirectToLocal(string returnUrl)
|
||||||
{
|
{
|
||||||
if (Url.IsLocalUrl(returnUrl))
|
if(Url.IsLocalUrl(returnUrl))
|
||||||
{
|
{
|
||||||
return Redirect(returnUrl);
|
return Redirect(returnUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ using BTCPayServer.Services.Stores;
|
|||||||
using BTCPayServer.Servcices.Invoices;
|
using BTCPayServer.Servcices.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using BTCPayServer.Validations;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
@@ -72,16 +73,6 @@ namespace BTCPayServer.Controllers
|
|||||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Regex _Email;
|
|
||||||
bool IsEmail(string str)
|
|
||||||
{
|
|
||||||
if(String.IsNullOrWhiteSpace(str))
|
|
||||||
return false;
|
|
||||||
if(_Email == null)
|
|
||||||
_Email = new Regex("^((([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+(\\.([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(\\\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.)+(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled, TimeSpan.FromSeconds(2.0));
|
|
||||||
return _Email.IsMatch(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
||||||
{
|
{
|
||||||
var derivationStrategy = store.DerivationStrategy;
|
var derivationStrategy = store.DerivationStrategy;
|
||||||
@@ -99,7 +90,7 @@ namespace BTCPayServer.Controllers
|
|||||||
entity.FullNotifications = invoice.FullNotifications;
|
entity.FullNotifications = invoice.FullNotifications;
|
||||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||||
entity.RefundMail = IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||||
entity.Status = "new";
|
entity.Status = "new";
|
||||||
|
|||||||
@@ -1,23 +1,31 @@
|
|||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.ServerViewModels;
|
using BTCPayServer.Models.ServerViewModels;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Mails;
|
||||||
|
using BTCPayServer.Validations;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Mail;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Authorize(Roles=Roles.ServerAdmin)]
|
[Authorize(Roles = Roles.ServerAdmin)]
|
||||||
public class ServerController : Controller
|
public class ServerController : Controller
|
||||||
{
|
{
|
||||||
private UserManager<ApplicationUser> _UserManager;
|
private UserManager<ApplicationUser> _UserManager;
|
||||||
|
SettingsRepository _SettingsRepository;
|
||||||
|
|
||||||
public ServerController(UserManager<ApplicationUser> userManager)
|
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
|
||||||
{
|
{
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
|
_SettingsRepository = settingsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("server/users")]
|
[Route("server/users")]
|
||||||
@@ -32,5 +40,58 @@ namespace BTCPayServer.Controllers
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
return View(users);
|
return View(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("server/emails")]
|
||||||
|
public async Task<IActionResult> Emails()
|
||||||
|
{
|
||||||
|
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||||
|
return View(new EmailsViewModel() { Settings = data });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/policies")]
|
||||||
|
public async Task<IActionResult> Policies()
|
||||||
|
{
|
||||||
|
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||||
|
return View(data);
|
||||||
|
}
|
||||||
|
[Route("server/policies")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||||
|
{
|
||||||
|
await _SettingsRepository.UpdateSetting(settings);
|
||||||
|
TempData["StatusMessage"] = "Policies upadated successfully";
|
||||||
|
return View(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/emails")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||||
|
{
|
||||||
|
if(command == "Test")
|
||||||
|
{
|
||||||
|
if(!ModelState.IsValid)
|
||||||
|
return View(model);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = model.Settings.CreateSmtpClient();
|
||||||
|
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||||
|
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
model.StatusMessage = "Error: " + ex.Message;
|
||||||
|
}
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModelState.Remove(nameof(model.TestEmail));
|
||||||
|
if(!ModelState.IsValid)
|
||||||
|
return View(model);
|
||||||
|
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||||
|
model.StatusMessage = "Email settings saved";
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ namespace BTCPayServer.Data
|
|||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DbSet<SettingData> Settings
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
var options = optionsBuilder.Options.FindExtension<SqliteOptionsExtension>();
|
var options = optionsBuilder.Options.FindExtension<SqliteOptionsExtension>();
|
||||||
|
|||||||
20
BTCPayServer/Data/SettingData.cs
Normal file
20
BTCPayServer/Data/SettingData.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data
|
||||||
|
{
|
||||||
|
public class SettingData
|
||||||
|
{
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ namespace BTCPayServer.Hosting
|
|||||||
var path = Path.Combine(provider.GetRequiredService<BTCPayServerOptions>().DataDir, "sqllite.db");
|
var path = Path.Combine(provider.GetRequiredService<BTCPayServerOptions>().DataDir, "sqllite.db");
|
||||||
o.UseSqlite("Data Source=" + path);
|
o.UseSqlite("Data Source=" + path);
|
||||||
});
|
});
|
||||||
|
services.TryAddSingleton<SettingsRepository>();
|
||||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||||
services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>();
|
services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>();
|
||||||
|
|||||||
369
BTCPayServer/Migrations/20170926073744_Settings.Designer.cs
generated
Normal file
369
BTCPayServer/Migrations/20170926073744_Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Servcices.Invoices;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20170926073744_Settings")]
|
||||||
|
partial class Settings
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerEmail");
|
||||||
|
|
||||||
|
b.Property<string>("ExceptionStatus");
|
||||||
|
|
||||||
|
b.Property<string>("ItemCode");
|
||||||
|
|
||||||
|
b.Property<string>("OrderId");
|
||||||
|
|
||||||
|
b.Property<string>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("Invoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("Payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("RefundAddresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("DerivationStrategy");
|
||||||
|
|
||||||
|
b.Property<int>("SpeedPolicy");
|
||||||
|
|
||||||
|
b.Property<byte[]>("StoreCertificate");
|
||||||
|
|
||||||
|
b.Property<string>("StoreName");
|
||||||
|
|
||||||
|
b.Property<string>("StoreWebsite");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Stores");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ApplicationUserId");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.Property<string>("Role");
|
||||||
|
|
||||||
|
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("UserStore");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StoreDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("Payments")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("RefundAddresses")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("ApplicationUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
BTCPayServer/Migrations/20170926073744_Settings.cs
Normal file
30
BTCPayServer/Migrations/20170926073744_Settings.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
public partial class Settings : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Settings", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
371
BTCPayServer/Migrations/20170926084408_RequiresEmailConfirmation.Designer.cs
generated
Normal file
371
BTCPayServer/Migrations/20170926084408_RequiresEmailConfirmation.Designer.cs
generated
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Servcices.Invoices;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20170926084408_RequiresEmailConfirmation")]
|
||||||
|
partial class RequiresEmailConfirmation
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerEmail");
|
||||||
|
|
||||||
|
b.Property<string>("ExceptionStatus");
|
||||||
|
|
||||||
|
b.Property<string>("ItemCode");
|
||||||
|
|
||||||
|
b.Property<string>("OrderId");
|
||||||
|
|
||||||
|
b.Property<string>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("Invoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("Payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("RefundAddresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("DerivationStrategy");
|
||||||
|
|
||||||
|
b.Property<int>("SpeedPolicy");
|
||||||
|
|
||||||
|
b.Property<byte[]>("StoreCertificate");
|
||||||
|
|
||||||
|
b.Property<string>("StoreName");
|
||||||
|
|
||||||
|
b.Property<string>("StoreWebsite");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Stores");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ApplicationUserId");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.Property<string>("Role");
|
||||||
|
|
||||||
|
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("UserStore");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("RequiresEmailConfirmation");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StoreDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("Payments")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("RefundAddresses")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("ApplicationUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
public partial class RequiresEmailConfirmation : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "RequiresEmailConfirmation",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RequiresEmailConfirmation",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Servcices.Invoices;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
@@ -79,6 +80,18 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("RefundAddresses");
|
b.ToTable("RefundAddresses");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -145,6 +158,8 @@ namespace BTCPayServer.Migrations
|
|||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed");
|
b.Property<bool>("PhoneNumberConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("RequiresEmailConfirmation");
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp");
|
b.Property<string>("SecurityStamp");
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled");
|
b.Property<bool>("TwoFactorEnabled");
|
||||||
|
|||||||
@@ -15,5 +15,10 @@ namespace BTCPayServer.Models
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool RequiresEmailConfirmation
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs
Normal file
29
BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using BTCPayServer.Services.Mails;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
|
{
|
||||||
|
public class EmailsViewModel
|
||||||
|
{
|
||||||
|
public string StatusMessage
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
public EmailSettings Settings
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string TestEmail
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using Hangfire;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mail;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Mails
|
namespace BTCPayServer.Services.Mails
|
||||||
@@ -9,9 +11,30 @@ namespace BTCPayServer.Services.Mails
|
|||||||
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
|
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
|
||||||
public class EmailSender : IEmailSender
|
public class EmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
|
IBackgroundJobClient _JobClient;
|
||||||
|
SettingsRepository _Repository;
|
||||||
|
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
|
||||||
|
{
|
||||||
|
if(jobClient == null)
|
||||||
|
throw new ArgumentNullException(nameof(jobClient));
|
||||||
|
_JobClient = jobClient;
|
||||||
|
_Repository = repository;
|
||||||
|
}
|
||||||
public Task SendEmailAsync(string email, string subject, string message)
|
public Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
|
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendMailCore(string email, string subject, string message)
|
||||||
|
{
|
||||||
|
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
||||||
|
if(settings == null)
|
||||||
|
throw new InvalidOperationException("Email settings not configured");
|
||||||
|
var smtp = settings.CreateSmtpClient();
|
||||||
|
MailMessage mail = new MailMessage(settings.From, email, subject, message);
|
||||||
|
mail.IsBodyHtml = true;
|
||||||
|
await smtp.SendMailAsync(mail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
BTCPayServer/Services/Mails/EmailSettings.cs
Normal file
59
BTCPayServer/Services/Mails/EmailSettings.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Mail;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services.Mails
|
||||||
|
{
|
||||||
|
public class EmailSettings
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Server
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int? Port
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public String Login
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public String Password
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[EmailAddress]
|
||||||
|
public string From
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EnableSSL
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmtpClient CreateSmtpClient()
|
||||||
|
{
|
||||||
|
SmtpClient client = new SmtpClient(Server, Port.Value);
|
||||||
|
client.EnableSsl = true;
|
||||||
|
client.UseDefaultCredentials = false;
|
||||||
|
client.Credentials = new NetworkCredential(Login, Password);
|
||||||
|
client.DeliveryMethod = SmtpDeliveryMethod.Network;
|
||||||
|
client.Timeout = 10000;
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
BTCPayServer/Services/PoliciesSettings.cs
Normal file
15
BTCPayServer/Services/PoliciesSettings.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public class PoliciesSettings
|
||||||
|
{
|
||||||
|
public bool RequiresConfirmedEmail
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
BTCPayServer/Services/SettingsRepository.cs
Normal file
66
BTCPayServer/Services/SettingsRepository.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using BTCPayServer.Data;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public class SettingsRepository
|
||||||
|
{
|
||||||
|
private ApplicationDbContextFactory _ContextFactory;
|
||||||
|
public SettingsRepository(ApplicationDbContextFactory contextFactory)
|
||||||
|
{
|
||||||
|
_ContextFactory = contextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> GetSettingAsync<T>()
|
||||||
|
{
|
||||||
|
var name = typeof(T).FullName;
|
||||||
|
using(var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync();
|
||||||
|
if(data == null)
|
||||||
|
return default(T);
|
||||||
|
return Deserialize<T>(data.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSetting<T>(T obj)
|
||||||
|
{
|
||||||
|
var name = obj.GetType().FullName;
|
||||||
|
using(var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var settings = new SettingData();
|
||||||
|
settings.Id = name;
|
||||||
|
settings.Value = Serialize(obj);
|
||||||
|
ctx.Attach(settings);
|
||||||
|
ctx.Entry(settings).State = EntityState.Modified;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch(DbUpdateException)
|
||||||
|
{
|
||||||
|
ctx.Entry(settings).State = EntityState.Added;
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private T Deserialize<T>(string value)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<T>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Serialize<T>(T obj)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
BTCPayServer/Validations/EmailValidator.cs
Normal file
21
BTCPayServer/Validations/EmailValidator.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Validations
|
||||||
|
{
|
||||||
|
public class EmailValidator
|
||||||
|
{
|
||||||
|
static Regex _Email;
|
||||||
|
public static bool IsEmail(string str)
|
||||||
|
{
|
||||||
|
if(String.IsNullOrWhiteSpace(str))
|
||||||
|
return false;
|
||||||
|
if(_Email == null)
|
||||||
|
_Email = new Regex("^((([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+(\\.([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(\\\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.)+(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled, TimeSpan.FromSeconds(2.0));
|
||||||
|
return _Email.IsMatch(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,13 @@
|
|||||||
ViewData["Title"] = "Confirm email";
|
ViewData["Title"] = "Confirm email";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>@ViewData["Title"]</h2>
|
<section>
|
||||||
<div>
|
<div class="container">
|
||||||
<p>
|
|
||||||
Thank you for confirming your email.
|
<div class="row">
|
||||||
</p>
|
<div class="col-lg-12 text-center">
|
||||||
</div>
|
@Html.Partial("_StatusMessage", "Thank you for confirming your email.")
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -5,6 +5,11 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 text-center">
|
<div class="col-lg-12 text-center">
|
||||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||||
|
|||||||
67
BTCPayServer/Views/Server/Emails.cshtml
Normal file
67
BTCPayServer/Views/Server/Emails.cshtml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@model EmailsViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = ServerNavPages.Emails;
|
||||||
|
ViewData.AddActivePage(ServerNavPages.Emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Settings.Server"></label>
|
||||||
|
<input asp-for="Settings.Server" class="form-control" />
|
||||||
|
<span asp-validation-for="Settings.Server" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Settings.Port"></label>
|
||||||
|
<input asp-for="Settings.Port" class="form-control" />
|
||||||
|
<span asp-validation-for="Settings.Port" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Settings.From"></label>
|
||||||
|
<input asp-for="Settings.From" class="form-control" />
|
||||||
|
<span asp-validation-for="Settings.From" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Settings.Login"></label>
|
||||||
|
<input asp-for="Settings.Login" class="form-control" />
|
||||||
|
<span asp-validation-for="Settings.Login" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Settings.Password"></label>
|
||||||
|
<input asp-for="Settings.Password" type="password" class="form-control" />
|
||||||
|
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Settings.EnableSSL"></label>
|
||||||
|
<input asp-for="Settings.EnableSSL" type="checkbox" class="form-check-inline" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TestEmail"></label>
|
||||||
|
<input asp-for="TestEmail" class="form-control" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-success" name="command" value="Save">Save</button>
|
||||||
|
<button type="submit" class="btn btn-default" name="command" value="Test">Test</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
||||||
29
BTCPayServer/Views/Server/Policies.cshtml
Normal file
29
BTCPayServer/Views/Server/Policies.cshtml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@model BTCPayServer.Services.PoliciesSettings
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = ServerNavPages.Emails;
|
||||||
|
ViewData.AddActivePage(ServerNavPages.Emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="RequiresConfirmedEmail"></label>
|
||||||
|
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success" name="command" value="Save">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
||||||
@@ -14,9 +14,13 @@ namespace BTCPayServer.Views.Server
|
|||||||
|
|
||||||
|
|
||||||
public static string Users => "Users";
|
public static string Users => "Users";
|
||||||
|
public static string Emails => "Email server";
|
||||||
|
public static string Policies => "Policies";
|
||||||
public static string Hangfire => "Hangfire";
|
public static string Hangfire => "Hangfire";
|
||||||
|
|
||||||
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
|
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
|
||||||
|
public static string EmailsNavClass(ViewContext viewContext) => PageNavClass(viewContext, Emails);
|
||||||
|
public static string PoliciesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Policies);
|
||||||
public static string HangfireNavClass(ViewContext viewContext) => PageNavClass(viewContext, Hangfire);
|
public static string HangfireNavClass(ViewContext viewContext) => PageNavClass(viewContext, Hangfire);
|
||||||
|
|
||||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="@ServerNavPages.UsersNavClass(ViewContext)"><a asp-action="Users">Users</a></li>
|
<li class="@ServerNavPages.UsersNavClass(ViewContext)"><a asp-action="Users">Users</a></li>
|
||||||
|
<li class="@ServerNavPages.EmailsNavClass(ViewContext)"><a asp-action="Emails">Email server</a></li>
|
||||||
|
<li class="@ServerNavPages.PoliciesNavClass(ViewContext)"><a asp-action="Policies">Policies</a></li>
|
||||||
<li class="@ServerNavPages.HangfireNavClass(ViewContext)"><a href="~/hangfire" target="_blank">Hangfire</a></li>
|
<li class="@ServerNavPages.HangfireNavClass(ViewContext)"><a href="~/hangfire" target="_blank">Hangfire</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user