diff --git a/.template.config/template.json b/.template.config/template.json new file mode 100644 index 0000000..5098c64 --- /dev/null +++ b/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/template", + "identity": "RazorStripe", + "author": "Dragon Mastery LLC", + "classifications": [ "Web/MVC/Razor Pages" ], + "name": "ASP.NET Core 2.2 Web App Razor Stripe", + "shortName": "razorstripe", + "tags": { + "language": "C#" + }, + "sourceName": "RazorStripe", + "symbols":{ + "copyrightName": { + "type": "parameter", + "defaultValue": "Company", + "replaces":"Dragon Mastery LLC" + } + }, + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/Areas/Identity/IdentityHostingStartup.cs b/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 0000000..de6facb --- /dev/null +++ b/Areas/Identity/IdentityHostingStartup.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using RazorStripe.Data; + +[assembly: HostingStartup(typeof(RazorStripe.Areas.Identity.IdentityHostingStartup))] +namespace RazorStripe.Areas.Identity +{ + public class IdentityHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => { + }); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/AccessDenied.cshtml b/Areas/Identity/Pages/Account/AccessDenied.cshtml new file mode 100644 index 0000000..017f6ff --- /dev/null +++ b/Areas/Identity/Pages/Account/AccessDenied.cshtml @@ -0,0 +1,10 @@ +@page +@model AccessDeniedModel +@{ + ViewData["Title"] = "Access denied"; +} + +
+

@ViewData["Title"]

+

You do not have access to this resource.

+
diff --git a/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs b/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs new file mode 100644 index 0000000..96e342a --- /dev/null +++ b/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + public class AccessDeniedModel : PageModel + { + public void OnGet() + { + + } + } +} + diff --git a/Areas/Identity/Pages/Account/ConfirmEmail.cshtml b/Areas/Identity/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..401bf32 --- /dev/null +++ b/Areas/Identity/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,12 @@ +@page +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

+
+

+ Thank you for confirming your email. +

+
diff --git a/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 0000000..274af13 --- /dev/null +++ b/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ConfirmEmailModel : PageModel + { + private readonly UserManager _userManager; + + public ConfirmEmailModel(UserManager userManager) + { + _userManager = userManager; + } + + public async Task OnGetAsync(string userId, string code) + { + if (userId == null || code == null) + { + return RedirectToPage("/Index"); + } + + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return NotFound($"Unable to load user with ID '{userId}'."); + } + + var result = await _userManager.ConfirmEmailAsync(user, code); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':"); + } + + return Page(); + } + } +} diff --git a/Areas/Identity/Pages/Account/ExternalLogin.cshtml b/Areas/Identity/Pages/Account/ExternalLogin.cshtml new file mode 100644 index 0000000..54273e2 --- /dev/null +++ b/Areas/Identity/Pages/Account/ExternalLogin.cshtml @@ -0,0 +1,34 @@ +@page +@model ExternalLoginModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

Associate your @Model.LoginProvider account.

+
+ +

+ You've successfully authenticated with @Model.LoginProvider. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} + diff --git a/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs new file mode 100644 index 0000000..9ce9d95 --- /dev/null +++ b/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ExternalLoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public ExternalLoginModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string LoginProvider { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public IActionResult OnGetAsync() + { + return RedirectToPage("./Login"); + } + + public IActionResult OnPost(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + if (remoteError != null) + { + ErrorMessage = $"Error from external provider: {remoteError}"; + return RedirectToPage("./Login", new {ReturnUrl = returnUrl }); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + ErrorMessage = "Error loading external login information."; + return RedirectToPage("./Login", new { ReturnUrl = returnUrl }); + } + + // 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); + if (result.Succeeded) + { + _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); + return LocalRedirect(returnUrl); + } + if (result.IsLockedOut) + { + return RedirectToPage("./Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ReturnUrl = returnUrl; + LoginProvider = info.LoginProvider; + if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input = new InputModel + { + Email = info.Principal.FindFirstValue(ClaimTypes.Email) + }; + } + return Page(); + } + } + + public async Task OnPostConfirmationAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + ErrorMessage = "Error loading external login information during confirmation."; + return RedirectToPage("./Login", new { ReturnUrl = returnUrl }); + } + + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); + return LocalRedirect(returnUrl); + } + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + LoginProvider = info.LoginProvider; + ReturnUrl = returnUrl; + return Page(); + } + } +} diff --git a/Areas/Identity/Pages/Account/ForgotPassword.cshtml b/Areas/Identity/Pages/Account/ForgotPassword.cshtml new file mode 100644 index 0000000..1342aa7 --- /dev/null +++ b/Areas/Identity/Pages/Account/ForgotPassword.cshtml @@ -0,0 +1,26 @@ +@page +@model ForgotPasswordModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs new file mode 100644 index 0000000..72b20bd --- /dev/null +++ b/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ForgotPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + + public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) + { + _userManager = userManager; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await _userManager.GeneratePasswordResetTokenAsync(user); + var callbackUrl = Url.Page( + "/Account/ResetPassword", + pageHandler: null, + values: new { code }, + protocol: Request.Scheme); + + await _emailSender.SendEmailAsync( + Input.Email, + "Reset Password", + $"Please reset your password by clicking here."); + + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + return Page(); + } + } +} diff --git a/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml b/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000..9468da0 --- /dev/null +++ b/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,11 @@ +@page +@model ForgotPasswordConfirmation +@{ + ViewData["Title"] = "Forgot password confirmation"; +} + +

@ViewData["Title"]

+

+ Please check your email to reset your password. +

+ diff --git a/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000..d30ea66 --- /dev/null +++ b/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ForgotPasswordConfirmation : PageModel + { + public void OnGet() + { + } + } +} diff --git a/Areas/Identity/Pages/Account/Lockout.cshtml b/Areas/Identity/Pages/Account/Lockout.cshtml new file mode 100644 index 0000000..4eded88 --- /dev/null +++ b/Areas/Identity/Pages/Account/Lockout.cshtml @@ -0,0 +1,10 @@ +@page +@model LockoutModel +@{ + ViewData["Title"] = "Locked out"; +} + +
+

@ViewData["Title"]

+

This account has been locked out, please try again later.

+
diff --git a/Areas/Identity/Pages/Account/Lockout.cshtml.cs b/Areas/Identity/Pages/Account/Lockout.cshtml.cs new file mode 100644 index 0000000..05d44d4 --- /dev/null +++ b/Areas/Identity/Pages/Account/Lockout.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LockoutModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/Areas/Identity/Pages/Account/Login.cshtml b/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 0000000..917d3a2 --- /dev/null +++ b/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,82 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"]

+
+
+
+
+

