Exercise Files

This commit is contained in:
Jess Chadwick
2018-06-06 23:57:58 -04:00
commit 20458e435e
4436 changed files with 1359080 additions and 0 deletions

View File

@ -0,0 +1,485 @@
using System;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using HPlusSports.Models;
namespace HPlusSports.Controllers
{
[Authorize]
public class AccountController : Controller
{
private ApplicationSignInManager _signInManager;
private ApplicationUserManager _userManager;
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
{
UserManager = userManager;
SignInManager = signInManager;
}
public ApplicationSignInManager SignInManager
{
get
{
return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
}
private set
{
_signInManager = value;
}
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
//
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
//
// GET: /Account/VerifyCode
[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, string returnUrl, bool rememberMe)
{
// Require that the user has already logged in via username/password or external login
if (!await SignInManager.HasBeenVerifiedAsync())
{
return View("Error");
}
return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe });
}
//
// POST: /Account/VerifyCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// The following code protects for brute force attacks against the two factor codes.
// If a user enters incorrect codes for a specified amount of time then the user account
// will be locked out for a specified amount of time.
// You can configure the account lockout settings in IdentityConfig
var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: model.RememberMe, rememberBrowser: model.RememberBrowser);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(model.ReturnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid code.");
return View(model);
}
}
//
// GET: /Account/Register
[AllowAnonymous]
public ActionResult Register()
{
return View();
}
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
// string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
// var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
// await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");
return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
//
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
//
// GET: /Account/ForgotPassword
[AllowAnonymous]
public ActionResult ForgotPassword()
{
return View();
}
//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
// string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
// var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
// await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");
// return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
// If we got this far, something failed, redisplay form
return View(model);
}
//
// GET: /Account/ForgotPasswordConfirmation
[AllowAnonymous]
public ActionResult ForgotPasswordConfirmation()
{
return View();
}
//
// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
return code == null ? View("Error") : View();
}
//
// POST: /Account/ResetPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
AddErrors(result);
return View();
}
//
// GET: /Account/ResetPasswordConfirmation
[AllowAnonymous]
public ActionResult ResetPasswordConfirmation()
{
return View();
}
//
// POST: /Account/ExternalLogin
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
//
// GET: /Account/SendCode
[AllowAnonymous]
public async Task<ActionResult> SendCode(string returnUrl, bool rememberMe)
{
var userId = await SignInManager.GetVerifiedUserIdAsync();
if (userId == null)
{
return View("Error");
}
var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId);
var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe });
}
//
// POST: /Account/SendCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> SendCode(SendCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View();
}
// Generate the token and send it
if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider))
{
return View("Error");
}
return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
}
//
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
//
// POST: /Account/ExternalLoginConfirmation
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Manage");
}
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await AuthenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
ViewBag.ReturnUrl = returnUrl;
return View(model);
}
//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
return RedirectToAction("Index", "Home");
}
//
// GET: /Account/ExternalLoginFailure
[AllowAnonymous]
public ActionResult ExternalLoginFailure()
{
return View();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_userManager != null)
{
_userManager.Dispose();
_userManager = null;
}
if (_signInManager != null)
{
_signInManager.Dispose();
_signInManager = null;
}
}
base.Dispose(disposing);
}
#region Helpers
// Used for XSRF protection when adding external logins
private const string XsrfKey = "XsrfId";
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
internal class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null)
{
}
public ChallengeResult(string provider, string redirectUri, string userId)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
}
#endregion
}
}

View File

@ -0,0 +1,111 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace HPlusSports.Models
{
public class ExternalLoginConfirmationViewModel
{
[Required]
[Display(Name = "Email")]
public string Email { get; set; }
}
public class ExternalLoginListViewModel
{
public string ReturnUrl { get; set; }
}
public class SendCodeViewModel
{
public string SelectedProvider { get; set; }
public ICollection<System.Web.Mvc.SelectListItem> Providers { get; set; }
public string ReturnUrl { get; set; }
public bool RememberMe { get; set; }
}
public class VerifyCodeViewModel
{
[Required]
public string Provider { get; set; }
[Required]
[Display(Name = "Code")]
public string Code { get; set; }
public string ReturnUrl { get; set; }
[Display(Name = "Remember this browser?")]
public bool RememberBrowser { get; set; }
public bool RememberMe { get; set; }
}
public class ForgotViewModel
{
[Required]
[Display(Name = "Email")]
public string Email { get; set; }
}
public class LoginViewModel
{
[Required]
[Display(Name = "Username")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} 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; }
}
public class ResetPasswordViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} 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; }
public string Code { get; set; }
}
public class ForgotPasswordViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
}
}

