commit 5347fe22d3c2b634a4dcfe11725732ae25b0ccd5 Author: Tommy Parnell Date: Sat Oct 17 23:06:36 2015 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..873b40b --- /dev/null +++ b/.gitignore @@ -0,0 +1,230 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Azure Emulator +efc/ +rfc/ + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..95143bd --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..f4398b0 --- /dev/null +++ b/Readme.md @@ -0,0 +1,15 @@ +Add [turbolink](https://github.com/rails/turbolinks) support to ASP.NET 5 applications + +This provides middlewear to add support for turbolinks. Simply add `app.UseTurboLinks();` and a build of turbolinks (one can be found in the wwwroot/js dir of the example project) + + +## Why use turbolinks? + +If you have an application that may not fit into an SPA, or just have a lot of code that is tied to .NET this provides SPA like speed by ajaxing the html and replacing it. This allows the browser to keep the cache of existing scripts. Turbolinks was made in the rails community, and a lot of existing documentation already exists. + + +**Warning** This stops page loads thus `$(document).ready(function(){})` does not fire on new pages. + +## What about asp.net 4x? + +I am going to back port this to owin, and eventually more classic version of ASP.NET In the mean time, you can find an MVC action filter [here](https://github.com/kazimanzurrashid/aspnetmvcturbolinks) \ No newline at end of file diff --git a/TurboLinks.Net.sln b/TurboLinks.Net.sln new file mode 100644 index 0000000..231a030 --- /dev/null +++ b/TurboLinks.Net.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{505F6C3D-2B40-442B-ABBB-69275B465571}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{15647589-E22B-47B8-B149-6693C35A09F2}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + NuGet.Config = NuGet.Config + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TurboLinks.Net", "src\TurboLinks.Net\TurboLinks.Net.xproj", "{B57EEE3B-37FF-49B5-8841-84B759DF1471}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TurboLinks.Net.Example", "src\TurboLinks.Net.Example\TurboLinks.Net.Example.xproj", "{B2ADC3D3-2EE1-4662-9130-FC2DFAFE2BBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B57EEE3B-37FF-49B5-8841-84B759DF1471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B57EEE3B-37FF-49B5-8841-84B759DF1471}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B57EEE3B-37FF-49B5-8841-84B759DF1471}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B57EEE3B-37FF-49B5-8841-84B759DF1471}.Release|Any CPU.Build.0 = Release|Any CPU + {B2ADC3D3-2EE1-4662-9130-FC2DFAFE2BBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2ADC3D3-2EE1-4662-9130-FC2DFAFE2BBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2ADC3D3-2EE1-4662-9130-FC2DFAFE2BBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2ADC3D3-2EE1-4662-9130-FC2DFAFE2BBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B57EEE3B-37FF-49B5-8841-84B759DF1471} = {505F6C3D-2B40-442B-ABBB-69275B465571} + {B2ADC3D3-2EE1-4662-9130-FC2DFAFE2BBF} = {505F6C3D-2B40-442B-ABBB-69275B465571} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..e9536a8 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-beta8" + } +} diff --git a/src/TurboLinks.Net.Example/.bowerrc b/src/TurboLinks.Net.Example/.bowerrc new file mode 100644 index 0000000..6406626 --- /dev/null +++ b/src/TurboLinks.Net.Example/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "wwwroot/lib" +} diff --git a/src/TurboLinks.Net.Example/Controllers/AccountController.cs b/src/TurboLinks.Net.Example/Controllers/AccountController.cs new file mode 100644 index 0000000..7755bb7 --- /dev/null +++ b/src/TurboLinks.Net.Example/Controllers/AccountController.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.Data.Entity; +using TurboLinks.Net.Example.Models; +using TurboLinks.Net.Example.Services; +using TurboLinks.Net.Example.ViewModels.Account; + +namespace TurboLinks.Net.Example.Controllers +{ + [Authorize] + public class AccountController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ISmsSender _smsSender; + private readonly ApplicationDbContext _applicationDbContext; + private static bool _databaseChecked; + + public AccountController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ISmsSender smsSender, + ApplicationDbContext applicationDbContext) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _smsSender = smsSender; + _applicationDbContext = applicationDbContext; + } + + // + // GET: /Account/Login + [HttpGet] + [AllowAnonymous] + public IActionResult Login(string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + // + // POST: /Account/Login + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model, string returnUrl = null) + { + EnsureDatabaseCreated(_applicationDbContext); + ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/Register + [HttpGet] + [AllowAnonymous] + public IActionResult Register() + { + return View(); + } + + // + // POST: /Account/Register + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model) + { + EnsureDatabaseCreated(_applicationDbContext); + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + //var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); + //await _emailSender.SendEmailAsync(model.Email, "Confirm your account", + // "Please confirm your account by clicking this link: link"); + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + AddErrors(result); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // POST: /Account/LogOff + [HttpPost] + [ValidateAntiForgeryToken] + public async Task LogOff() + { + await _signInManager.SignOutAsync(); + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + + // + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public IActionResult ExternalLogin(string provider, string returnUrl = null) + { + EnsureDatabaseCreated(_applicationDbContext); + // Request a redirect to the external login provider. + var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return new ChallengeResult(provider, properties); + } + + // + // GET: /Account/ExternalLoginCallback + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl = null) + { + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return RedirectToAction(nameof(Login)); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); + if (result.Succeeded) + { + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl }); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ViewData["ReturnUrl"] = returnUrl; + ViewData["LoginProvider"] = info.LoginProvider; + var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email); + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); + } + } + + // + // POST: /Account/ExternalLoginConfirmation + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null) + { + if (User.IsSignedIn()) + { + return RedirectToAction(nameof(ManageController.Index),"Manage"); + } + + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await _signInManager.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, info); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } + + ViewData["ReturnUrl"] = returnUrl; + return View(model); + } + + // GET: /Account/ConfirmEmail + [HttpGet] + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return View("Error"); + } + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return View("Error"); + } + var result = await _userManager.ConfirmEmailAsync(user, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + // + // GET: /Account/ForgotPassword + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPassword() + { + return View(); + } + + // + // POST: /Account/ForgotPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ForgotPassword(ForgotPasswordViewModel model) + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByNameAsync(model.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // 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 http://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + //var code = await _userManager.GeneratePasswordResetTokenAsync(user); + //var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); + //await _emailSender.SendEmailAsync(model.Email, "Reset Password", + // "Please reset your password by clicking here: link"); + //return View("ForgotPasswordConfirmation"); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/ForgotPasswordConfirmation + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/ResetPassword + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPassword(string code = null) + { + return code == null ? View("Error") : View(); + } + + // + // POST: /Account/ResetPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task 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(nameof(AccountController.ResetPasswordConfirmation), "Account"); + } + var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account"); + } + AddErrors(result); + return View(); + } + + // + // GET: /Account/ResetPasswordConfirmation + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/SendCode + [HttpGet] + [AllowAnonymous] + public async Task SendCode(string returnUrl = null, bool rememberMe = false) + { + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); + 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 SendCode(SendCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + + // Generate the token and send it + var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider); + if (string.IsNullOrWhiteSpace(code)) + { + return View("Error"); + } + + var message = "Your security code is: " + code; + if (model.SelectedProvider == "Email") + { + await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message); + } + else if (model.SelectedProvider == "Phone") + { + await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message); + } + + return RedirectToAction(nameof(VerifyCode), new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); + } + + // + // GET: /Account/VerifyCode + [HttpGet] + [AllowAnonymous] + public async Task VerifyCode(string provider, bool rememberMe, string returnUrl = null) + { + // Require that the user has already logged in via username/password or external login + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/VerifyCode + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task 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. + var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); + if (result.Succeeded) + { + return RedirectToLocal(model.ReturnUrl); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + ModelState.AddModelError("", "Invalid code."); + return View(model); + } + } + + #region Helpers + + // The following code creates the database and schema if they don't exist. + // This is a temporary workaround since deploying database through EF migrations is + // not yet supported in this release. + // Please see this http://go.microsoft.com/fwlink/?LinkID=615859 for more information on how to do deploy the database + // when publishing your application. + private static void EnsureDatabaseCreated(ApplicationDbContext context) + { + if (!_databaseChecked) + { + _databaseChecked = true; + context.Database.Migrate(); + } + } + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private async Task GetCurrentUserAsync() + { + return await _userManager.FindByIdAsync(HttpContext.User.GetUserId()); + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + } + + #endregion + } +} diff --git a/src/TurboLinks.Net.Example/Controllers/HomeController.cs b/src/TurboLinks.Net.Example/Controllers/HomeController.cs new file mode 100644 index 0000000..fb6783f --- /dev/null +++ b/src/TurboLinks.Net.Example/Controllers/HomeController.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace TurboLinks.Net.Example.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + + public void About() + { + Response.Redirect("http://about.tommyparnell.com"); + } + + public IActionResult Contact() + { + ViewData["Message"] = "Your contact page."; + + return View(); + } + + public IActionResult Error() + { + return View("~/Views/Shared/Error.cshtml"); + } + } +} \ No newline at end of file diff --git a/src/TurboLinks.Net.Example/Controllers/ManageController.cs b/src/TurboLinks.Net.Example/Controllers/ManageController.cs new file mode 100644 index 0000000..ccd4d2d --- /dev/null +++ b/src/TurboLinks.Net.Example/Controllers/ManageController.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Security.Claims; +using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Mvc; +using TurboLinks.Net.Example.Models; +using TurboLinks.Net.Example.Services; +using TurboLinks.Net.Example.ViewModels.Manage; + +namespace TurboLinks.Net.Example.Controllers +{ + [Authorize] + public class ManageController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ISmsSender _smsSender; + + public ManageController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ISmsSender smsSender) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _smsSender = smsSender; + } + + // + // GET: /Account/Index + [HttpGet] + public async Task Index(ManageMessageId? message = null) + { + ViewData["StatusMessage"] = + message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." + : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." + : message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set." + : message == ManageMessageId.Error ? "An error has occurred." + : message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added." + : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." + : ""; + + var user = await GetCurrentUserAsync(); + var model = new IndexViewModel + { + HasPassword = await _userManager.HasPasswordAsync(user), + PhoneNumber = await _userManager.GetPhoneNumberAsync(user), + TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user), + Logins = await _userManager.GetLoginsAsync(user), + BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user) + }; + return View(model); + } + + // + // GET: /Account/RemoveLogin + [HttpGet] + public async Task RemoveLogin() + { + var user = await GetCurrentUserAsync(); + var linkedAccounts = await _userManager.GetLoginsAsync(user); + ViewData["ShowRemoveButton"] = await _userManager.HasPasswordAsync(user) || linkedAccounts.Count > 1; + return View(linkedAccounts); + } + + // + // POST: /Manage/RemoveLogin + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemoveLogin(string loginProvider, string providerKey) + { + ManageMessageId? message = ManageMessageId.Error; + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + message = ManageMessageId.RemoveLoginSuccess; + } + } + return RedirectToAction(nameof(ManageLogins), new { Message = message }); + } + + // + // GET: /Account/AddPhoneNumber + public IActionResult AddPhoneNumber() + { + return View(); + } + + // + // POST: /Account/AddPhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task AddPhoneNumber(AddPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + // Generate the token and send it + var user = await GetCurrentUserAsync(); + var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, model.PhoneNumber); + await _smsSender.SendSmsAsync(model.PhoneNumber, "Your security code is: " + code); + return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber }); + } + + // + // POST: /Manage/EnableTwoFactorAuthentication + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.SetTwoFactorEnabledAsync(user, true); + await _signInManager.SignInAsync(user, isPersistent: false); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // POST: /Manage/DisableTwoFactorAuthentication + [HttpPost] + [ValidateAntiForgeryToken] + public async Task DisableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _signInManager.SignInAsync(user, isPersistent: false); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // GET: /Account/VerifyPhoneNumber + [HttpGet] + public async Task VerifyPhoneNumber(string phoneNumber) + { + var code = await _userManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), phoneNumber); + // Send an SMS to verify the phone number + return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); + } + + // + // POST: /Account/VerifyPhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task VerifyPhoneNumber(VerifyPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.AddPhoneSuccess }); + } + } + // If we got this far, something failed, redisplay the form + ModelState.AddModelError(string.Empty, "Failed to verify phone number"); + return View(model); + } + + // + // GET: /Account/RemovePhoneNumber + [HttpGet] + public async Task RemovePhoneNumber() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.SetPhoneNumberAsync(user, null); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess }); + } + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/ChangePassword + [HttpGet] + public IActionResult ChangePassword() + { + return View(); + } + + // + // POST: /Account/Manage + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/SetPassword + [HttpGet] + public IActionResult SetPassword() + { + return View(); + } + + // + // POST: /Manage/SetPassword + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SetPassword(SetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.AddPasswordAsync(user, model.NewPassword); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetPasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + //GET: /Account/Manage + [HttpGet] + public async Task ManageLogins(ManageMessageId? message = null) + { + ViewData["StatusMessage"] = + message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + : message == ManageMessageId.AddLoginSuccess ? "The external login was added." + : message == ManageMessageId.Error ? "An error has occurred." + : ""; + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var userLogins = await _userManager.GetLoginsAsync(user); + var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList(); + ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; + return View(new ManageLoginsViewModel + { + CurrentLogins = userLogins, + OtherLogins = otherLogins + }); + } + + // + // POST: /Manage/LinkLogin + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult LinkLogin(string provider) + { + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Action("LinkLoginCallback", "Manage"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, User.GetUserId()); + return new ChallengeResult(provider, properties); + } + + // + // GET: /Manage/LinkLoginCallback + [HttpGet] + public async Task LinkLoginCallback() + { + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var info = await _signInManager.GetExternalLoginInfoAsync(User.GetUserId()); + if (info == null) + { + return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); + } + var result = await _userManager.AddLoginAsync(user, info); + var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error; + return RedirectToAction(nameof(ManageLogins), new { Message = message }); + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private async Task HasPhoneNumber() + { + var user = await _userManager.FindByIdAsync(User.GetUserId()); + if (user != null) + { + return user.PhoneNumber != null; + } + return false; + } + + public enum ManageMessageId + { + AddPhoneSuccess, + AddLoginSuccess, + ChangePasswordSuccess, + SetTwoFactorSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + RemovePhoneSuccess, + Error + } + + private async Task GetCurrentUserAsync() + { + return await _userManager.FindByIdAsync(HttpContext.User.GetUserId()); + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return RedirectToAction(nameof(HomeController.Index), nameof(HomeController)); + } + } + + #endregion + } +} diff --git a/src/TurboLinks.Net.Example/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/TurboLinks.Net.Example/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..efb739c --- /dev/null +++ b/src/TurboLinks.Net.Example/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Infrastructure; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Data.Entity.Migrations; +using TurboLinks.Net.Example.Models; + +namespace TurboLinks.Net.Example.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .Annotation("ProductVersion", "7.0.0-beta8") + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .Annotation("MaxLength", 256); + + b.Property("NormalizedName") + .Annotation("MaxLength", 256); + + b.HasKey("Id"); + + b.Index("NormalizedName") + .Annotation("Relational:Name", "RoleNameIndex"); + + b.Annotation("Relational:TableName", "AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.Annotation("Relational:TableName", "AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.Annotation("Relational:TableName", "AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.Annotation("Relational:TableName", "AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.Annotation("Relational:TableName", "AspNetUserRoles"); + }); + + modelBuilder.Entity("TurboLinks.Net.Example.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .Annotation("MaxLength", 256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .Annotation("MaxLength", 256); + + b.Property("NormalizedUserName") + .Annotation("MaxLength", 256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .Annotation("MaxLength", 256); + + b.HasKey("Id"); + + b.Index("NormalizedEmail") + .Annotation("Relational:Name", "EmailIndex"); + + b.Index("NormalizedUserName") + .Annotation("Relational:Name", "UserNameIndex"); + + b.Annotation("Relational:TableName", "AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNet.Identity.EntityFramework.IdentityRole") + .WithMany() + .ForeignKey("RoleId"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim", b => + { + b.HasOne("TurboLinks.Net.Example.Models.ApplicationUser") + .WithMany() + .ForeignKey("UserId"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin", b => + { + b.HasOne("TurboLinks.Net.Example.Models.ApplicationUser") + .WithMany() + .ForeignKey("UserId"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNet.Identity.EntityFramework.IdentityRole") + .WithMany() + .ForeignKey("RoleId"); + + b.HasOne("TurboLinks.Net.Example.Models.ApplicationUser") + .WithMany() + .ForeignKey("UserId"); + }); + } + } +} diff --git a/src/TurboLinks.Net.Example/Migrations/00000000000000_CreateIdentitySchema.cs b/src/TurboLinks.Net.Example/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..7ec6d89 --- /dev/null +++ b/src/TurboLinks.Net.Example/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Data.Entity.Migrations; + +namespace TurboLinks.Net.Example.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(nullable: true), + NormalizedName = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityRole", x => x.Id); + }); + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(nullable: true), + EmailConfirmed = table.Column(nullable: false), + LockoutEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + NormalizedEmail = table.Column(nullable: true), + NormalizedUserName = table.Column(nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + SecurityStamp = table.Column(nullable: true), + TwoFactorEnabled = table.Column(nullable: false), + UserName = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUser", x => x.Id); + }); + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityRoleClaim", x => x.Id); + table.ForeignKey( + name: "FK_IdentityRoleClaim_IdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityUserClaim", x => x.Id); + table.ForeignKey( + name: "FK_IdentityUserClaim_ApplicationUser_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityUserLogin", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_IdentityUserLogin_ApplicationUser_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityUserRole", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_IdentityUserRole_IdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_IdentityUserRole_ApplicationUser_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName"); + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable("AspNetRoleClaims"); + migrationBuilder.DropTable("AspNetUserClaims"); + migrationBuilder.DropTable("AspNetUserLogins"); + migrationBuilder.DropTable("AspNetUserRoles"); + migrationBuilder.DropTable("AspNetRoles"); + migrationBuilder.DropTable("AspNetUsers"); + } + } +} diff --git a/src/TurboLinks.Net.Example/Migrations/ApplicationDbContextModelSnapshot.cs b/src/TurboLinks.Net.Example/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..3a03042 --- /dev/null +++ b/src/TurboLinks.Net.Example/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Infrastructure; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Data.Entity.Migrations; +using TurboLinks.Net.Example.Models; + +namespace TurboLinks.Net.Example.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .Annotation("ProductVersion", "7.0.0-beta8") + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .Annotation("MaxLength", 256); + + b.Property("NormalizedName") + .Annotation("MaxLength", 256); + + b.HasKey("Id"); + + b.Index("NormalizedName") + .Annotation("Relational:Name", "RoleNameIndex"); + + b.Annotation("Relational:TableName", "AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.Annotation("Relational:TableName", "AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.Annotation("Relational:TableName", "AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.Annotation("Relational:TableName", "AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.Annotation("Relational:TableName", "AspNetUserRoles"); + }); + + modelBuilder.Entity("TurboLinks.Net.Example.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .Annotation("MaxLength", 256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .Annotation("MaxLength", 256); + + b.Property("NormalizedUserName") + .Annotation("MaxLength", 256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .Annotation("MaxLength", 256); + + b.HasKey("Id"); + + b.Index("NormalizedEmail") + .Annotation("Relational:Name", "EmailIndex"); + + b.Index("NormalizedUserName") + .Annotation("Relational:Name", "UserNameIndex"); + + b.Annotation("Relational:TableName", "AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNet.Identity.EntityFramework.IdentityRole") + .WithMany() + .ForeignKey("RoleId"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim", b => + { + b.HasOne("TurboLinks.Net.Example.Models.ApplicationUser") + .WithMany() + .ForeignKey("UserId"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin", b => + { + b.HasOne("TurboLinks.Net.Example.Models.ApplicationUser") + .WithMany() + .ForeignKey("UserId"); + }); + + modelBuilder.Entity("Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNet.Identity.EntityFramework.IdentityRole") + .WithMany() + .ForeignKey("RoleId"); + + b.HasOne("TurboLinks.Net.Example.Models.ApplicationUser") + .WithMany() + .ForeignKey("UserId"); + }); + } + } +} diff --git a/src/TurboLinks.Net.Example/Models/ApplicationDbContext.cs b/src/TurboLinks.Net.Example/Models/ApplicationDbContext.cs new file mode 100644 index 0000000..cd1dec7 --- /dev/null +++ b/src/TurboLinks.Net.Example/Models/ApplicationDbContext.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.Data.Entity; + +namespace TurboLinks.Net.Example.Models +{ + public class ApplicationDbContext : IdentityDbContext + { + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + // 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); + } + } +} diff --git a/src/TurboLinks.Net.Example/Models/ApplicationUser.cs b/src/TurboLinks.Net.Example/Models/ApplicationUser.cs new file mode 100644 index 0000000..808ed04 --- /dev/null +++ b/src/TurboLinks.Net.Example/Models/ApplicationUser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity.EntityFramework; + +namespace TurboLinks.Net.Example.Models +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + } +} diff --git a/src/TurboLinks.Net.Example/Properties/AssemblyInfo.cs b/src/TurboLinks.Net.Example/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9d01790 --- /dev/null +++ b/src/TurboLinks.Net.Example/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TurboLinks.Net.Example")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TurboLinks.Net.Example")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b2adc3d3-2ee1-4662-9130-fc2dfafe2bbf")] diff --git a/src/TurboLinks.Net.Example/Services/IEmailSender.cs b/src/TurboLinks.Net.Example/Services/IEmailSender.cs new file mode 100644 index 0000000..c2981e1 --- /dev/null +++ b/src/TurboLinks.Net.Example/Services/IEmailSender.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.Services +{ + public interface IEmailSender + { + Task SendEmailAsync(string email, string subject, string message); + } +} diff --git a/src/TurboLinks.Net.Example/Services/ISmsSender.cs b/src/TurboLinks.Net.Example/Services/ISmsSender.cs new file mode 100644 index 0000000..0b6ac6c --- /dev/null +++ b/src/TurboLinks.Net.Example/Services/ISmsSender.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.Services +{ + public interface ISmsSender + { + Task SendSmsAsync(string number, string message); + } +} diff --git a/src/TurboLinks.Net.Example/Services/MessageServices.cs b/src/TurboLinks.Net.Example/Services/MessageServices.cs new file mode 100644 index 0000000..46d7e9f --- /dev/null +++ b/src/TurboLinks.Net.Example/Services/MessageServices.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.Services +{ + // This class is used by the application to send Email and SMS + // when you turn on two-factor authentication in ASP.NET Identity. + // For more details see this link http://go.microsoft.com/fwlink/?LinkID=532713 + public class AuthMessageSender : IEmailSender, ISmsSender + { + public Task SendEmailAsync(string email, string subject, string message) + { + // Plug in your email service here to send an email. + return Task.FromResult(0); + } + + public Task SendSmsAsync(string number, string message) + { + // Plug in your SMS service here to send a text message. + return Task.FromResult(0); + } + } +} diff --git a/src/TurboLinks.Net.Example/Startup.cs b/src/TurboLinks.Net.Example/Startup.cs new file mode 100644 index 0000000..2c7a341 --- /dev/null +++ b/src/TurboLinks.Net.Example/Startup.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Facebook; +using Microsoft.AspNet.Authentication.Google; +using Microsoft.AspNet.Authentication.MicrosoftAccount; +using Microsoft.AspNet.Authentication.Twitter; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics.Entity; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.Data.Entity; +using Microsoft.Dnx.Runtime; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using TurboLinks.Net.Example.Models; +using TurboLinks.Net.Example.Services; + +namespace TurboLinks.Net.Example +{ + public class Startup + { + public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) + { + // Setup configuration sources. + + var builder = new ConfigurationBuilder() + .SetBasePath(appEnv.ApplicationBasePath) + .AddJsonFile("appsettings.json") + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + + if(env.IsDevelopment()) + { + // This reads the configuration keys from the secret store. + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; set; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add Entity Framework services to the services container. + services.AddEntityFramework() + .AddSqlServer() + .AddDbContext(options => + options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"])); + + // Add Identity services to the services container. + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Add MVC services to the services container. + services.AddMvc(); + + // Uncomment the following line to add Web API services which makes it easier to port Web API 2 controllers. + // You will also need to add the Microsoft.AspNet.Mvc.WebApiCompatShim package to the 'dependencies' section of project.json. + // services.AddWebApiConventions(); + + // Register application services. + services.AddTransient(); + services.AddTransient(); + } + + // Configure is called after ConfigureServices is called. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.AddConsole(); + loggerFactory.AddDebug(); + + // Configure the HTTP request pipeline. + + // Add the following to the request pipeline only in development environment. + if(env.IsDevelopment()) + { + app.UseBrowserLink(); + app.UseDeveloperExceptionPage(); + app.UseDatabaseErrorPage(DatabaseErrorPageOptions.ShowAll); + } + else + { + // Add Error handling middleware which catches all application specific errors and + // sends the request to the following path or controller action. + app.UseExceptionHandler("/Home/Error"); + } + + // Add the platform handler to the request pipeline. + app.UseIISPlatformHandler(); + + // Add static files to the request pipeline. + app.UseStaticFiles(); + app.UseTurboLinks(); + + // Add cookie-based authentication to the request pipeline. + app.UseIdentity(); + + // Add and configure the options for authentication middleware to the request pipeline. + // You can add options for middleware as shown below. + // For more information see http://go.microsoft.com/fwlink/?LinkID=532715 + //app.UseFacebookAuthentication(options => + //{ + // options.AppId = Configuration["Authentication:Facebook:AppId"]; + // options.AppSecret = Configuration["Authentication:Facebook:AppSecret"]; + //}); + //app.UseGoogleAuthentication(options => + //{ + // options.ClientId = Configuration["Authentication:Google:ClientId"]; + // options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; + //}); + //app.UseMicrosoftAccountAuthentication(options => + //{ + // options.ClientId = Configuration["Authentication:MicrosoftAccount:ClientId"]; + // options.ClientSecret = Configuration["Authentication:MicrosoftAccount:ClientSecret"]; + //}); + //app.UseTwitterAuthentication(options => + //{ + // options.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"]; + // options.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"]; + //}); + + // Add MVC to the request pipeline. + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + // Uncomment the following line to add a route for porting Web API 2 controllers. + // routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"); + }); + } + } +} \ No newline at end of file diff --git a/src/TurboLinks.Net.Example/TurboLinks.Net.Example.xproj b/src/TurboLinks.Net.Example/TurboLinks.Net.Example.xproj new file mode 100644 index 0000000..5170e2d --- /dev/null +++ b/src/TurboLinks.Net.Example/TurboLinks.Net.Example.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b2adc3d3-2ee1-4662-9130-fc2dfafe2bbf + TurboLinks.Net.Example + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 59553 + + + \ No newline at end of file diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/ExternalLoginConfirmationViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/ExternalLoginConfirmationViewModel.cs new file mode 100644 index 0000000..ae5c677 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/ExternalLoginConfirmationViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/ForgotPasswordViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/ForgotPasswordViewModel.cs new file mode 100644 index 0000000..30d1419 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/ForgotPasswordViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + public class ForgotPasswordViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/LoginViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/LoginViewModel.cs new file mode 100644 index 0000000..f3ed278 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/LoginViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + public class LoginViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/RegisterViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/RegisterViewModel.cs new file mode 100644 index 0000000..355514e --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/RegisterViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + 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; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/ResetPasswordViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/ResetPasswordViewModel.cs new file mode 100644 index 0000000..b8d598d --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/ResetPasswordViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + public class ResetPasswordViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} 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; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/SendCodeViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/SendCodeViewModel.cs new file mode 100644 index 0000000..f44e484 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/SendCodeViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + public class SendCodeViewModel + { + public string SelectedProvider { get; set; } + + public ICollection Providers { get; set; } + + public string ReturnUrl { get; set; } + + public bool RememberMe { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Account/VerifyCodeViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Account/VerifyCodeViewModel.cs new file mode 100644 index 0000000..3960d6f --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Account/VerifyCodeViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Account +{ + public class VerifyCodeViewModel + { + [Required] + public string Provider { get; set; } + + [Required] + public string Code { get; set; } + + public string ReturnUrl { get; set; } + + [Display(Name = "Remember this browser?")] + public bool RememberBrowser { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/AddPhoneNumberViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/AddPhoneNumberViewModel.cs new file mode 100644 index 0000000..faf91f7 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/AddPhoneNumberViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + public class AddPhoneNumberViewModel + { + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/ChangePasswordViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/ChangePasswordViewModel.cs new file mode 100644 index 0000000..c9c2f02 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/ChangePasswordViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + 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; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/ConfigureTwoFactorViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/ConfigureTwoFactorViewModel.cs new file mode 100644 index 0000000..6f0b4ba --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/ConfigureTwoFactorViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + public class ConfigureTwoFactorViewModel + { + public string SelectedProvider { get; set; } + + public ICollection Providers { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/FactorViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/FactorViewModel.cs new file mode 100644 index 0000000..c14b3f5 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/FactorViewModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + public class FactorViewModel + { + public string Purpose { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/IndexViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/IndexViewModel.cs new file mode 100644 index 0000000..96ac0b4 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/IndexViewModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + public class IndexViewModel + { + public bool HasPassword { get; set; } + + public IList Logins { get; set; } + + public string PhoneNumber { get; set; } + + public bool TwoFactor { get; set; } + + public bool BrowserRemembered { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/ManageLoginsViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/ManageLoginsViewModel.cs new file mode 100644 index 0000000..95799e6 --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/ManageLoginsViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Identity; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + public class ManageLoginsViewModel + { + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/SetPasswordViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/SetPasswordViewModel.cs new file mode 100644 index 0000000..f93a86d --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/SetPasswordViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + 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; } + } +} diff --git a/src/TurboLinks.Net.Example/ViewModels/Manage/VerifyPhoneNumberViewModel.cs b/src/TurboLinks.Net.Example/ViewModels/Manage/VerifyPhoneNumberViewModel.cs new file mode 100644 index 0000000..308044e --- /dev/null +++ b/src/TurboLinks.Net.Example/ViewModels/Manage/VerifyPhoneNumberViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TurboLinks.Net.Example.ViewModels.Manage +{ + public class VerifyPhoneNumberViewModel + { + [Required] + public string Code { get; set; } + + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/ConfirmEmail.cshtml b/src/TurboLinks.Net.Example/Views/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..3244fef --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ConfirmEmail.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "Confirm Email"; +} + +

@ViewData["Title"].

+
+

+ Thank you for confirming your email. Please Click here to Log in. +

+
diff --git a/src/TurboLinks.Net.Example/Views/Account/ExternalLoginConfirmation.cshtml b/src/TurboLinks.Net.Example/Views/Account/ExternalLoginConfirmation.cshtml new file mode 100644 index 0000000..59f9ba8 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ExternalLoginConfirmation.cshtml @@ -0,0 +1,35 @@ +@model ExternalLoginConfirmationViewModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"].

+

Associate your @ViewData["LoginProvider"] account.

+ +
+

Association Form

+
+
+ +

+ You've successfully authenticated with @ViewData["LoginProvider"]. + Please enter a user name for this site below and click the Register button to finish + logging in. +

+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/ExternalLoginFailure.cshtml b/src/TurboLinks.Net.Example/Views/Account/ExternalLoginFailure.cshtml new file mode 100644 index 0000000..71a0532 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ExternalLoginFailure.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Login Failure"; +} + +
+

@ViewData["Title"].

+

Unsuccessful login with service.

+
diff --git a/src/TurboLinks.Net.Example/Views/Account/ForgotPassword.cshtml b/src/TurboLinks.Net.Example/Views/Account/ForgotPassword.cshtml new file mode 100644 index 0000000..5b38e85 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ForgotPassword.cshtml @@ -0,0 +1,31 @@ +@model ForgotPasswordViewModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"].

+

+ For more information on how to enable reset password please see this article. +

+ +@*
+

Enter your email.

+
+
+
+ +
+ + +
+
+
+
+ +
+
+
*@ + +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/ForgotPasswordConfirmation.cshtml b/src/TurboLinks.Net.Example/Views/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000..ab9bf44 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Forgot Password Confirmation"; +} + +

@ViewData["Title"].

+

+ Please check your email to reset your password. +

diff --git a/src/TurboLinks.Net.Example/Views/Account/Lockout.cshtml b/src/TurboLinks.Net.Example/Views/Account/Lockout.cshtml new file mode 100644 index 0000000..6850a48 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/Lockout.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Locked out"; +} + +
+

Locked out.

+

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

+
diff --git a/src/TurboLinks.Net.Example/Views/Account/Login.cshtml b/src/TurboLinks.Net.Example/Views/Account/Login.cshtml new file mode 100644 index 0000000..c7155aa --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/Login.cshtml @@ -0,0 +1,90 @@ +@using System.Collections.Generic +@using Microsoft.AspNet.Http +@using Microsoft.AspNet.Http.Authentication +@model LoginViewModel +@inject SignInManager SignInManager + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"].

+
+
+
+
+

Use a local account to log in.

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

+ Register as a new user? +

+

+ Forgot your password? +

+
+
+
+
+
+

Use another service to log in.

+
+ @{ + var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList(); + if (loginProviders.Count == 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 loginProviders) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/Register.cshtml b/src/TurboLinks.Net.Example/Views/Account/Register.cshtml new file mode 100644 index 0000000..da86fec --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/Register.cshtml @@ -0,0 +1,42 @@ +@model RegisterViewModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"].

+ +
+

Create a new account.

+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/ResetPassword.cshtml b/src/TurboLinks.Net.Example/Views/Account/ResetPassword.cshtml new file mode 100644 index 0000000..7ddf52f --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ResetPassword.cshtml @@ -0,0 +1,43 @@ +@model ResetPasswordViewModel +@{ + ViewData["Title"] = "Reset password"; +} + +

@ViewData["Title"].

+ +
+

Reset your password.

+
+
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/ResetPasswordConfirmation.cshtml b/src/TurboLinks.Net.Example/Views/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000..bef2e45 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Reset password confirmation"; +} + +

@ViewData["Title"].

+

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

diff --git a/src/TurboLinks.Net.Example/Views/Account/SendCode.cshtml b/src/TurboLinks.Net.Example/Views/Account/SendCode.cshtml new file mode 100644 index 0000000..0c87629 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/SendCode.cshtml @@ -0,0 +1,21 @@ +@model SendCodeViewModel +@{ + ViewData["Title"] = "Send Verification Code"; +} + +

@ViewData["Title"].

+ +
+ +
+
+ Select Two-Factor Authentication Provider: + + +
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Account/VerifyCode.cshtml b/src/TurboLinks.Net.Example/Views/Account/VerifyCode.cshtml new file mode 100644 index 0000000..af98c78 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Account/VerifyCode.cshtml @@ -0,0 +1,38 @@ +@model VerifyCodeViewModel +@{ + ViewData["Title"] = "Verify"; +} + +

@ViewData["Title"].

+ +
+
+ + +

@ViewData["Status"]

+
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Home/About.cshtml b/src/TurboLinks.Net.Example/Views/Home/About.cshtml new file mode 100644 index 0000000..50476d1 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Home/About.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"].

+

@ViewData["Message"]

+ +

Use this area to provide additional information.

diff --git a/src/TurboLinks.Net.Example/Views/Home/Contact.cshtml b/src/TurboLinks.Net.Example/Views/Home/Contact.cshtml new file mode 100644 index 0000000..15c12c6 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Home/Contact.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"].

+

@ViewData["Message"]

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/src/TurboLinks.Net.Example/Views/Home/Index.cshtml b/src/TurboLinks.Net.Example/Views/Home/Index.cshtml new file mode 100644 index 0000000..1a0fa32 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Home/Index.cshtml @@ -0,0 +1,104 @@ +@{ + ViewData["Title"] = "Home Page"; +} + + + + diff --git a/src/TurboLinks.Net.Example/Views/Manage/AddPhoneNumber.cshtml b/src/TurboLinks.Net.Example/Views/Manage/AddPhoneNumber.cshtml new file mode 100644 index 0000000..daf3bb2 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/AddPhoneNumber.cshtml @@ -0,0 +1,27 @@ +@model AddPhoneNumberViewModel +@{ + ViewData["Title"] = "Add Phone Number"; +} + +

@ViewData["Title"].

+
+

Add a phone number.

+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Manage/ChangePassword.cshtml b/src/TurboLinks.Net.Example/Views/Manage/ChangePassword.cshtml new file mode 100644 index 0000000..ce0e757 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/ChangePassword.cshtml @@ -0,0 +1,42 @@ +@model ChangePasswordViewModel +@{ + ViewData["Title"] = "Change Password"; +} + +

@ViewData["Title"].

+ +
+

Change Password Form

+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Manage/Index.cshtml b/src/TurboLinks.Net.Example/Views/Manage/Index.cshtml new file mode 100644 index 0000000..b7a806c --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/Index.cshtml @@ -0,0 +1,79 @@ +@model IndexViewModel +@{ + ViewData["Title"] = "Manage your account"; +} + +

@ViewData["Title"].

+

@ViewData["StatusMessage"]

+
+

Change your account settings

+
+
+
Password:
+
+ [ + @if (Model.HasPassword) + { + Change + } + else + { + Create + } + ] +
+
External Logins:
+
+ + @Model.Logins.Count [Manage] +
+ + +
Phone Number:
+
+

+ Phone Numbers can used as a second factor of verification in two-factor authentication. + See this article + for details on setting up this ASP.NET application to support two-factor authentication using SMS. +

+ @*@(Model.PhoneNumber ?? "None") [ + @if (Model.PhoneNumber != null) + { + Change + @:  |  + Remove + } + else + { + Add + } + ]*@ +
+ +
Two-Factor Authentication:
+
+

+ There are no two-factor authentication providers configured. See this article + for setting up this application to support two-factor authentication. +

+ @*@if (Model.TwoFactor) + { +
+ + Enabled + + +
+ } + else + { +
+ + Disabled + + +
+ }*@ +
+
+
diff --git a/src/TurboLinks.Net.Example/Views/Manage/ManageLogins.cshtml b/src/TurboLinks.Net.Example/Views/Manage/ManageLogins.cshtml new file mode 100644 index 0000000..3a809b2 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/ManageLogins.cshtml @@ -0,0 +1,54 @@ +@model ManageLoginsViewModel +@using Microsoft.AspNet.Http.Authentication +@{ + ViewData["Title"] = "Manage your external logins"; +} + +

@ViewData["Title"].

+ +

@ViewData["StatusMessage"]

+@if (Model.CurrentLogins.Count > 0) +{ +

Registered Logins

+ + + @foreach (var account in Model.CurrentLogins) + { + + + + + } + +
@account.LoginProvider + @if ((bool)ViewData["ShowRemoveButton"]) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins.Count > 0) +{ +

Add another service to log in.

+
+
+
+

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

+
+
+} diff --git a/src/TurboLinks.Net.Example/Views/Manage/RemoveLogin.cshtml b/src/TurboLinks.Net.Example/Views/Manage/RemoveLogin.cshtml new file mode 100644 index 0000000..99a57da --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/RemoveLogin.cshtml @@ -0,0 +1,35 @@ +@model ICollection +@{ + ViewData["Title"] = "Remove Login"; +} + +@if (Model.Count > 0) +{ +

Registered Logins

+ + + @foreach (var account in Model) + { + + + + + } + +
@account.LoginProvider + @if ((bool)ViewData["ShowRemoveButton"]) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} diff --git a/src/TurboLinks.Net.Example/Views/Manage/SetPassword.cshtml b/src/TurboLinks.Net.Example/Views/Manage/SetPassword.cshtml new file mode 100644 index 0000000..d7c72a9 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/SetPassword.cshtml @@ -0,0 +1,38 @@ +@model SetPasswordViewModel +@{ + ViewData["Title"] = "Set Password"; +} + +

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

+ +
+

Set your password

+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Manage/VerifyPhoneNumber.cshtml b/src/TurboLinks.Net.Example/Views/Manage/VerifyPhoneNumber.cshtml new file mode 100644 index 0000000..a0e508d --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Manage/VerifyPhoneNumber.cshtml @@ -0,0 +1,30 @@ +@model VerifyPhoneNumberViewModel +@{ + ViewData["Title"] = "Verify Phone Number"; +} + +

@ViewData["Title"].

+ +
+ +

Add a phone number.

+
@ViewData["Status"]
+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/src/TurboLinks.Net.Example/Views/Shared/Error.cshtml b/src/TurboLinks.Net.Example/Views/Shared/Error.cshtml new file mode 100644 index 0000000..4852442 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Shared/Error.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

diff --git a/src/TurboLinks.Net.Example/Views/Shared/_Layout.cshtml b/src/TurboLinks.Net.Example/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..8eb4a18 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Shared/_Layout.cshtml @@ -0,0 +1,61 @@ + + + + + + @ViewData["Title"] - TurboLinks.Net.Example + + + + + + + + + + + + + + +
+ @RenderBody() +
+ + + + + + +
+

© 2015 - TurboLinks.Net.Example

+
+
+ + + @RenderSection("scripts", required: false) + + \ No newline at end of file diff --git a/src/TurboLinks.Net.Example/Views/Shared/_LoginPartial.cshtml b/src/TurboLinks.Net.Example/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000..f774bb3 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,20 @@ +@using System.Security.Claims + +@if (User.Identity.IsAuthenticated) +{ + +} +else +{ + +} diff --git a/src/TurboLinks.Net.Example/Views/Shared/_ValidationScriptsPartial.cshtml b/src/TurboLinks.Net.Example/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..eacda51 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/TurboLinks.Net.Example/Views/_ViewImports.cshtml b/src/TurboLinks.Net.Example/Views/_ViewImports.cshtml new file mode 100644 index 0000000..d93bb4c --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/_ViewImports.cshtml @@ -0,0 +1,6 @@ +@using TurboLinks.Net.Example +@using TurboLinks.Net.Example.Models +@using TurboLinks.Net.Example.ViewModels.Account +@using TurboLinks.Net.Example.ViewModels.Manage +@using Microsoft.AspNet.Identity +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" diff --git a/src/TurboLinks.Net.Example/Views/_ViewStart.cshtml b/src/TurboLinks.Net.Example/Views/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/src/TurboLinks.Net.Example/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/TurboLinks.Net.Example/appsettings.json b/src/TurboLinks.Net.Example/appsettings.json new file mode 100644 index 0000000..bec67cf --- /dev/null +++ b/src/TurboLinks.Net.Example/appsettings.json @@ -0,0 +1,7 @@ +{ + "Data": { + "DefaultConnection": { + "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=aspnet5-TurboLinks.Net.Example-660c79c7-b99e-4a1f-a454-505230c45a5e;Trusted_Connection=True;MultipleActiveResultSets=true" + } + } +} diff --git a/src/TurboLinks.Net.Example/bower.json b/src/TurboLinks.Net.Example/bower.json new file mode 100644 index 0000000..51002ed --- /dev/null +++ b/src/TurboLinks.Net.Example/bower.json @@ -0,0 +1,12 @@ +{ + "name": "ASP.NET", + "private": true, + "dependencies": { + "bootstrap": "3.0.0", + "bootstrap-touch-carousel": "0.8.0", + "hammer.js": "2.0.4", + "jquery": "2.1.4", + "jquery-validation": "1.11.1", + "jquery-validation-unobtrusive": "3.2.2" + } +} diff --git a/src/TurboLinks.Net.Example/gulpfile.js b/src/TurboLinks.Net.Example/gulpfile.js new file mode 100644 index 0000000..684c5ac --- /dev/null +++ b/src/TurboLinks.Net.Example/gulpfile.js @@ -0,0 +1,45 @@ +/// + +var gulp = require("gulp"), + rimraf = require("rimraf"), + concat = require("gulp-concat"), + cssmin = require("gulp-cssmin"), + uglify = require("gulp-uglify"), + project = require("./project.json"); + +var paths = { + webroot: "./" + project.webroot + "/" +}; + +paths.js = paths.webroot + "js/**/*.js"; +paths.minJs = paths.webroot + "js/**/*.min.js"; +paths.css = paths.webroot + "css/**/*.css"; +paths.minCss = paths.webroot + "css/**/*.min.css"; +paths.concatJsDest = paths.webroot + "js/site.min.js"; +paths.concatCssDest = paths.webroot + "css/site.min.css"; + +gulp.task("clean:js", function (cb) { + rimraf(paths.concatJsDest, cb); +}); + +gulp.task("clean:css", function (cb) { + rimraf(paths.concatCssDest, cb); +}); + +gulp.task("clean", ["clean:js", "clean:css"]); + +gulp.task("min:js", function () { + gulp.src([paths.js, "!" + paths.minJs], { base: "." }) + .pipe(concat(paths.concatJsDest)) + .pipe(uglify()) + .pipe(gulp.dest(".")); +}); + +gulp.task("min:css", function () { + gulp.src([paths.css, "!" + paths.minCss]) + .pipe(concat(paths.concatCssDest)) + .pipe(cssmin()) + .pipe(gulp.dest(".")); +}); + +gulp.task("min", ["min:js", "min:css"]); diff --git a/src/TurboLinks.Net.Example/package.json b/src/TurboLinks.Net.Example/package.json new file mode 100644 index 0000000..d4d71a9 --- /dev/null +++ b/src/TurboLinks.Net.Example/package.json @@ -0,0 +1,11 @@ +{ + "name": "ASP.NET", + "version": "0.0.0", + "devDependencies": { + "gulp": "3.8.11", + "gulp-concat": "2.5.2", + "gulp-cssmin": "0.1.7", + "gulp-uglify": "1.2.0", + "rimraf": "2.2.8" + } +} diff --git a/src/TurboLinks.Net.Example/project.json b/src/TurboLinks.Net.Example/project.json new file mode 100644 index 0000000..1f8ec85 --- /dev/null +++ b/src/TurboLinks.Net.Example/project.json @@ -0,0 +1,54 @@ +{ + "webroot": "wwwroot", + "userSecretsId": "aspnet5-TurboLinks.Net.Example-660c79c7-b99e-4a1f-a454-505230c45a5e", + "version": "1.0.0-*", + + "dependencies": { + "EntityFramework.Commands": "7.0.0-beta8", + "EntityFramework.SqlServer": "7.0.0-beta8", + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-beta8", + "Microsoft.AspNet.Authentication.Facebook": "1.0.0-beta8", + "Microsoft.AspNet.Authentication.Google": "1.0.0-beta8", + "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-beta8", + "Microsoft.AspNet.Authentication.Twitter": "1.0.0-beta8", + "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", + "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta8", + "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", + "Microsoft.AspNet.Mvc": "6.0.0-beta8", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", + "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", + "Microsoft.Framework.Configuration.Abstractions": "1.0.0-beta8", + "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", + "Microsoft.Framework.Configuration.UserSecrets": "1.0.0-beta8", + "Microsoft.Framework.Logging": "1.0.0-beta8", + "Microsoft.Framework.Logging.Console": "1.0.0-beta8", + "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", + "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta8", + "TurboLinks.Net": "" + }, + + "commands": { + "web": "Microsoft.AspNet.Server.Kestrel", + "ef": "EntityFramework.Commands" + }, + + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + + "exclude": [ + "wwwroot", + "node_modules" + ], + "publishExclude": [ + "**.user", + "**.vspscc" + ], + "scripts": { + "prepublish": [ "npm install", "bower install", "gulp clean", "gulp min" ] + } +} \ No newline at end of file diff --git a/src/TurboLinks.Net/TurboLinks.Net.xproj b/src/TurboLinks.Net/TurboLinks.Net.xproj new file mode 100644 index 0000000..230643b --- /dev/null +++ b/src/TurboLinks.Net/TurboLinks.Net.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + b57eee3b-37ff-49b5-8841-84b759df1471 + TurboLinks.Net + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/TurboLinks.Net/TurboLinks.cs b/src/TurboLinks.Net/TurboLinks.cs new file mode 100644 index 0000000..3861e0f --- /dev/null +++ b/src/TurboLinks.Net/TurboLinks.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; + +namespace TurboLinks.Net +{ + public class TurboLinks + { + private RequestDelegate _next; + + public TurboLinks(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + var memoryStream = new MemoryStream(); + var bodyStream = context.Response.Body; + context.Response.Body = memoryStream; + await _next?.Invoke(context); + var request = context.Request; + var response = context.Response; + + if(!string.IsNullOrWhiteSpace(request.Headers["X-XHR-Referer"])) + { + context.Response.Cookies.Append("request_method", request.Method, new CookieOptions { HttpOnly = false }); + if(context.Response.StatusCode == 301 || context.Response.StatusCode == 302) + { + var uri = new Uri(response.Headers["Location"]); + if(uri.Host.Equals(request.Host.Value)) + { + response.Headers["X-XHR-Redirected-To"] = response.Headers["Location"]; + } + } + } + memoryStream.WriteTo(bodyStream); + await bodyStream.FlushAsync(); + memoryStream.Dispose(); + bodyStream.Dispose(); + } + } + + public static class BuilderExtension + { + public static void UseTurboLinks(this IApplicationBuilder app) + { + app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/TurboLinks.Net/project.json b/src/TurboLinks.Net/project.json new file mode 100644 index 0000000..9fa5b19 --- /dev/null +++ b/src/TurboLinks.Net/project.json @@ -0,0 +1,27 @@ +{ + "version": "1.0.0-*", + "description": "TurboLinks.Net Class Library", + "authors": [ "Tommy Parnell" ], + "tags": [ "" ], + "projectUrl": "", + "licenseUrl": "", + "dependencies": { + }, + "frameworks": { + "dnx451": { + "dependencies": { + "Microsoft.AspNet.Http.Abstractions": "1.0.0-beta8" + } + }, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-23225", + "System.Collections": "4.0.11-beta-23225", + "System.Linq": "4.0.1-beta-23225", + "System.Runtime": "4.0.21-beta-23225", + "System.Threading": "4.0.11-beta-23225", + "Microsoft.AspNet.Http.Abstractions": "1.0.0-beta8" + } + } + } +} \ No newline at end of file