Use a local account to log in.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Login.cshtml.cs b/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..bfee255 --- /dev/null +++ b/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, UserManager userManager, + ILogger logger) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [Display(Name = "Username/Email")] + public string Username_Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + if (!string.IsNullOrEmpty(ErrorMessage)) + { + ModelState.AddModelError(string.Empty, ErrorMessage); + } + + returnUrl = returnUrl ?? Url.Content("~/"); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var user = await _userManager.FindByNameAsync(Input.Username_Email) ?? await _userManager.FindByEmailAsync(Input.Username_Email); + if (user == null) + { + ModelState.AddModelError(string.Empty, "User not found."); + return Page(); + } + var result = await _signInManager.PasswordSignInAsync(user, Input.Password, Input.RememberMe, lockoutOnFailure: true); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return LocalRedirect(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Password is incorrect."); + return Page(); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/LoginWith2fa.cshtml b/Areas/Identity/Pages/Account/LoginWith2fa.cshtml new file mode 100644 index 0000000..a9d25fd --- /dev/null +++ b/Areas/Identity/Pages/Account/LoginWith2fa.cshtml @@ -0,0 +1,41 @@ +@page +@model LoginWith2faModel +@{ + ViewData["Title"] = "Two-factor authentication"; +} + +

@ViewData["Title"]

+
+

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs b/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs new file mode 100644 index 0000000..0be4d06 --- /dev/null +++ b/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginWith2faModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWith2faModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public bool RememberMe { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } + + public async Task OnGetAsync(bool rememberMe, string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + RememberMe = rememberMe; + + return Page(); + } + + public async Task OnPostAsync(bool rememberMe, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + returnUrl = returnUrl ?? Url.Content("~/"); + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); + + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); + return LocalRedirect(returnUrl); + } + else if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); + ModelState.AddModelError(string.Empty, "Invalid authenticator code."); + return Page(); + } + } + } +} diff --git a/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml b/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 0000000..abd45aa --- /dev/null +++ b/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,29 @@ +@page +@model LoginWithRecoveryCodeModel +@{ + ViewData["Title"] = "Recovery code verification"; +} + +

@ViewData["Title"]

+
+

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+
+
+
+ + + +
+ +
+
+
+ + @section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs new file mode 100644 index 0000000..ccb2d86 --- /dev/null +++ b/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginWithRecoveryCodeModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [BindProperty] + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + + return Page(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id); + return LocalRedirect(returnUrl ?? Url.Content("~/")); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); + ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); + return Page(); + } + } + } +} diff --git a/Areas/Identity/Pages/Account/Logout.cshtml b/Areas/Identity/Pages/Account/Logout.cshtml new file mode 100644 index 0000000..cb864ef --- /dev/null +++ b/Areas/Identity/Pages/Account/Logout.cshtml @@ -0,0 +1,10 @@ +@page +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} + +
+

@ViewData["Title"]

+

You have successfully logged out of the application.

+
\ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Logout.cshtml.cs b/Areas/Identity/Pages/Account/Logout.cshtml.cs new file mode 100644 index 0000000..d5edd7c --- /dev/null +++ b/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LogoutModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LogoutModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + public void OnGet() + { + } + + public async Task OnPost(string returnUrl = null) + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + if (returnUrl != null) + { + return LocalRedirect(returnUrl); + } + else + { + return Page(); + } + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml b/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml new file mode 100644 index 0000000..5f3e58c --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml @@ -0,0 +1,35 @@ +@page +@model ChangePasswordModel +@{ + ViewData["Title"] = "Change password"; +} + +

@ViewData["Title"]

+@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs new file mode 100644 index 0000000..057b434 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class ChangePasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public ChangePasswordModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToPage("./SetPassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + foreach (var error in changePasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Your password has been changed."; + + return RedirectToPage(); + } + } +} diff --git a/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml b/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml new file mode 100644 index 0000000..4a8eb27 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml @@ -0,0 +1,34 @@ +@page +@model DeletePersonalDataModel +@{ + ViewData["Title"] = "Delete Personal Data"; + ViewData["ActivePage"] = ManageNavPages.DeletePersonalData; +} + +

@ViewData["Title"]

+ + + +
+
+
+ @if (Model.RequirePassword) + { +
+ + + +
+ } + +
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs new file mode 100644 index 0000000..3f707d5 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class DeletePersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public DeletePersonalDataModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + } + + public bool RequirePassword { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + if (RequirePassword) + { + if (!await _userManager.CheckPasswordAsync(user, Input.Password)) + { + ModelState.AddModelError(string.Empty, "Password not correct."); + return Page(); + } + } + + var result = await _userManager.DeleteAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred deleteing user with ID '{userId}'."); + } + + await _signInManager.SignOutAsync(); + + _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + return Redirect("~/"); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml b/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml new file mode 100644 index 0000000..f64f11e --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml @@ -0,0 +1,26 @@ +@page +@model Disable2faModel +@{ + ViewData["Title"] = "Disable two-factor authentication (2FA)"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + +@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +

@ViewData["Title"]

+ + + +
+
+ +
+
\ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs b/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs new file mode 100644 index 0000000..184a23e --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class Disable2faModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public Disable2faModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!await _userManager.GetTwoFactorEnabledAsync(user)) + { + throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); + StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; + return RedirectToPage("./TwoFactorAuthentication"); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml b/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml new file mode 100644 index 0000000..0bf709f --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml @@ -0,0 +1,12 @@ +@page +@model DownloadPersonalDataModel +@{ + ViewData["Title"] = "Download Your Data"; + ViewData["ActivePage"] = ManageNavPages.DownloadPersonalData; +} + +

@ViewData["Title"]

+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs new file mode 100644 index 0000000..d145d3b --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class DownloadPersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public DownloadPersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(ApplicationUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json"); + return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json"); + } + } +} diff --git a/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml b/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000..abdd39c --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,54 @@ +@page +@model EnableAuthenticatorModel +@{ + ViewData["Title"] = "Configure authenticator app"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + +@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Windows Phone, + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    +
    To enable QR code generation please read our documentation.
    +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs new file mode 100644 index 0000000..19326ac --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -0,0 +1,158 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class EnableAuthenticatorModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + public EnableAuthenticatorModel( + UserManager userManager, + ILogger logger, + UrlEncoder urlEncoder) + { + _userManager = userManager; + _logger = logger; + _urlEncoder = urlEncoder; + } + + public string SharedKey { get; set; } + + public string AuthenticatorUri { get; set; } + + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadSharedKeyAndQrCodeUriAsync(user); + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + // Strip spaces and hypens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("Input.Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + var userId = await _userManager.GetUserIdAsync(user); + _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + StatusMessage = "Your authenticator app has been verified."; + + if (await _userManager.CountRecoveryCodesAsync(user) == 0) + { + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); + } + else + { + return RedirectToPage("./TwoFactorAuthentication"); + } + } + + private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + SharedKey = FormatKey(unformattedKey); + + var email = await _userManager.GetEmailAsync(user); + AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + AuthenticatorUriFormat, + _urlEncoder.Encode("RazorStripe"), + _urlEncoder.Encode(email), + unformattedKey); + } + } +} diff --git a/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml b/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml new file mode 100644 index 0000000..31027a6 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml @@ -0,0 +1,52 @@ +@page +@model ExternalLoginsModel +@{ + ViewData["Title"] = "Manage your external logins"; +} + +@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +@if (Model.CurrentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.LoginProvider + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Add another service to log in.

+
+ +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs new file mode 100644 index 0000000..6ce1c11 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class ExternalLoginsModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ExternalLoginsModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + + public bool ShowRemoveButton { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + CurrentLogins = await _userManager.GetLoginsAsync(user); + OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; + return Page(); + } + + public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (!result.Succeeded) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred removing external login for user with ID '{userId}'."); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "The external login was removed."; + return RedirectToPage(); + } + + public async Task OnPostLinkLoginAsync(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetLinkLoginCallbackAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); + if (info == null) + { + throw new InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); + } + + var result = await _userManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "The external login was added."; + return RedirectToPage(); + } + } +} diff --git a/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000..7d8d822 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,27 @@ +@page +@model GenerateRecoveryCodesModel +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + +@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..b1407cd --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class GenerateRecoveryCodesModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public GenerateRecoveryCodesModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!isTwoFactorEnabled) + { + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + + _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); + StatusMessage = "You have generated new recovery codes."; + return RedirectToPage("./ShowRecoveryCodes"); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/Index.cshtml b/Areas/Identity/Pages/Account/Manage/Index.cshtml new file mode 100644 index 0000000..76e987c --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -0,0 +1,57 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Profile"; +} + +