View File

@ -0,0 +1,10 @@
@{
ViewBag.Title = "Confirm Email";
}
<h2>@ViewBag.Title.</h2>
<div>
<p>
Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
</p>
</div>

View File

@ -0,0 +1,36 @@
@model HPlusSports.Models.ExternalLoginConfirmationViewModel
@{
ViewBag.Title = "Register";
}
<h2>@ViewBag.Title.</h2>
<h3>Associate your @ViewBag.LoginProvider account.</h3>
@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Association Form</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<p class="text-info">
You've successfully authenticated with <strong>@ViewBag.LoginProvider</strong>.
Please enter a user name for this site below and click the Register button to finish
logging in.
</p>
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,8 @@
@{
ViewBag.Title = "Login Failure";
}
<hgroup>
<h2>@ViewBag.Title.</h2>
<h3 class="text-danger">Unsuccessful login with service.</h3>
</hgroup>

View File

@ -0,0 +1,29 @@
@model HPlusSports.Models.ForgotPasswordViewModel
@{
ViewBag.Title = "Forgot your password?";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Enter your email.</h4>
<hr />
@Html.ValidationSummary("", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Email Link" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,13 @@
@{
ViewBag.Title = "Forgot Password Confirmation";
}
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
</hgroup>
<div>
<p>
Please check your email to reset your password.
</p>
</div>

View File

@ -0,0 +1,47 @@
using System.Data.Entity;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
namespace HPlusSports.Models
{
public static class UserRoles
{
public const string Admin = "Admin";
public const string ProductAdmin = "ProductAdmin";
}
// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit https://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser
{
public ApplicationUser() : base()
{
}
public ApplicationUser(string username) : base(username)
{
}
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("HPlusSports_Identity", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
}

View File

@ -0,0 +1,53 @@
@using HPlusSports.Models
@model LoginViewModel
@{
ViewBag.Title = "";
}
<h2>User Login</h2>
<div class="row">
<div class="col-md-8">
<section id="loginForm">
@using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
@Html.CheckBoxFor(m => m.RememberMe)
@Html.LabelFor(m => m.RememberMe)
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Log in" class="btn btn-default" />
</div>
</div>
<p>
@Html.ActionLink("Register as a new user", "Register")
</p>
}
</section>
</div>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security;
namespace HPlusSports.Models
{
public class IndexViewModel
{
public bool HasPassword { get; set; }
public IList<UserLoginInfo> Logins { get; set; }
public string PhoneNumber { get; set; }
public bool TwoFactor { get; set; }
public bool BrowserRemembered { get; set; }
}
public class ManageLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
public IList<AuthenticationDescription> OtherLogins { get; set; }
}
public class FactorViewModel
{
public string Purpose { get; set; }
}
public class SetPasswordViewModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} 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 class ChangePasswordViewModel
{
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} 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 class AddPhoneNumberViewModel
{
[Required]
[Phone]
[Display(Name = "Phone Number")]
public string Number { get; set; }
}
public class VerifyPhoneNumberViewModel
{
[Required]
[Display(Name = "Code")]
public string Code { get; set; }
[Required]
[Phone]
[Display(Name = "Phone Number")]
public string PhoneNumber { get; set; }
}
public class ConfigureTwoFactorViewModel
{
public string SelectedProvider { get; set; }
public ICollection<System.Web.Mvc.SelectListItem> Providers { get; set; }
}
}

View File

@ -0,0 +1,41 @@
@model HPlusSports.Models.RegisterViewModel
@{
ViewBag.Title = "Register";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Create a new account.</h4>
<hr />
@Html.ValidationSummary("", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,42 @@
@model HPlusSports.Models.ResetPasswordViewModel
@{
ViewBag.Title = "Reset password";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Reset your password.</h4>
<hr />
@Html.ValidationSummary("", new { @class = "text-danger" })
@Html.HiddenFor(model => model.Code)
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Reset" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,12 @@
@{
ViewBag.Title = "Reset password confirmation";
}
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
</hgroup>
<div>
<p>
Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
</p>
</div>

View File

@ -0,0 +1,24 @@
@model HPlusSports.Models.SendCodeViewModel
@{
ViewBag.Title = "Send";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("SendCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
@Html.AntiForgeryToken()
@Html.Hidden("rememberMe", @Model.RememberMe)
<h4>Send verification code</h4>
<hr />
<div class="row">
<div class="col-md-8">
Select Two-Factor Authentication Provider:
@Html.DropDownListFor(model => model.SelectedProvider, Model.Providers)
<input type="submit" value="Submit" class="btn btn-default" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,38 @@
@model HPlusSports.Models.VerifyCodeViewModel
@{
ViewBag.Title = "Verify";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
@Html.AntiForgeryToken()
@Html.Hidden("provider", @Model.Provider)
@Html.Hidden("rememberMe", @Model.RememberMe)
<h4>Enter verification code</h4>
<hr />
@Html.ValidationSummary("", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Code, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
@Html.CheckBoxFor(m => m.RememberBrowser)
@Html.LabelFor(m => m.RememberBrowser)
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Submit" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,28 @@
@model HPlusSports.Models.ExternalLoginListViewModel
@using Microsoft.Owin.Security
<h4>Use another service to log in.</h4>
<hr />
@{
var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes();
if (loginProviders.Count() == 0) {
<div>
<p>
There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkId=403804">this article</a>
for details on setting up this ASP.NET application to support logging in via external services.
</p>
</div>
}
else {
using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) {
@Html.AntiForgeryToken()
<div id="socialLoginList">
<p>
@foreach (AuthenticationDescription p in loginProviders) {
<button type="submit" class="btn btn-default" id="@p.AuthenticationType" name="provider" value="@p.AuthenticationType" title="Log in using your @p.Caption account">@p.AuthenticationType</button>
}
</p>
</div>
}
}
}

View File

@ -0,0 +1,108 @@
@model HPlusSports.Models.ShoppingCart
@{
ViewBag.Title = "My Cart";
}
@if (TempData.SuccessMessage() != null)
{
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@TempData.SuccessMessage()
</div>
}
<div class="cart row">
<div>
<hr />
<table class="table">
<thead>
<tr>
<th class="col-md-1">
</th>
<th class="col-md-6">
Product
</th>
<th class="col-md-1">
Quantity
</th>
<th class="col-md-2">
Price
</th>
<th class="col-md-2">
Subtotal
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr class="text-middle">
<td>
<a href="@Url.Action("Remove", "Cart", new { id = item.Id })">
<i class="glyphicon glyphicon-remove text-danger"></i>
</a>
</td>
<td>
<a href="@Url.Product(item.SKU)">
<img class="product-image img-thumbnail" src="@Url.Action("Product", "Images", new { id = item.SKU })" />
<span>@item.Name</span>
</a>
</td>
<td>
<a href="@Url.Action("Add", "Cart", new { item.SKU, quantity = -1 })">
<i class="glyphicon glyphicon-minus"></i>
</a>
@item.Quantity
<a href="@Url.Action("Add", "Cart", new { item.SKU })">
<i class="glyphicon glyphicon-plus"></i>
</a>
</td>
<td>
@item.Price.ToString("C")
</td>
<td>
@item.Total.ToString("C")
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="text-primary">
<td colspan="4" class="text-right">
Subtotal
</td>
<td>
@Model.Subtotal.ToString("C")
</td>
</tr>
<tr class="text-muted">
<td colspan="4" class="text-right">
Tax
</td>
<td>
@Model.Tax.ToString("C")
</td>
</tr>
<tr class="text-muted">
<td colspan="4" class="text-right">
Shipping
</td>
<td>
@Model.Shipping.ToString("C")
</td>
</tr>
<tr class="text-primary text-large">
<th colspan="4" class="text-right">
Total
</th>
<th>
@Model.Total.ToString("C")
</th>
</tr>
</tfoot>
</table>
</div>
</div>

View File

@ -0,0 +1,132 @@
using HPlusSports.Models;
using System;
using System.Data.Entity.Validation;
using System.Linq;
using System.Web.Mvc;
namespace HPlusSports.Controllers
{
public class CartController : Controller
{
private readonly HPlusSportsDbContext _context;
public CartController()
: this(new HPlusSportsDbContext())
{
}
public CartController(HPlusSportsDbContext context)
{
_context = context;
}
public ActionResult Index()
{
var cart = GetCart();
if (cart.Items.Any())
return View("Cart", cart);
return View("EmptyCart");
}
public ActionResult Add(string sku, int quantity = 1)
{
var product = _context.Products.FirstOrDefault(x => x.SKU == sku);
var cart = GetCart();
var item = cart.Items.FirstOrDefault(x => x.SKU == sku);
if (item == null)
{
item = new ShoppingCartItem
{
SKU = product?.SKU,
Name = product?.Name,
MSRP = (product?.MSRP).GetValueOrDefault(),
Price = (product?.Price).GetValueOrDefault(),
Quantity = quantity,
};
cart.Items.Add(item);
}
else
{
item.Quantity += quantity;
}
if (item.Quantity <= 0)
{
cart.Items.Remove(item);
}
try
{
_context.SaveChanges();
}
catch (DbEntityValidationException ex)
{
var errors = ex.EntityValidationErrors.SelectMany(x => x.ValidationErrors);
foreach (var error in errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
if (!ModelState.IsValid)
{
return View();
}
TempData.SuccessMessage($"Successfully added {item.Name} to the cart");
return RedirectToAction("Index", "Cart");
}
public ActionResult Remove(long id)
{
var cart = GetCart();
var item = cart.Items.FirstOrDefault(x => x.Id == id);
if (item == null)
{
ModelState.AddModelError("", "Missing or invalid cart item");
return View();
}
cart.Items.Remove(item);
_context.SaveChanges();
return RedirectToAction("Index", "Cart");
}
private ShoppingCart GetCart()
{
var userId = GetUserId(this);
var cart =
_context.ShoppingCarts
.Include("Items")
.FirstOrDefault(x => x.UserId == userId);
if (cart == null)
{
cart = new ShoppingCart { UserId = userId };
_context.ShoppingCarts.Add(cart);
_context.SaveChanges();
}
cart.Recalculate();
return cart;
}
// Overwriteable function for unit testing
internal Func<Controller, string> GetUserId =
(controller) => controller.User.Identity.Name;
}
}

View File

@ -0,0 +1,16 @@
@model HPlusSports.Models.ShoppingCart
@{
ViewBag.Title = "Cart";
}
<div class="row">
<div class="well well-lg">
<p>Your shopping cart is empty!</p>
<p>
Head on over to
@Html.ActionLink("the Product Catalog", "Index", "Products")
to find something!
</p>
</div>
</div>

View File

@ -0,0 +1,6 @@
@{
ViewBag.Title = "About";
}
<h3>@ViewBag.Message</h3>
<p>Use this area to provide additional information.</p>

View File

@ -0,0 +1,16 @@
@{
ViewBag.Title = "Contact";
}
<h3>@ViewBag.Message</h3>
<address>
One Microsoft Way<br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br />
<strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

View File

@ -0,0 +1,57 @@
using HPlusSports.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace HPlusSports.Controllers
{
[RoutePrefix("home/{name}")]
public class HomeController : Controller
{
private readonly HPlusSportsDbContext _context;
public HomeController()
: this(new HPlusSportsDbContext())
{
}
public HomeController(HPlusSportsDbContext context)
{
_context = context;
}
public ActionResult Index()
{
var categories =
(
from category in _context.Categories
let count = _context.Products.Count(x => x.CategoryId == category.Id)
select new { category, count }
).ToDictionary(x => x.category, x => x.count);
return View(categories);
}
[Route("about")]
public ActionResult About(string name)
{
ViewBag.Message = "Your application description page, " + name;
return View();
}
public ActionResult Broken(string error)
{
throw new Exception(error);
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}

View File

@ -0,0 +1,19 @@
@model IDictionary<HPlusSports.Models.Category, int>
@{
ViewBag.Title = "Categories";
}
<div class="row">
@foreach (var category in Model.Keys)
{
<div class="category col-md-4 text-center">
<a href="@Url.Action("Category", "Products", new { id = category.Key })">
<img class="img-thumbnail" src="@Url.Action("Category", "Images", new { id = category.Key })" />
<h3>@category.Name</h3>
</a>
</div>
}
</div>

View File

@ -0,0 +1,82 @@
using System.Linq;
using System.Web.Mvc;
using HPlusSports.Models;
namespace HPlusSports.Controllers
{
public class ImagesController : Controller
{
private readonly HPlusSportsDbContext _context;
public ImagesController()
: this(new HPlusSportsDbContext())
{
}
public ImagesController(HPlusSportsDbContext context)
{
_context = context;
}
// /images/category/{SKU}
// Redirects to the main image for a category
public ActionResult Category(string id)
{
var imageId =
_context.Categories
.Where(x => x.Key == id)
.Select(x => x.ImageId)
.FirstOrDefault();
return Image(imageId);
}
// /images/product/{SKU}
// Redirects to the main image for a product
public ActionResult Product(string id)
{
var imageId =
_context.Products
.Where(x => x.SKU == id)
.SelectMany(x => x.Images.Select(img => img.Id))
.FirstOrDefault();
return Image(imageId);
}
public ActionResult Image(long? id)
{
var image = _context.Images.FirstOrDefault(x => x.Id == id);
if (!string.IsNullOrWhiteSpace(image.Url))
{
return RedirectPermanent(image.Url);
}
if (image?.Content == null)
{
image = GetPlaceholderImage();
}
return File(image.Content, image.ContentType);
}
internal static volatile byte[] PlaceholderImageContent;
private Image GetPlaceholderImage()
{
if (PlaceholderImageContent == null)
{
var path = Server.MapPath("~/Content/placeholder.jpg");
PlaceholderImageContent = System.IO.File.ReadAllBytes(path);
}
return new Image
{
Content = PlaceholderImageContent,
ContentType = "image/jpeg"
};
}
}
}

View File

@ -0,0 +1,85 @@
@model HPlusSports.Models.CreateProductRequest
@{
ViewBag.Title = "Add New Product";
}
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Product</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.CategoryId, "CategoryId", htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(model => model.CategoryId, null, htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.SKU, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.SKU, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.SKU, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Summary, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Summary, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Summary, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.MSRP, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.MSRP, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.MSRP, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace HPlusSports.Models
{
public class CreateProductRequest
{
[Required]
public long CategoryId { get; set; }
[Required]
public string SKU { get; set; }
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Text)]
public string Summary { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double MSRP { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double Price { get; set; }
}
}

View File

@ -0,0 +1,93 @@
@model IEnumerable<HPlusSports.Models.Product>
@{
ViewBag.Title = "Inventory";
}
@if (TempData.HasSuccessMessage())
{
<div class="alert alert-error alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@TempData.SuccessMessage()
</div>
}
else if (TempData.HasErrorMessage())
{
<div class="alert alert-error alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@TempData.ErrorMessage()
</div>
}
<h2>
@ViewBag.Title
<a class="btn btn-primary btn-sm pull-right" href="@Url.Action("Create")">Add Product</a>
</h2>
<table class="table tablesorter">
<thead>
<tr>
<th>
Category
</th>
<th>
@Html.DisplayNameFor(model => model.SKU)
</th>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th>
<th>
Last Update
</th>
<th disabled></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Category.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.SKU)
</td>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
<span class="clearfix text-muted">
@Html.DisplayFor(modelItem => item.MSRP)
</span>
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
<span class="text-muted">
<span title="@item.LastUpdated.ToString()">
@item.LastUpdated.ToShortDateString()
</span>
by
@item.LastUpdatedUserId
</span>
</td>
<td>
<a href="@Url.Action("Update", new { id = item.Id })">
<i class="glyphicon glyphicon-pencil text-info"></i>
</a>
<a href="@Url.Action("Delete", new { id = item.Id })">
<i class="glyphicon glyphicon-remove text-danger"></i>
</a>
</td>
</tr>
}
</tbody>
</table>

View File

@ -0,0 +1,142 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
using HPlusSports.Models;
using HPlusSports.Requests;
using HPlusSports.Services;
using MediatR;
namespace HPlusSports.Controllers
{
[Authorize(Roles = UserRoles.Admin)]
public class InventoryController : Controller
{
private HPlusSportsDbContext _context;
private readonly IMediator _mediator;
public InventoryController(
HPlusSportsDbContext context,
MediatR.IMediator mediator)
{
_context = context;
_mediator = mediator;
}
public ActionResult Index()
{
var products =
_context.Products
.OrderBy(x => x.CategoryId)
.ThenBy(x => x.Name);
return View(products);
}
[HttpGet]
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(CreateProductRequest request)
{
if (!ModelState.IsValid)
{
return View();
}
var product = new Product
{
CategoryId = request.CategoryId,
Description = request.Description,
MSRP = request.MSRP,
Name = request.Name,
Price = request.Price,
SKU = request.SKU,
Summary = request.Summary,
LastUpdated = DateTime.UtcNow,
LastUpdatedUserId = GetUserId(this),
};
_context.Products.Add(product);
_context.SaveChanges();
TempData.SuccessMessage($"Successfully created \"{product.Name}\"");
return RedirectToAction(nameof(Index));
}
[HttpGet]
public ActionResult Update(long id)
{
var existing = _context.Products.Find(id);
if (existing == null)
{
TempData.ErrorMessage($"Couldn't update product #\"{id}\": product not found!");
}
return View(existing);
}
[HttpPost]
public async Task<ActionResult> Update(UpdateProductRequest request)
{
if (!ModelState.IsValid)
{
return View();
}
request.LastUpdatedUserId = GetUserId(this);
var response = await _mediator.Send(request);
if (!response.Success)
{
TempData.ErrorMessage(response.Message);
return View();
}
TempData.SuccessMessage(response.Message);
return RedirectToAction(nameof(Index));
}
public ActionResult Delete(long id)
{
var product = _context.Products.Find(id);
if (product != null)
{
_context.Products.Remove(product);
_context.SaveChanges();
TempData.SuccessMessage($"Successfully deleted \"{product.Name}\"");
}
else
{
TempData.ErrorMessage($"Couldn't delete \"{product.Name}\": product not found!");
}
return RedirectToAction(nameof(Index));
}
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
ViewData["CategoryId"] =
_context.Categories
.Select(x => new SelectListItem
{
Text = x.Name,
Value = x.Id.ToString(),
})
.ToArray();
}
// Overwriteable function for unit testing
internal Func<Controller, string> GetUserId =
(controller) => controller.User.Identity.Name;
}
}

View File

@ -0,0 +1,104 @@
@model HPlusSports.Models.Product
@{
ViewBag.Title = "Update Product";
}
<h2>@ViewBag.Title</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Product</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.Id)
<div class="form-group">
@Html.LabelFor(model => model.CategoryId, "CategoryId", htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(model => model.CategoryId, null, htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.SKU, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.SKU, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.SKU, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Summary, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Summary, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Summary, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.MSRP, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.MSRP, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.MSRP, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.LastUpdated, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DisplayFor(model => model.LastUpdated, new { htmlAttributes = new { @class = "form-control" } })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.LastUpdatedUserId, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DisplayFor(model => model.LastUpdatedUserId, new { htmlAttributes = new { @class = "form-control" } })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -0,0 +1,61 @@
@model ShoppingCart
@{
var cart = Model;
}
<a class="dropbtn nav-cart">
<i class="glyphicon glyphicon-shopping-cart"></i>
Cart
@if (cart.Items.Any())
{
<span>
(@cart.Items.Count() items)
</span>
}
</a>
<div class="dropdown-content">
@if (cart.Items.Count == 0)
{
<p>Your shopping cart is empty - keep shopping!</p>
}
else
{
<table class="table table-condensed">
@foreach (var item in cart.Items)
{
<tr class="text-middle">
<td>
<a href="@Url.Action("Remove", "Cart", new { id = item.Id })">
<i class="glyphicon glyphicon-remove text-danger"></i>
</a>
</td>
<td>
<img width="50" height="50" src="@Url.Action("Product", "Images", new { id = item.SKU })" alt="">
</td>
<td>
<a href="@Url.Product(item.SKU)">
@item.Name
</a>
</td>
<td class="text-right">
@item.Quantity
x
@item.Price.ToString("C")
</td>
</tr>
}
<tr>
<th colspan="3" class="text-right">
Subtotal:
</th>
<th class="text-right">
@cart.Subtotal.ToString("C")
</th>
</tr>
</table>
}
<p class="buttons text-right">
<a href="@Url.Action("Index", "Cart")" class="btn btn-default">View Cart</a>
</p>
</div>

View File

@ -0,0 +1,68 @@
@model Product
@{
ViewBag.Title = "";
}
<div class="product row">
<div class="col-md-1">
@if (Model.Images.Count > 1)
{
<ul class="list-group">
@foreach (var image in Model.Images)
{
<li class="list-group-item">
<img class="img img-thumbnail"
src="@Url.Action("Image", "Images", new { id = image.Id })" />
</li>
}
</ul>
}
</div>
<div class="col-md-4">
<img class="img img-thumbnail"
src="@Url.Action("Product", "Images", new { id = Model.SKU })" />
</div>
<div class="col-md-6">
<h2>@Model.Name</h2>
<p>
@{
var rating = (HPlusSports.Models.ProductRating)ViewData["Rating"];
}
@Html.Rating(rating)
</p>
<p>
<span class="price">@Html.DisplayFor(modelItem => modelItem.Price)</span>
<span class="text-muted">@Html.DisplayFor(modelItem => modelItem.MSRP)</span>
@{
var discount = (Model.MSRP - Model.Price) / Model.MSRP;
}
@if (discount > .05)
{
<span class="discount label label-primary">
@discount.ToString("P0") off!
</span>
}
</p>
<p class="buy-button">
<a class="btn btn-sm btn-warning" href="@Url.Action("Add", "Cart", new { Model.SKU })">
Add To Cart
</a>
</p>
<hr />
<p>
@Html.DisplayFor(model => model.Summary)
</p>
</div>
</div>

View File

@ -0,0 +1,103 @@
@model ProductsListViewModel
@{
var category = (Category)ViewData["Category"];
ViewBag.Title = category.Name;
}
<div class="paged-content">
<div class="row">
@foreach (var product in Model.Products)
{
<div class="product-summary col-md-3 text-center">
<h4>
<a href="@Url.Product(product.SKU)">
<img class="product-image img-thumbnail" src="@Url.Action("Product", "Images", new { id = product.SKU })" />
<span class="name">@product.Name</span>
</a>
</h4>
<p>
<span class="price">@Html.DisplayFor(x => product.Price)</span>
<span class="text-muted">@Html.DisplayFor(x => product.MSRP)</span>
@if (product.HasAdvertisableDiscount)
{
<span class="discount label label-primary">
@Html.DisplayFor(x => product.Discount) off!
</span>
}
</p>
<p>
@Html.Rating(product.Rating)
</p>
<p>
<a class="btn btn-sm btn-warning" href="@Url.Action("Add", "Cart", new { product.SKU })">
Add To Cart
</a>
</p>
</div>
}
</div>
<div class="pager row">
<div class="pull-left">
<nav>
<ul class="pager">
<li>
<span>
@ViewBag.ResultsCount results
</span>
</li>
</ul>
</nav>
</div>
<div class="col-md-4 text-right">
@if (ViewBag.PreviousPage != null)
{
<nav>
<ul class="pager">
<li>
<a data-partial href="?page=@ViewBag.PreviousPage&count=@ViewBag.PageSize">
<i class="glyphicon glyphicon-chevron-left"></i>
Prev.
</a>
</li>
</ul>
</nav>
}
</div>
<div class="col-md-2 text-center">
<nav>
<ul class="pager">
<li>
<span>Page @ViewBag.CurrentPage of @ViewBag.PageCount</span>
</li>
</ul>
</nav>
</div>
<div class="col-md-5 text-left">
@if (ViewBag.NextPage != null)
{
<nav>
<ul class="pager">
<li>
<a data-partial href="?page=@ViewBag.NextPage&count=@ViewBag.PageSize">
Next
<i class="glyphicon glyphicon-chevron-right"></i>
</a>
</li>
</ul>
</nav>
}
</div>
</div>
</div>

View File

@ -0,0 +1,118 @@
using System.Linq;
using System.Web.Mvc;
using HPlusSports.Extensions;
using HPlusSports.Models;
namespace HPlusSports.Controllers
{
public class ProductsController : Controller
{
public const int DefaultPageSize = 8;
private readonly HPlusSportsDbContext _context;
public ProductsController(HPlusSportsDbContext context)
{
_context = context;
}
public ActionResult Index(int? page = null, int? count = null)
{
return Category(null, page, count);
}
[AllowPartialRendering]
public ActionResult Category(string id, int? page = null, int? count = null)
{
var category = _context.Categories.FirstOrDefault(x => x.Key == id);
IQueryable<Product> products = _context.Products;
if (category == null)
{
products =
products
.OrderBy(x => x.CategoryId)
.ThenBy(x => x.Name);
}
else
{
products =
products
.Where(x => x.CategoryId == category.Id)
.OrderBy(x => x.Name);
}
var skus = products.Select(x => x.SKU);
var ratings = _context.GetProductRatings(skus);
ViewData["Ratings"] = ratings;
ViewData["Category"] = category ?? new Category { Name = "All Categories" };
/**** Paging Logic ****/
var model = products;
var resultsCount = model.Count();
var pageSize = count.GetValueOrDefault(DefaultPageSize);
var currentPage = page.GetValueOrDefault(1);
var pageCount = resultsCount / pageSize + (resultsCount % pageSize > 0 ? 1 : 0);
var previousPage = (currentPage - 1 > 0) ? currentPage - 1 : (int?)null;
var nextPage = (currentPage + 1 <= pageCount) ? currentPage + 1 : (int?)null;
model = model
.Skip((currentPage - 1) * pageSize)
.Take(pageSize);
ViewData["PageSize"] = pageSize;
ViewData["ResultsCount"] = resultsCount;
ViewData["CurrentPage"] = currentPage;
ViewData["PageCount"] = pageCount;
ViewData["PreviousPage"] = previousPage;
ViewData["NextPage"] = nextPage;
/**** End Paging Logic ****/
var vm = new ProductsListViewModel
{
Products = products.Select(x => new ProductViewModel {
MSRP = x.MSRP,
Name = x.Name,
Price = x.Price,
SKU = x.SKU,
Rating = ratings.FirstOrDefault(y => x.SKU == y.SKU),
})
};
return View("ProductList", vm);
}
public ActionResult Product(string id)
{
var product = _context.FindProductBySku(id);
if (product == null)
return HttpNotFound();
var imageIds =
product
.Images
.Select(img => img.Id)
.ToArray();
ViewData["Rating"] = _context.GetProductRating(product.SKU);
ViewData["Category"] = product.Category;
ViewData["Images"] = imageIds;
return View(product);
}
[ChildActionOnly]
public ActionResult MenuCart()
{
var userId = User.Identity.Name;
var cart = _context.ShoppingCarts.FirstOrDefault(x => x.UserId == userId)
?? new ShoppingCart { UserId = userId };
return PartialView("MenuCart", cart);
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace HPlusSports.Models
{
public class ProductsListViewModel
{
public IEnumerable<ProductViewModel> Products { get; set; }
}
public class ProductViewModel
{
public const double MinimumAdvertisableDiscount = 0.05;
public string SKU { get; set; }
public string Name { get; set; }
[DataType(DataType.Currency)]
public double MSRP { get; set; }
[DataType(DataType.Currency)]
public double Price { get; set; }
[DisplayFormat(DataFormatString = "{0:P0}")]
public double? Discount
{
get { return (MSRP - Price) / MSRP; }
}
public bool HasAdvertisableDiscount
{
get { return Discount > MinimumAdvertisableDiscount; }
}
public ProductRating Rating { get; set; }
}
}

View File

@ -0,0 +1,10 @@
@model ProductRating
<span title="@Model.Rating.GetValueOrDefault().ToString("N1")">
@for (var i = 1; i <= 5; i++)
{
var starClass = (Model.Rating >= i) ? "star" : "star-empty";
<span class="rating-star glyphicon glyphicon-@starClass"></span>
}
</span>
<span>(@Model.ReviewCount)</span>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Error</title>
</head>
<body>
<hgroup>
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
</hgroup>
</body>
</html>

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - H+ Sport</title>
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
<link rel="icon" href="https://hplussport.com/wp-content/uploads/2015/12/cropped-HSport_01-32x32.png" sizes="32x32">
</head>
<body>
<div class="navbar">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="~/" class="navbar-brand">
<img src="~/Content/logo.png" />
</a>
</div>
<div class="col-md-6 text-center title navbar-header">
<h1>@ViewData["Title"]</h1>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav pull-right">
<li class="dropdown">
@if (User.IsInRole(HPlusSports.Models.UserRoles.Admin))
{
<a class="dropbtn" href="@Url.Action("Index", "Inventory")">
<i class="glyphicon glyphicon-list-alt"></i>
Inventory
</a>
}
</li>
<li class="dropdown">
@Html.Action("MenuCart", "Products")
</li>
<li>
@if (User.Identity.IsAuthenticated)
{
<form method="post" action="@Url.Action("Logoff", "Account", new { area = "" })">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-default">Logout</button>
</form>
}
else
{
<div>
<a href="@Url.Action("Login", "Account")" class="btn btn-default">Login</a>
</div>
}
</li>
</ul>
</div>
</div>
</div>
<div id="main" class="container">
@RenderBody()
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.30.1/js/jquery.tablesorter.min.js"></script>
@Scripts.Render("~/bundles/site")
@RenderSection("scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,44 @@
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
<add namespace="HPlusSports"/>
<add namespace="HPlusSports.Models"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.webServer>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
<system.web>
<compilation>
<assemblies>
<add assembly="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
</system.web>
</configuration>

View File

@ -0,0 +1,3 @@
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}