@ViewData["Title"]

+@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + +
+
+ + @if (Model.IsEmailConfirmed) + { +
+ + +
+ } + else + { + + + } + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs new file mode 100644 index 0000000..a844e6e --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Data.SqlClient; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public partial class IndexModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + private readonly RoleManager _roleManager; + private readonly ApplicationDbContext _db; + + public IndexModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender, + RoleManager roleManager, + ApplicationDbContext db) + { + _roleManager = roleManager; + _db = db; + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + } + + public string Username { get; set; } + + public bool IsEmailConfirmed { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [Display(Name = "First Name")] + public string FirstName { get; set; } + + [Required] + [Display(Name = "Last Name")] + public string LastName { get; set; } + + [Required] + [EmailAddress] + public string Email { get; set; } + + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var userName = await _userManager.GetUserNameAsync(user); + var email = await _userManager.GetEmailAsync(user); + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + + Username = userName; + + Input = new InputModel + { + FirstName = user.FirstName, + LastName = user.LastName, + Email = email, + PhoneNumber = phoneNumber, + }; + + IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var email = await _userManager.GetEmailAsync(user); + if (Input.Email != email) + { + var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email); + if (!setEmailResult.Succeeded) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'."); + } + } + + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + if (Input.PhoneNumber != phoneNumber) + { + var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'."); + } + } + + + ApplicationUser userInDb = _db.Users.Where(u => u.Email.ToLower().Equals(Input.Email.ToLower())).FirstOrDefault(); + userInDb.FirstName = Input.FirstName; + userInDb.LastName = Input.LastName; + + + await _db.SaveChangesAsync(); + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your profile has been updated"; + return RedirectToPage(); + } + + public async Task OnPostSendVerificationEmailAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var userId = await _userManager.GetUserIdAsync(user); + var email = await _userManager.GetEmailAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId = userId, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + email, + "Confirm your email", + $"Please confirm your account by clicking here."); + + StatusMessage = "Verification email sent. Please check your email."; + return RedirectToPage(); + } + + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs new file mode 100644 index 0000000..2cf1db0 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public static class ManageNavPages + { + public static string Index => "Index"; + + public static string ChangePassword => "ChangePassword"; + + public static string DownloadPersonalData => "DownloadPersonalData"; + + public static string DeletePersonalData => "DeletePersonalData"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string PersonalData => "PersonalData"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); + + public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + public static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + } +} diff --git a/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml b/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml new file mode 100644 index 0000000..0aa5213 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml @@ -0,0 +1,27 @@ +@page +@model PersonalDataModel +@{ + ViewData["Title"] = "Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +
+
+

Your account contains personal data that you have given us. This page allows you to download or delete that data.

+

+ Deleting this data will permanently remove your account, and this cannot be recovered. +

+
+ +
+

+ Delete +

+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs b/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs new file mode 100644 index 0000000..279c0c2 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class PersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public PersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml b/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000..d4eb529 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,24 @@ +@page +@model ResetAuthenticatorModel +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + +@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs new file mode 100644 index 0000000..64e5876 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class ResetAuthenticatorModel : PageModel + { + UserManager _userManager; + private readonly SignInManager _signInManager; + ILogger _logger; + + public ResetAuthenticatorModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."; + + return RedirectToPage("./EnableAuthenticator"); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml b/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml new file mode 100644 index 0000000..eadd2f2 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml @@ -0,0 +1,35 @@ +@page +@model SetPasswordModel +@{ + ViewData["Title"] = "Set password"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

Set your password

+@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs new file mode 100644 index 0000000..e9bb147 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class SetPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public SetPasswordModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToPage("./ChangePassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); + if (!addPasswordResult.Succeeded) + { + foreach (var error in addPasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your password has been set."; + + return RedirectToPage(); + } + } +} diff --git a/Areas/Identity/Pages/Account/Manage/Subscriptions.cshtml b/Areas/Identity/Pages/Account/Manage/Subscriptions.cshtml new file mode 100644 index 0000000..692823e --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/Subscriptions.cshtml @@ -0,0 +1,116 @@ +@page +@model SubscriptionsModel +@{ + ViewData["Title"] = "Subscriptions"; +} + +

Subcriptions

+@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +
+ @{ if (Model.StripeCustomerId != null) + { + if (Model.Subscriptions.Any()) + { + foreach (var subscription in Model.Subscriptions) + { +
+ Status: @subscription.Status

@subscription.Plan.Nickname : @subscription.Plan.Amount @subscription.Plan.Currency : @subscription.Start - @subscription.CurrentPeriodEnd

+
+ } + + } + + } + else + { + + } + } +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+ + @* +
+ +
+ +
+ + + +
+
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+ *@ + +
+
+ +@section Scripts { + + + @* + *@ +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/Subscriptions.cshtml.cs b/Areas/Identity/Pages/Account/Manage/Subscriptions.cshtml.cs new file mode 100644 index 0000000..34e4aca --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/Subscriptions.cshtml.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Data.SqlClient; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RazorStripe.Data; +using RazorStripe.Data.Models; +using RazorStripe.Extensions; +using RazorStripe.Services; +using Stripe; +using Stripe.Infrastructure; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class SubscriptionsModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + private readonly RoleManager _roleManager; + private readonly ApplicationDbContext _db; + private readonly StripeSettings _stripeSettings; + + public SubscriptionsModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender, + RoleManager roleManager, + ApplicationDbContext db, + IOptions stripeSettings) + { + _roleManager = roleManager; + _db = db; + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + _stripeSettings = stripeSettings.Value; + } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public SelectList SubscriptionProducts { get; set; } + public SelectList SubscriptionPlans { get; set; } + + public List Subscriptions { get; set; } + + public string StripeKey { get; set; } + public string UserEmail { get; set; } + public string StripeCustomerId { get; set; } + + public long? PlanPrice { get; set; } + + public class InputModel + { + public string ProductId { get; set; } + + public string PlanId { get; set; } + } + + public JsonResult OnGetProducts(string productId) + { + if (string.IsNullOrEmpty(productId)) + { + return new JsonResult(null); + } + StripeList plans = GetProductPlans(productId); + SubscriptionPlans = new SelectList(plans, "Id", "Nickname"); + return new JsonResult(SubscriptionPlans); + } + + public JsonResult OnGetPlanPrice(string planId) + { + if (string.IsNullOrEmpty(planId)) + { + return new JsonResult(null); + } + PlanPrice = GetPlanPrice(planId); + + return new JsonResult(PlanPrice); + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + StripeCustomerId = user.CustomerIdentifier; + var nullCustomerId = string.IsNullOrEmpty(StripeCustomerId); + UserEmail = user.Email; + + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var product = new ProductService(); + var productsOptions = new ProductListOptions + { + }; + StripeList products = product.List(productsOptions); + + SubscriptionProducts = new SelectList(products, "Id", "Name"); + + if (!nullCustomerId) + { + var subcriptionService = new SubscriptionService(); + IEnumerable response = subcriptionService.List(new SubscriptionListOptions + { + CustomerId = StripeCustomerId + }); + Subscriptions = response.ToList(); + } + + StripeKey = _stripeSettings.PublishableKey; + + return Page(); + } + + public async Task OnPostAsync([FromForm]string stripeToken) + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + UserEmail = user.Email; + StripeCustomerId = user.CustomerIdentifier; + var nullCustomerId = string.IsNullOrEmpty(StripeCustomerId); + var planId = Input.PlanId; + var planAmount = GetPlanPrice(planId); + + var customerService = new CustomerService(); + Customer customerLookup = new Customer(); + + if (!nullCustomerId) + { + customerLookup = customerService.Get(StripeCustomerId); + } + + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + //Create new customer if doesnt exist + if (nullCustomerId || customerLookup.Deleted == true) + { + var customers = new CustomerService(); + + var customer = customers.Create(new CustomerCreateOptions + { + Email = UserEmail, + SourceToken = stripeToken, + PlanId = planId, + Description = UserEmail + " " + "[" + user.Id + "]" + }); + + user.CustomerIdentifier = customer.Id; + StripeCustomerId = user.CustomerIdentifier; + } + else + { + var subcriptionService = new SubscriptionService(); + var subscriptionItems = new List + { + new SubscriptionItemOption + { + PlanId = planId + } + }; + var stripeSubscription = subcriptionService.Create(new SubscriptionCreateOptions + { + CustomerId = StripeCustomerId, + Items = subscriptionItems + }); + } + + Charge charge = new Charge(); + if (planAmount > 0) + { + var chargeOptions = new ChargeCreateOptions + { + Amount = planAmount, + Currency = "usd", + Description = "RazorStripe for" + " " + UserEmail, + CustomerId = StripeCustomerId, + }; + var chargeService = new ChargeService(); + charge = chargeService.Create(chargeOptions); + } + await _db.SaveChangesAsync(); + + await _signInManager.RefreshSignInAsync(user); + + StatusMessage = "Your payment: " + charge.Status; + + return RedirectToPage(); + } + + private StripeList GetProductPlans(string productId) + { + var service = new PlanService(); + var serviceOptions = new PlanListOptions + { + ProductId = productId + }; + StripeList plans = service.List(serviceOptions); + + return plans; + } + + private long? GetPlanPrice(string planId) + { + var service = new PlanService(); + Plan plan = service.Get(planId); + long? planPrice = plan.Amount; + + return planPrice; + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml b/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 0000000..b36988f --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,56 @@ +@page +@model TwoFactorAuthenticationModel +@{ + ViewData["Title"] = "Two-factor authentication (2FA)"; +} + +@await Html.PartialAsync("_StatusMessage", Model.StatusMessage) +

@ViewData["Title"]

+@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (Model.RecoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + if (Model.IsMachineRemembered) + { +
+ +
+ } + Disable 2FA + Reset recovery codes +} + +
Authenticator app
+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Setup authenticator app + Reset authenticator app +} + +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs new file mode 100644 index 0000000..eb4972a --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account.Manage +{ + public class TwoFactorAuthenticationModel : PageModel + { + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; + + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public TwoFactorAuthenticationModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + public bool HasAuthenticator { get; set; } + + public int RecoveryCodesLeft { get; set; } + + [BindProperty] + public bool Is2faEnabled { get; set; } + + public bool IsMachineRemembered { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; + Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); + + return Page(); + } + + public async Task OnPost() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _signInManager.ForgetTwoFactorClientAsync(); + StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code."; + return RedirectToPage(); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Manage/_Layout.cshtml b/Areas/Identity/Pages/Account/Manage/_Layout.cshtml new file mode 100644 index 0000000..66d5ecd --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/_Layout.cshtml @@ -0,0 +1,22 @@ +@{ + Layout = "/Areas/Identity/Pages/_Layout.cshtml"; +} + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} diff --git a/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml new file mode 100644 index 0000000..cd90abe --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -0,0 +1,15 @@ +@using RazorStripe.Data.Models +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + diff --git a/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml b/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000..e996841 --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml b/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml new file mode 100644 index 0000000..01b8c9f --- /dev/null +++ b/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using RazorStripe.Areas.Identity.Pages.Account.Manage diff --git a/Areas/Identity/Pages/Account/Register.cshtml b/Areas/Identity/Pages/Account/Register.cshtml new file mode 100644 index 0000000..fc7821f --- /dev/null +++ b/Areas/Identity/Pages/Account/Register.cshtml @@ -0,0 +1,58 @@ +@page +@model RegisterModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/Register.cshtml.cs b/Areas/Identity/Pages/Account/Register.cshtml.cs new file mode 100644 index 0000000..0fc1938 --- /dev/null +++ b/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class RegisterModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + private readonly RoleManager _roleManager; + private readonly ApplicationDbContext _db; + + public RegisterModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender, + RoleManager roleManager, + ApplicationDbContext db) + { + _roleManager = roleManager; + _db = db; + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + public string Username { get; set; } + + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + [Required] + [Display(Name = "First Name")] + public string FirstName { get; set; } + + [Required] + [Display(Name = "Last Name")] + public string LastName { get; set; } + + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } + + public void OnGet(string returnUrl = null) + { + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + if (ModelState.IsValid) + { + var user = new ApplicationUser + { + UserName = Input.Username, + Email = Input.Email, + FirstName = Input.FirstName, + LastName = Input.LastName, + PhoneNumber = Input.PhoneNumber + }; + + var result = await _userManager.CreateAsync(user, Input.Password); + + if (result.Succeeded) + { + + await _db.SaveChangesAsync(); + + + _logger.LogInformation("User created a new account with password."); + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId = user.Id, code = code }, + protocol: Request.Scheme); + + await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", + $"Please confirm your account by clicking here."); + + await _signInManager.SignInAsync(user, isPersistent: true); + return LocalRedirect(returnUrl); + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/Account/ResetPassword.cshtml b/Areas/Identity/Pages/Account/ResetPassword.cshtml new file mode 100644 index 0000000..ba4d6ef --- /dev/null +++ b/Areas/Identity/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,37 @@ +@page +@model ResetPasswordModel +@{ + ViewData["Title"] = "Reset password"; +} + +

@ViewData["Title"]

+

Reset your password.

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 0000000..955d1dc --- /dev/null +++ b/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using RazorStripe.Data; +using RazorStripe.Data.Models; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordModel : PageModel + { + private readonly UserManager _userManager; + + public ResetPasswordModel(UserManager userManager) + { + _userManager = userManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } + + public IActionResult OnGet(string code = null) + { + if (code == null) + { + return BadRequest("A code must be supplied for password reset."); + } + else + { + Input = new InputModel + { + Code = code + }; + return Page(); + } + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToPage("./ResetPasswordConfirmation"); + } + + var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + return RedirectToPage("./ResetPasswordConfirmation"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + } +} diff --git a/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml b/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000..a9972d4 --- /dev/null +++ b/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,10 @@ +@page +@model ResetPasswordConfirmationModel +@{ + ViewData["Title"] = "Reset password confirmation"; +} + +

@ViewData["Title"]

+

+ Your password has been reset. Please click here to log in. +

diff --git a/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000..3c35680 --- /dev/null +++ b/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordConfirmationModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/Areas/Identity/Pages/Account/_ViewImports.cshtml b/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..9c64c25 --- /dev/null +++ b/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using RazorStripe.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/Areas/Identity/Pages/Error.cshtml b/Areas/Identity/Pages/Error.cshtml new file mode 100644 index 0000000..b1f3143 --- /dev/null +++ b/Areas/Identity/Pages/Error.cshtml @@ -0,0 +1,23 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, 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 ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/Areas/Identity/Pages/Error.cshtml.cs b/Areas/Identity/Pages/Error.cshtml.cs new file mode 100644 index 0000000..9812fbe --- /dev/null +++ b/Areas/Identity/Pages/Error.cshtml.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; + +namespace RazorStripe.Areas.Identity.Pages +{ + [AllowAnonymous] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} \ No newline at end of file diff --git a/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..bacc0ae --- /dev/null +++ b/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/Areas/Identity/Pages/_ViewImports.cshtml b/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..68a1da4 --- /dev/null +++ b/Areas/Identity/Pages/_ViewImports.cshtml @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Identity +@using RazorStripe.Areas.Identity +@using RazorStripe.Data +@using RazorStripe.Data.Models +@namespace RazorStripe.Areas.Identity.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Areas/Identity/Pages/_ViewStart.cshtml b/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..7bd9b6b --- /dev/null +++ b/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Pages/Shared/_Layout.cshtml"; +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..d78d20e --- /dev/null +++ b/Data/ApplicationDbContext.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RazorStripe.Data.Models; + +namespace RazorStripe.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Customize the ASP.NET Identity model and override the defaults if needed. For example, + // you can rename the ASP.NET Identity table names and more. Add your customizations + // after calling base.OnModelCreating(builder); + } + } +} \ No newline at end of file diff --git a/Data/DbInitializer.cs b/Data/DbInitializer.cs new file mode 100644 index 0000000..57e9e89 --- /dev/null +++ b/Data/DbInitializer.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using RazorStripe.Data.Models; + +namespace RazorStripe.Data +{ + public class DbInitializer + { + public static void Initialize(ApplicationDbContext context) + { + //TODO: remove code for migrations + context.Database.EnsureCreated(); + + } + } +} \ No newline at end of file diff --git a/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..93715fb --- /dev/null +++ b/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,236 @@ +// +using System; +using RazorStripe.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace RazorStripe.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..76602e1 --- /dev/null +++ b/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,220 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RazorStripe.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(maxLength: 128, nullable: false), + ProviderKey = table.Column(maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(maxLength: 128, nullable: false), + Name = table.Column(maxLength: 128, nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..868f90e --- /dev/null +++ b/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,234 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RazorStripe.Data; + +namespace RazorStripe.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Models/ApplicationUser.cs b/Data/Models/ApplicationUser.cs new file mode 100644 index 0000000..fe18737 --- /dev/null +++ b/Data/Models/ApplicationUser.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Stripe; + +namespace RazorStripe.Data.Models +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + public ApplicationUser() + { + Subscriptions = new List(); + } + + public virtual ICollection Subscriptions { get; set; } + + public string CustomerIdentifier { get; set; } + + [Display(Name = "Full Name")] + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string FullName + { + get + { + return FirstName + " " + LastName; + } + } + + } +} \ No newline at end of file diff --git a/Data/Models/UserSubscription.cs b/Data/Models/UserSubscription.cs new file mode 100644 index 0000000..05b7432 --- /dev/null +++ b/Data/Models/UserSubscription.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace RazorStripe.Data.Models +{ + public class UserSubscription + { + [Key] + public Guid UserSubscriptionId { get; set; } + + public DateTime PurchaseDate { get; set; } + + public DateTime ExpirationDate { get; set; } + + public int Amount { get; set; } + + public string UserId { get; set; } + + public string PaymentSubscriptionId { get; set; } + + public Guid ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/Extensions/EmailSenderExtensions.cs b/Extensions/EmailSenderExtensions.cs new file mode 100644 index 0000000..c8b549c --- /dev/null +++ b/Extensions/EmailSenderExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.UI.Services; +using RazorStripe.Services; + +namespace RazorStripe.Services +{ + public static class EmailSenderExtensions + { + public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) + { + return emailSender.SendEmailAsync(email, "Confirm your email", + $"Please confirm your account by clicking here."); + } + + public static Task SendResetPasswordAsync(this IEmailSender emailSender, string email, string callbackUrl) + { + return emailSender.SendEmailAsync(email, "Reset Password", + $"Please reset your password by clicking here."); + } + } +} \ No newline at end of file diff --git a/Extensions/EnumExtensions.cs b/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..47a25a2 --- /dev/null +++ b/Extensions/EnumExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace RazorStripe.Extensions +{ + public static class EnumExtensions + { + // + // Retrieves the property on the + // of the current enum value, or the enum's member name if the is not present. + // + // This enum member to get the name for. + // The property on the attribute, if present. + public static string GetDisplayName(this Enum val) + { + return val.GetType() + .GetMember(val.ToString()) + .FirstOrDefault() + ?.GetCustomAttribute(false) + ?.Name + ?? val.ToString(); + } + } +} \ No newline at end of file diff --git a/Extensions/IEnumerableExtensions.cs b/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..8caee9d --- /dev/null +++ b/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RazorStripe.Extensions +{ + public static class IEnumerableExtensions + { + public static IEnumerable ToSelectListItem(this IEnumerable items, int selectedValue) + { + return from item in items + select new SelectListItem + { + Text = item.GetPropertyValue("Name"), + Value = item.GetPropertyValue("Id"), + Selected = item.GetPropertyValue("Id").Equals(selectedValue.ToString()) + }; + } + } +} \ No newline at end of file diff --git a/Extensions/PageConventionCollectionExtensions.cs b/Extensions/PageConventionCollectionExtensions.cs new file mode 100644 index 0000000..fbec0de --- /dev/null +++ b/Extensions/PageConventionCollectionExtensions.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Authorization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RazorStripe.Extensions +{ + public static class PageConventionCollectionExtensions + { + public static PageConventionCollection AuthorizeFolder(this PageConventionCollection conventions, string folderPath, string[] roles) + { + if (conventions == null) + { + throw new ArgumentNullException(nameof(conventions)); + } + + if (string.IsNullOrEmpty(folderPath)) + { + throw new ArgumentException("Argument cannot be null or empty.", nameof(folderPath)); + } + + var policy = new AuthorizationPolicyBuilder(). + RequireRole(roles).Build(); + var authorizeFilter = new AuthorizeFilter(policy); + conventions.AddFolderApplicationModelConvention(folderPath, model => model.Filters.Add(authorizeFilter)); + return conventions; + } + + public static PageConventionCollection AuthorizePage(this PageConventionCollection conventions, string pageName, string[] roles) + { + if (conventions == null) + { + throw new ArgumentNullException(nameof(conventions)); + } + + if (string.IsNullOrEmpty(pageName)) + { + throw new ArgumentException("Argument cannot be null or empty.", nameof(pageName)); + } + + var policy = new AuthorizationPolicyBuilder(). + RequireRole(roles).Build(); + var authorizeFilter = new AuthorizeFilter(policy); + conventions.AddPageApplicationModelConvention(pageName, model => model.Filters.Add(authorizeFilter)); + return conventions; + } + } +} \ No newline at end of file diff --git a/Extensions/ReflectionExtensions.cs b/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..b89b790 --- /dev/null +++ b/Extensions/ReflectionExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RazorStripe.Extensions +{ + public static class ReflectionExtensions + { + public static string GetPropertyValue(this T item, string propertyName) + { + return item.GetType().GetProperty(propertyName).GetValue(item, null).ToString(); + } + } +} \ No newline at end of file diff --git a/Extensions/UrlHelperExtensions.cs b/Extensions/UrlHelperExtensions.cs new file mode 100644 index 0000000..4caa644 --- /dev/null +++ b/Extensions/UrlHelperExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc +{ + public static class UrlHelperExtensions + { + public static string GetLocalUrl(this IUrlHelper urlHelper, string localUrl) + { + if (!urlHelper.IsLocalUrl(localUrl)) + { + return urlHelper.Page("/Index"); + } + + return localUrl; + } + + public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId, code }, + protocol: scheme); + } + + public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Page( + "/Account/ResetPassword", + pageHandler: null, + values: new { userId, code }, + protocol: scheme); + } + } +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..492fba9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2018-2019 Dragon Mastery LLC + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Pages/About.cshtml b/Pages/About.cshtml new file mode 100644 index 0000000..3c090d1 --- /dev/null +++ b/Pages/About.cshtml @@ -0,0 +1,9 @@ +@page +@model AboutModel +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"]

+

@Model.Message

+ +

Use this area to provide additional information.

diff --git a/Pages/About.cshtml.cs b/Pages/About.cshtml.cs new file mode 100644 index 0000000..cdc7f89 --- /dev/null +++ b/Pages/About.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Pages +{ + public class AboutModel : PageModel + { + public string Message { get; set; } + + public void OnGet() + { + Message = "Your application description page."; + } + } +} diff --git a/Pages/Contact.cshtml b/Pages/Contact.cshtml new file mode 100644 index 0000000..b683c82 --- /dev/null +++ b/Pages/Contact.cshtml @@ -0,0 +1,19 @@ +@page +@model ContactModel +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"]

+

@Model.Message

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/Pages/Contact.cshtml.cs b/Pages/Contact.cshtml.cs new file mode 100644 index 0000000..579eca5 --- /dev/null +++ b/Pages/Contact.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Pages +{ + public class ContactModel : PageModel + { + public string Message { get; set; } + + public void OnGet() + { + Message = "Your contact page."; + } + } +} diff --git a/Pages/Error.cshtml b/Pages/Error.cshtml new file mode 100644 index 0000000..b1f3143 --- /dev/null +++ b/Pages/Error.cshtml @@ -0,0 +1,23 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, 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 ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/Pages/Error.cshtml.cs b/Pages/Error.cshtml.cs new file mode 100644 index 0000000..e89b053 --- /dev/null +++ b/Pages/Error.cshtml.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} \ No newline at end of file diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml new file mode 100644 index 0000000..c3bd6f5 --- /dev/null +++ b/Pages/Index.cshtml @@ -0,0 +1,98 @@ +@page +@model IndexModel +@using Microsoft.Extensions.Options + +@{ + ViewData["Title"] = "Home page"; +} + + + + \ No newline at end of file diff --git a/Pages/Index.cshtml.cs b/Pages/Index.cshtml.cs new file mode 100644 index 0000000..d07667e --- /dev/null +++ b/Pages/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Pages +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/Pages/Privacy.cshtml b/Pages/Privacy.cshtml new file mode 100644 index 0000000..f3787ba --- /dev/null +++ b/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

\ No newline at end of file diff --git a/Pages/Privacy.cshtml.cs b/Pages/Privacy.cshtml.cs new file mode 100644 index 0000000..a1bca42 --- /dev/null +++ b/Pages/Privacy.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorStripe.Pages +{ + public class PrivacyModel : PageModel + { + public void OnGet() + { + } + } +} \ No newline at end of file diff --git a/Pages/Shared/_CookieConsentPartial.cshtml b/Pages/Shared/_CookieConsentPartial.cshtml new file mode 100644 index 0000000..f444e46 --- /dev/null +++ b/Pages/Shared/_CookieConsentPartial.cshtml @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Http.Features + +@{ + var consentFeature = Context.Features.Get(); + var showBanner = !consentFeature?.CanTrack ?? false; + var cookieString = consentFeature?.CreateConsentCookie(); +} + +@if (showBanner) +{ + + +} \ No newline at end of file diff --git a/Pages/Shared/_Layout.cshtml b/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..383f259 --- /dev/null +++ b/Pages/Shared/_Layout.cshtml @@ -0,0 +1,107 @@ +@using Microsoft.AspNetCore.Http; +@inject IHttpContextAccessor HttpContextAccessor + + + + + + @ViewData["Title"] - RazorStripe + + + + + + + + + + + + + + + + + + + + + +
+
+ @RenderBody() +
+
+

© 2012-@DateTime.Today.Year - RazorStripe

+
+
+ + + + + + + + + + + + + + + + + + + + @RenderSection("Scripts", required: false) + + \ No newline at end of file diff --git a/Pages/Shared/_LoginPartial.cshtml b/Pages/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000..3d5044f --- /dev/null +++ b/Pages/Shared/_LoginPartial.cshtml @@ -0,0 +1,40 @@ +@using Microsoft.AspNetCore.Identity +@using RazorStripe.Data.Models +@inject SignInManager SignInManager +@inject UserManager UserManager + +@if (SignInManager.IsSignedIn(User)) +{ + + + + +} +else +{ + + +} \ No newline at end of file diff --git a/Pages/Shared/_ValidationScriptsPartial.cshtml b/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..ef848fe --- /dev/null +++ b/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/Pages/_ViewImports.cshtml b/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..36f2f4d --- /dev/null +++ b/Pages/_ViewImports.cshtml @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Identity +@using RazorStripe +@using RazorStripe.Data +@using RazorStripe.Data.Models +@namespace RazorStripe.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Pages/_ViewStart.cshtml b/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..cc90010 --- /dev/null +++ b/Program.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RazorStripe.Data; + +namespace RazorStripe +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + var context = services.GetRequiredService(); + DbInitializer.Initialize(context); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred creating the DB."); + } + } + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8c363d --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# RazorStripe + +RazorStripe is an ASP.NET Core 2.2 Template that combines Razor Pages with Stripe integration. + +## Run the Project +1. Download/fork the project and run the .sln +2. Add your own stripe keys in the [appsettings](appsettings.json) +3. Add your credentials in the [EmailSender](/Services/EmailSender.cs) +4. Re-save the [libman file](libman.json) and verify that the 5 folders have been downloaded into the [wwwroot](/wwwroot). +5. Run on IIS Express + +## Custom Templates +There is also a .template.config folder so you can use dotnet new -i to create your own templates from this. More information [HERE](https://docs.microsoft.com/en-us/dotnet/core/tools/custom-templates) + +## Community +Contributions are welcome! Follow the community and contribute [HERE](https://discord.gg/6SAfBMc) + +## License + +See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). diff --git a/RazorStripe.csproj b/RazorStripe.csproj new file mode 100644 index 0000000..92304e2 --- /dev/null +++ b/RazorStripe.csproj @@ -0,0 +1,68 @@ + + + + netcoreapp2.2 + aspnet-RazorStripe-66F3C237-5BC7-493C-B78E-D8A31F0EDFE1 + false + InProcess + + + + + + + + + + + + + + + + + + + + + + + + <_ContentIncludedByDefault Remove="bundleconfig.json" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RazorStripe.sln b/RazorStripe.sln new file mode 100644 index 0000000..635ff48 --- /dev/null +++ b/RazorStripe.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28407.52 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorStripe", "RazorStripe.csproj", "{83FE2DFC-8123-4928-B045-7C7AC7F0D4CD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {83FE2DFC-8123-4928-B045-7C7AC7F0D4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83FE2DFC-8123-4928-B045-7C7AC7F0D4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83FE2DFC-8123-4928-B045-7C7AC7F0D4CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83FE2DFC-8123-4928-B045-7C7AC7F0D4CD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5394A107-FB45-4FE7-B5CA-B86B154CB9EE} + EndGlobalSection +EndGlobal diff --git a/Services/EmailSender.cs b/Services/EmailSender.cs new file mode 100644 index 0000000..38b3f26 --- /dev/null +++ b/Services/EmailSender.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Identity.UI.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Mail; +using System.Threading.Tasks; + +namespace RazorStripe.Services +{ + // This class is used by the application to send email for account confirmation and password reset. + // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 + public class EmailSender : IEmailSender + { + public Task SendEmailAsync(string email, string subject, string message) + { + SmtpClient client = new SmtpClient("smtp.mailgun.org", 587); + client.UseDefaultCredentials = false; + //add credentials + client.Credentials = new NetworkCredential("postmaster@email.email.com", "xxxx"); + client.EnableSsl = true; + + //MailMessage mailMessage = new MailMessage + //{ + // From = new MailAddress("email@email.com"), + // Body = message, + // IsBodyHtml = true + //}; + MailMessage mailMessage = new MailMessage(); + //from address + mailMessage.From = new MailAddress("email@email.com"); + mailMessage.To.Add(email); + mailMessage.Body = message; + mailMessage.IsBodyHtml = true; + mailMessage.Subject = subject; + + //mailMessage.Body = message; + //mailMessage.IsBodyHtml = true; + client.Send(mailMessage); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Services/StripeSettings.cs b/Services/StripeSettings.cs new file mode 100644 index 0000000..0fe396c --- /dev/null +++ b/Services/StripeSettings.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RazorStripe.Services +{ + public class StripeSettings + { + public string SecretKey { get; set; } + public string PublishableKey { get; set; } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..98207bb --- /dev/null +++ b/Startup.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RazorStripe.Data; +using RazorStripe.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using RazorStripe.Extensions; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.StaticFiles; +using Stripe; +using RazorStripe.Data.Models; +using Microsoft.AspNetCore.Identity.UI; + +namespace RazorStripe +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.Configure(options => + { + // This lambda determines whether user consent for non-essential cookies is needed for a given request. + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); + + services.AddDbContext(options => + options.UseSqlServer( + Configuration.GetConnectionString("DefaultConnection"))); + + services.AddIdentity() + .AddEntityFrameworkStores() + //.AddDefaultUI(UIFramework.Bootstrap4) + .AddDefaultTokenProviders(); + + // Configure Identity + services.Configure(options => + { + // Password settings. + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequireUppercase = true; + options.Password.RequiredLength = 6; + options.Password.RequiredUniqueChars = 1; + + // Lockout settings. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + // User settings. + options.User.AllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + options.User.RequireUniqueEmail = true; + }); + + services.ConfigureApplicationCookie(options => + { + // Cookie settings + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromHours(24); + + options.LoginPath = $"/Identity/Account/Login"; + options.LogoutPath = $"/Identity/Account/Logout"; + options.AccessDeniedPath = $"/Identity/Account/AccessDenied"; + options.SlidingExpiration = true; + }); + + services.Configure(Configuration.GetSection("Stripe")); + + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) + .AddRazorPagesOptions(options => + { + //Logged in user pages + options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage"); + options.Conventions.AuthorizeFolder("/ClientProfiles"); + options.Conventions.AuthorizeFolder("/ClientLocations"); + options.Conventions.AuthorizeFolder("/ClientContacts"); + options.Conventions.AuthorizeFolder("/SalesManager"); + options.Conventions.AuthorizeFolder("/MasterAgentContacts"); + + //customer pages + + //admin pages + }); + + services.AddAuthorization(options => + { + }); + + //services.AddAuthentication().AddFacebook(facebookOptions => + //{ + // facebookOptions.AppId = "xxx"; + // facebookOptions.AppSecret = "xxx"; + //}); + + //services.AddAuthentication().AddGoogle(googleOptions => + //{ + // googleOptions.ClientId = "xxx"; + // googleOptions.ClientSecret = "xxx"; + //}); + + //Register no-op EmailSender used by account confirmation and password reset during + // development For more information on how to enable account confirmation and password + // reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 + + services.AddSingleton(); + //services.Configure(Configuration); + + services.AddSession(options => + { + options.IdleTimeout = TimeSpan.FromMinutes(30); + options.Cookie.HttpOnly = true; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseDatabaseErrorPage(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + StripeConfiguration.SetApiKey(Configuration.GetSection("Stripe")["SecretKey"]); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseCookiePolicy(); + app.UseAuthentication(); + app.UseSession(); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller}/{acion=Index}/{id?}"); + }); + + // Set up custom content types -associating file extension to MIME type + var provider = new FileExtensionContentTypeProvider(); + app.UseStaticFiles(new StaticFileOptions + { + ContentTypeProvider = provider + }); + } + } +} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..cad98b3 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,15 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=RazorStripe;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "Stripe": { + "SecretKey": "sk_test_xxx", + "PublishableKey": "pk_test_xxx" + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/bundleconfig.json b/bundleconfig.json new file mode 100644 index 0000000..2aee0e7 --- /dev/null +++ b/bundleconfig.json @@ -0,0 +1,14 @@ +[ + { + "outputFileName": "wwwroot/css/site.min.css", + "inputFiles": [ + "wwwroot/css/site.css" + ] + }, + { + "outputFileName": "wwwroot/js/site.min.js", + "inputFiles": [ + "wwwroot/js/site.js" + ] + } +] diff --git a/libman.json b/libman.json new file mode 100644 index 0000000..8dec40e --- /dev/null +++ b/libman.json @@ -0,0 +1,30 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "twitter-bootstrap@4.1.1", + "destination": "wwwroot/lib/twitter-bootstrap/" + }, + { + "provider": "cdnjs", + "library": "popper.js@1.14.6", + "destination": "wwwroot/lib/popper.js/" + }, + { + "provider": "cdnjs", + "library": "jquery@3.1.1", + "destination": "wwwroot/lib/jquery/" + }, + { + "provider": "cdnjs", + "library": "jqueryui@1.12.1", + "destination": "wwwroot/lib/jqueryui/" + }, + { + "provider": "cdnjs", + "library": "jquery-timepicker@1.10.0", + "destination": "wwwroot/lib/jquery-timepicker/" + } + ] +} \ No newline at end of file diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css new file mode 100644 index 0000000..0cf5ffe --- /dev/null +++ b/wwwroot/css/site.css @@ -0,0 +1,253 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification\ +for details on configuring this project to bundle and minify static web assets. */ +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; + } +} + +/* Remove margins and padding from the list */ +.my-list { + margin: 0; + padding: 0; +} + + /* Style the list items */ + .my-list li { + cursor: pointer; + position: relative; + padding: 12px 8px 12px 40px; + list-style-type: none; + background: #eee; + font-size: 18px; + transition: 0.2s; + /* make the list items unselectable */ + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + /* Set all odd list items to a different color (zebra-stripes) */ + .my-list li:nth-child(odd) { + background: #f9f9f9; + } + + /* Darker background-color on hover */ + .my-list li:hover { + background: #ddd; + } + + /* When clicked on, add a background color and strike out text */ + .my-list li.checked { + background: #888; + color: #fff; + text-decoration: line-through; + } + + /* Add a "checked" mark when clicked on */ + .my-list li.checked::before { + content: ''; + position: absolute; + border-color: #fff; + border-style: solid; + border-width: 0 2px 2px 0; + top: 10px; + left: 16px; + transform: rotate(45deg); + height: 15px; + width: 7px; + } + +span { + vertical-align: text-top; +} + +.alert-dismissible .close { + padding: .3rem 1.25rem; +} +/* Style the close button */ +.close:hover { + background-color: #f44336; + color: white; +} + +/* Style the header */ +.header { + background-color: #f44336; + padding: 30px 40px; + color: white; + text-align: center; +} + + /* Clear floats after the header */ + .header:after { + content: ""; + display: table; + clear: both; + } + +/* Style the input */ + +/* Style the "Add" button */ +.add-btn { + padding: 10px; + width: 25%; + background: #d9d9d9; + color: #555; + float: left; + text-align: center; + font-size: 16px; + cursor: pointer; + transition: 0.3s; + border-radius: 0; +} + + .add-btn:hover { + background-color: #bbb; + } + +.text-center { + text-align: center; +} + +.healthbar { + width: 80%; + height: 40px; + background-color: #a51417; + margin: auto; + transition: width 500ms; +} + +.controls, .log { + margin-top: 30px; + text-align: center; + padding: 10px; + border: 1px solid #ccc; + box-shadow: 0px 3px 6px #ccc; +} + +.turn { + margin-top: 20px; + margin-bottom: 20px; + font-weight: bold; + font-size: 22px; +} + +.log ul { + list-style: none; + font-weight: bold; + text-transform: uppercase; +} + + .log ul li { + margin: 5px; + } + + .log ul .player-turn { + color: blue; + background-color: #e4e8ff; + } + + .log ul .monster-turn { + color: red; + background-color: #ffc0c1; + } + +button { + margin: 5px; +} + +.start-game { + background-color: #aaffb0; +} + + .start-game:hover { + background-color: #76ff7e; + } + +.basic-1 { + background-color: #ff7367; +} + + .basic-1:hover { + background-color: #ff3f43; + } + +.basic-2 { + background-color: #ff7367; +} + + .basic-2:hover { + background-color: #ff3f43; + } + +.basic-3 { + background-color: #ff7367; +} + + .basic-3:hover { + background-color: #ff3f43; + } + +.smash-1 { + background-color: #aaffb0; +} + + .smash-1:hover { + background-color: #76ff7e; + } + +.smash-2 { + background-color: #aaffb0; +} + + .smash-2:hover { + background-color: #76ff7e; + } + +.special-attack { + background-color: #ffaf4f; +} + + .special-attack:hover { + background-color: #ff9a2b; + } + +.give-up { + background-color: #ffffff; +} + + .give-up:hover { + background-color: #c7c7c7; + } \ No newline at end of file diff --git a/wwwroot/css/site.min.css b/wwwroot/css/site.min.css new file mode 100644 index 0000000..d72cdf1 --- /dev/null +++ b/wwwroot/css/site.min.css @@ -0,0 +1 @@ +body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}}.my-list{margin:0;padding:0}.my-list li{cursor:pointer;position:relative;padding:12px 8px 12px 40px;list-style-type:none;background:#eee;font-size:18px;transition:.2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.my-list li:nth-child(odd){background:#f9f9f9}.my-list li:hover{background:#ddd}.my-list li.checked{background:#888;color:#fff;text-decoration:line-through}.my-list li.checked::before{content:'';position:absolute;border-color:#fff;border-style:solid;border-width:0 2px 2px 0;top:10px;left:16px;transform:rotate(45deg);height:15px;width:7px}span{vertical-align:text-top}.alert-dismissible .close{padding:.3rem 1.25rem}.close:hover{background-color:#f44336;color:#fff}.header{background-color:#f44336;padding:30px 40px;color:#fff;text-align:center}.header:after{content:"";display:table;clear:both}.add-btn{padding:10px;width:25%;background:#d9d9d9;color:#555;float:left;text-align:center;font-size:16px;cursor:pointer;transition:.3s;border-radius:0}.add-btn:hover{background-color:#bbb}.text-center{text-align:center}.healthbar{width:80%;height:40px;background-color:#a51417;margin:auto;transition:width 500ms}.controls,.log{margin-top:30px;text-align:center;padding:10px;border:1px solid #ccc;box-shadow:0 3px 6px #ccc}.turn{margin-top:20px;margin-bottom:20px;font-weight:bold;font-size:22px}.log ul{list-style:none;font-weight:bold;text-transform:uppercase}.log ul li{margin:5px}.log ul .player-turn{color:#00f;background-color:#e4e8ff}.log ul .monster-turn{color:#f00;background-color:#ffc0c1}button{margin:5px}.start-game{background-color:#aaffb0}.start-game:hover{background-color:#76ff7e}.basic-1{background-color:#ff7367}.basic-1:hover{background-color:#ff3f43}.basic-2{background-color:#ff7367}.basic-2:hover{background-color:#ff3f43}.basic-3{background-color:#ff7367}.basic-3:hover{background-color:#ff3f43}.smash-1{background-color:#aaffb0}.smash-1:hover{background-color:#76ff7e}.smash-2{background-color:#aaffb0}.smash-2:hover{background-color:#76ff7e}.special-attack{background-color:#ffaf4f}.special-attack:hover{background-color:#ff9a2b}.give-up{background-color:#fff}.give-up:hover{background-color:#c7c7c7} \ No newline at end of file diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico new file mode 100644 index 0000000..5c32f45 Binary files /dev/null and b/wwwroot/favicon.ico differ diff --git a/wwwroot/images/banner1.svg b/wwwroot/images/banner1.svg new file mode 100644 index 0000000..557f31f --- /dev/null +++ b/wwwroot/images/banner1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wwwroot/images/banner2.svg b/wwwroot/images/banner2.svg new file mode 100644 index 0000000..4c48808 --- /dev/null +++ b/wwwroot/images/banner2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wwwroot/images/banner3.svg b/wwwroot/images/banner3.svg new file mode 100644 index 0000000..ab2171c --- /dev/null +++ b/wwwroot/images/banner3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wwwroot/images/banner4.svg b/wwwroot/images/banner4.svg new file mode 100644 index 0000000..38b3d7c --- /dev/null +++ b/wwwroot/images/banner4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wwwroot/js/Subscriptions.js b/wwwroot/js/Subscriptions.js new file mode 100644 index 0000000..9ecd057 --- /dev/null +++ b/wwwroot/js/Subscriptions.js @@ -0,0 +1,56 @@ +$(document).ready(function () { + loadPlans(); + onProductChange(); + onPlanChange(); +}); + +function loadPlans() { + var url = '/Identity/Account/Manage/Subscriptions?handler=Products'; + var productId = $('.subscription-products').val(); + var data = { "productId": productId }; + var plans = $('.subscription-plans'); + plans.empty(); + $.getJSON(url, data, function (productPlans) { + if (productPlans !== null) { + $.each(productPlans, function (index, htmlData) { + plans.append($('