├── .gitignore ├── BlogPlayground.E2ETest ├── .gitignore ├── BlogPlayground.E2ETest.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── commands │ └── login.js ├── globals.js ├── nightwatch.config.js ├── package.json ├── selenium-drivers │ ├── MicrosoftWebDriver.exe │ ├── chromedriver-2.35-linux │ ├── chromedriver-2.35-mac │ └── chromedriver-2.35-win.exe └── tests │ ├── 0.smoke.test.js │ └── articles.test.js ├── BlogPlayground.IntegrationTest ├── ArticlesApiTest.cs ├── ArticlesTest.cs ├── BlogPlayground.IntegrationTest.csproj ├── Data │ ├── DatabaseSeeder.cs │ └── PredefinedData.cs ├── HomeTest.cs ├── TestFixture.cs └── TestStartup.cs ├── BlogPlayground.Test ├── BlogPlayground.Test.csproj └── Controllers │ ├── ArticlesApiControllerTest.cs │ └── ArticlesControllerTest.cs ├── BlogPlayground.sln ├── BlogPlayground ├── .bowerrc ├── BlogPlayground.csproj ├── Controllers │ ├── AccountController.cs │ ├── ArticlesApiController.cs │ ├── ArticlesController.cs │ ├── HomeController.cs │ └── ManageController.cs ├── Data │ ├── ApplicationDbContext.cs │ ├── ArticlesRepository.cs │ ├── IArticlesRepository.cs │ └── Migrations │ │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ │ ├── 00000000000000_CreateIdentitySchema.cs │ │ ├── 20160910210414_update-profile.Designer.cs │ │ ├── 20160910210414_update-profile.cs │ │ ├── 20160913175647_articles.Designer.cs │ │ ├── 20160913175647_articles.cs │ │ └── ApplicationDbContextModelSnapshot.cs ├── Models │ ├── AccountViewModels │ │ ├── ExternalLoginConfirmationViewModel.cs │ │ ├── ForgotPasswordViewModel.cs │ │ ├── LoginViewModel.cs │ │ ├── RegisterViewModel.cs │ │ ├── ResetPasswordViewModel.cs │ │ ├── SendCodeViewModel.cs │ │ └── VerifyCodeViewModel.cs │ ├── ApplicationUser.cs │ ├── Article.cs │ └── ManageViewModels │ │ ├── AddPhoneNumberViewModel.cs │ │ ├── ChangePasswordViewModel.cs │ │ ├── ConfigureTwoFactorViewModel.cs │ │ ├── FactorViewModel.cs │ │ ├── IndexViewModel.cs │ │ ├── ManageLoginsViewModel.cs │ │ ├── RemoveLoginViewModel.cs │ │ ├── SetPasswordViewModel.cs │ │ └── VerifyPhoneNumberViewModel.cs ├── Program.cs ├── Project_Readme.html ├── Properties │ └── launchSettings.json ├── Services │ ├── GooglePictureLocator.cs │ ├── IEmailSender.cs │ ├── IGooglePictureLocator.cs │ ├── IRequestUserProvider.cs │ ├── ISmsSender.cs │ ├── MessageServices.cs │ └── RequestUserProvider.cs ├── Startup.cs ├── TagHelpers │ ├── MarkdownTagHelper.cs │ └── ProfilePictureTagHelper.cs ├── ViewComponents │ └── LatestArticlesViewComponent.cs ├── Views │ ├── Account │ │ ├── ConfirmEmail.cshtml │ │ ├── ExternalLoginConfirmation.cshtml │ │ ├── ExternalLoginFailure.cshtml │ │ ├── ForgotPassword.cshtml │ │ ├── ForgotPasswordConfirmation.cshtml │ │ ├── Lockout.cshtml │ │ ├── Login.cshtml │ │ ├── Register.cshtml │ │ ├── ResetPassword.cshtml │ │ ├── ResetPasswordConfirmation.cshtml │ │ ├── SendCode.cshtml │ │ └── VerifyCode.cshtml │ ├── Articles │ │ ├── Create.cshtml │ │ ├── Delete.cshtml │ │ ├── Details.cshtml │ │ ├── Index.cshtml │ │ └── _ArticleSummary.cshtml │ ├── Home │ │ ├── About.cshtml │ │ ├── Contact.cshtml │ │ └── Index.cshtml │ ├── Manage │ │ ├── AddPhoneNumber.cshtml │ │ ├── ChangePassword.cshtml │ │ ├── Index.cshtml │ │ ├── ManageLogins.cshtml │ │ ├── SetPassword.cshtml │ │ └── VerifyPhoneNumber.cshtml │ ├── Shared │ │ ├── Components │ │ │ └── LatestArticles │ │ │ │ └── Default.cshtml │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ ├── _LoginPartial.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.json ├── bower.json ├── bundleconfig.json ├── web.config └── wwwroot │ ├── _references.js │ ├── css │ ├── site.css │ └── site.min.css │ ├── favicon.ico │ ├── images │ ├── banner1.svg │ ├── banner2.svg │ ├── banner3.svg │ ├── banner4.svg │ └── placeholder.png │ ├── js │ ├── site.js │ └── site.min.js │ └── lib │ ├── bootstrap │ ├── .bower.json │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── npm.js │ ├── jquery-validation-unobtrusive │ ├── .bower.json │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── .bower.json │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── .bower.json │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/.gitignore: -------------------------------------------------------------------------------- 1 | .test-results/** -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/BlogPlayground.E2ETest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.PlatformAbstractions; 11 | 12 | namespace BlogPlayground.E2ETest 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | BuildWebHost(args).Run(); 19 | } 20 | 21 | public static IWebHost BuildWebHost(string[] args) 22 | { 23 | // To avoid hardcoding path to project, see: https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing#integration-testing 24 | var integrationTestsPath = PlatformServices.Default.Application.ApplicationBasePath; // e2e_tests/bin/Debug/netcoreapp2.0 25 | var applicationPath = Path.GetFullPath(Path.Combine(integrationTestsPath, "../../../../BlogPlayground")); 26 | 27 | return WebHost.CreateDefaultBuilder(args) 28 | .UseStartup() 29 | .UseContentRoot(applicationPath) 30 | .UseEnvironment("Development") 31 | .Build(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56744/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BlogPlayground.E2ETest": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:56745/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Configuration; 10 | using BlogPlayground.IntegrationTest.Data; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Data.Sqlite; 13 | using BlogPlayground.Data; 14 | using Microsoft.EntityFrameworkCore; 15 | 16 | namespace BlogPlayground.E2ETest 17 | { 18 | public class TestStartup: BlogPlayground.Startup 19 | { 20 | public TestStartup(IConfiguration configuration) : base(configuration) 21 | { 22 | } 23 | 24 | public override void ConfigureDatabase(IServiceCollection services) 25 | { 26 | // Replace default database connection with SQLite in memory 27 | var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" }; 28 | var connection = new SqliteConnection(connectionStringBuilder.ToString()); 29 | services.AddDbContext(options => options.UseSqlite(connection)); 30 | 31 | // Register the database seeder 32 | services.AddTransient(); 33 | } 34 | 35 | public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 36 | { 37 | // Perform all the configuration in the base class 38 | base.Configure(app, env, loggerFactory); 39 | 40 | // Now create and seed the database 41 | using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) 42 | using (var dbContext = serviceScope.ServiceProvider.GetService()) 43 | { 44 | dbContext.Database.OpenConnection(); 45 | dbContext.Database.EnsureCreated(); 46 | 47 | var seeder = serviceScope.ServiceProvider.GetService(); 48 | seeder.Seed().Wait(); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/commands/login.js: -------------------------------------------------------------------------------- 1 | exports.command = function (userInfo) { 2 | const browser = this; 3 | browser 4 | // go to login url 5 | .url(browser.globals.login_url) 6 | .waitForElementVisible('.form-login', browser.globals.initial_load_timeout) 7 | // fill login form 8 | .setValue('input[name=Email]', userInfo.email) 9 | .setValue('input[name=Password]', userInfo.password) 10 | .click('.form-login button.btn-default') 11 | .pause(1000) 12 | // After login, we should land in the home page logged in as the test user 13 | .assert.title('Home Page - BlogPlayground') 14 | .assert.containsText('.navbar form#logout-form', 'Hello Tester!'); 15 | 16 | return this; // allows the command to be chained. 17 | }; -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/globals.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | let dotnetWebServer = null; 6 | let dotnetWebServerStarted = false; 7 | 8 | function startWebApplication(outputFolder, done) { 9 | const logFile = path.join(outputFolder, 'server.log'); 10 | console.log(`Starting web application. Log found at: ${logFile}`); 11 | 12 | // Start web app in separated process. 13 | dotnetWebServer = childProcess.spawn("dotnet", ["run"]); 14 | 15 | // Fail test run startup if the server dies before it got properly started 16 | dotnetWebServer.on('close', (code) => { 17 | if (code !== 0 && !dotnetWebServerStarted) { 18 | console.error(`Could not start dotnet server. Exited with code ${code}. Check log at ${logFile}`); 19 | process.exit(-1); 20 | } 21 | }); 22 | 23 | // Do not start the test until we see the "Application started" message from dotnet 24 | dotnetWebServer.stdout.on('data', (chunk) => { 25 | if (chunk.toString().includes("Application started")) { 26 | dotnetWebServerStarted = true; 27 | done(); 28 | } 29 | }); 30 | 31 | // Redirect the standard output of the web application to a log file 32 | const appLogPath = path.join(__dirname, logFile); 33 | const childServerLog = fs.createWriteStream(appLogPath); 34 | dotnetWebServer.stdout.pipe(childServerLog); 35 | dotnetWebServer.stderr.pipe(childServerLog); 36 | } 37 | 38 | module.exports = { 39 | // Specific initialization per browser, available in before/after methods as this.my_setting 40 | // 'default' : { 41 | // my_setting : true, 42 | // }, 43 | // 'chrome' : { 44 | // my_setting : true, 45 | // }, 46 | 47 | // External before hook is ran at the beginning of the tests run, before creating the Selenium session 48 | before: function (done) { 49 | 50 | // run this only if we want to start the web app as part of the test run 51 | if (this.start_app) { 52 | startWebApplication(this.output_folder, done); 53 | } else { 54 | done(); 55 | } 56 | }, 57 | 58 | // External after hook is ran at the very end of the tests run, after closing the Selenium session 59 | after: function (done) { 60 | // run this only if we start the app as part of the test run 61 | if (this.start_app) { 62 | // Kill the dotnet web application 63 | const os = process.platform; 64 | if (/^win/.test(os)) childProcess.spawn("taskkill", ["/pid", dotnetWebServer.pid, '/f', '/t']); 65 | else dotnetWebServer.kill('SIGINT'); 66 | 67 | done(); 68 | } else { 69 | done(); 70 | } 71 | }, 72 | 73 | // This will be run before each test suite is started 74 | beforeEach: function (browser, done) { 75 | // Set specific browser window size 76 | browser 77 | .resizeWindow(browser.globals.window_size.width, browser.globals.window_size.height) 78 | .pause(500); 79 | 80 | // Every test will need to login with the test user (except in the smokeTest where login is part of the test itself) 81 | if ( !browser.currentTest.module.endsWith("smoke.test")) { 82 | browser.login(browser.globals.user_info); 83 | } 84 | 85 | //Once all steps are finished, signal we are done 86 | browser.perform(function () { 87 | done(); 88 | }); 89 | }, 90 | 91 | // This will be run after each test suite is finished 92 | afterEach: function (browser, done) { 93 | //close the browser then signal we are done 94 | browser 95 | .end() 96 | .perform(function () { 97 | done(); 98 | }); 99 | } 100 | }; -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/nightwatch.config.js: -------------------------------------------------------------------------------- 1 | const seleniumJar = require('selenium-server-standalone-jar'); 2 | 3 | // Nightwatch.js configuration. 4 | // See the following url for a full list of settings: http://nightwatchjs.org/gettingstarted#settings-file 5 | const settings = { 6 | // Nightwatch global settings 7 | src_folders: ['./tests'], 8 | output_folder: '.test-results/', 9 | 10 | // Nightwatch extension components (commands, pages, assertions, global hooks) 11 | globals_path: './globals.js', 12 | custom_commands_path: './commands', 13 | //'page_objects_path ': './pages', 14 | //custom_assertions_path: './assertions' 15 | 16 | // Selenium settings 17 | selenium: { 18 | start_process: true, 19 | server_path: seleniumJar.path, 20 | start_session: true, 21 | log_path: '.test-results/', 22 | port: process.env.SELENIUM_PORT || 4444, 23 | host: process.env.SELENIUM_HOST || '127.0.0.1', 24 | debug: true, 25 | cli_args: { 26 | 'webdriver.edge.driver': './selenium-drivers/MicrosoftWebDriver.exe', 27 | 'webdriver.chrome.driver': '', 28 | 'webdriver.chrome.logfile': '.test-results/chromedriver.log', 29 | 'webdriver.chrome.verboseLogging': false, 30 | } 31 | }, 32 | 33 | test_settings: { 34 | default: { 35 | // Nightwatch test-specific settings 36 | launch_url: process.env.LAUNCH_URL || 'http://localhost:56745', 37 | selenium_port: process.env.SELENIUM_PORT || 4444, 38 | selenium_host: process.env.SELENIUM_HOST || 'localhost', 39 | silent: true, 40 | screenshots: { 41 | enabled: true, 42 | on_failure: true, 43 | on_error: true, 44 | path: '.test-results/screenshots' 45 | }, 46 | // browser-related settings. To be defined on each specific browser section 47 | desiredCapabilities: { 48 | }, 49 | // user defined settings 50 | globals: { 51 | window_size: { 52 | width: 1280, 53 | height: 1024 54 | }, 55 | start_app: process.env.LAUNCH_URL ? false : true, 56 | login_url: (process.env.LAUNCH_URL || 'http://localhost:56745') + '/Account/Login', 57 | user_info: { 58 | email: 'tester@test.com', 59 | password: '!Covfefe123', 60 | }, 61 | navigation_timeout: 5000, 62 | initial_load_timeout: 10000 63 | } 64 | }, 65 | // Define an environment per each of the target browsers 66 | chrome: { 67 | desiredCapabilities: { 68 | browserName: 'chrome', 69 | javascriptEnabled: true, 70 | acceptSslCerts: true, 71 | chromeOptions: { 72 | args: [ 73 | '--headless', 74 | '--no-sandbox', 75 | '--disable-popup-blocking', 76 | '--disable-infobars', 77 | '--enable-automation', 78 | ], 79 | //useAutomationExtension: true, 80 | //prefs: { 81 | // credentials_enable_service: false, 82 | // profile: { 83 | // password_manager_enabled: false, 84 | // }, 85 | //}, 86 | } 87 | }, 88 | }, 89 | edge: { 90 | desiredCapabilities: { 91 | browserName: 'MicrosoftEdge', 92 | javascriptEnabled: true, 93 | acceptSslCerts: true 94 | } 95 | } 96 | } 97 | }; 98 | 99 | //make output folder available in code (so we can redirect the dotnet server output to a log file there) 100 | settings.test_settings.default.globals.output_folder = settings.output_folder; 101 | 102 | //Set path to chromedriver depending on host OS 103 | var os = process.platform; 104 | if (/^win/.test(os)) { 105 | settings.selenium.cli_args['webdriver.chrome.driver'] = './selenium-drivers/chromedriver-2.35-win.exe'; 106 | } else if (/^darwin/.test(os)) { 107 | settings.selenium.cli_args['webdriver.chrome.driver'] = './selenium-drivers/chromedriver-2.35-mac.exe'; 108 | } else { 109 | settings.selenium.cli_args['webdriver.chrome.driver'] = './selenium-drivers/chromedriver-2.35-linux.exe'; 110 | } 111 | 112 | // Display effecctive settings on test run output 113 | console.log(JSON.stringify(settings, null, 2)); 114 | 115 | module.exports = settings; -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "blogplayground.e2etest", 4 | "private": true, 5 | "scripts": { 6 | "test:chrome": "nightwatch --config ./nightwatch.config.js --env chrome", 7 | "test:edge": "nightwatch --config ./nightwatch.config.js --env edge" 8 | }, 9 | "devDependencies": { 10 | "nightwatch": "0.9.19", 11 | "selenium-server-standalone-jar": "3.8.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/selenium-drivers/MicrosoftWebDriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground.E2ETest/selenium-drivers/MicrosoftWebDriver.exe -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/selenium-drivers/chromedriver-2.35-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground.E2ETest/selenium-drivers/chromedriver-2.35-linux -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/selenium-drivers/chromedriver-2.35-mac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground.E2ETest/selenium-drivers/chromedriver-2.35-mac -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/selenium-drivers/chromedriver-2.35-win.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground.E2ETest/selenium-drivers/chromedriver-2.35-win.exe -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/tests/0.smoke.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | '@tags': ['smoke-test', 'home-page'], 5 | 6 | 'can login with test user': function (browser) { 7 | browser.login(browser.globals.user_info); 8 | }, 9 | 10 | 'home page can be opened with default url': function (browser) { 11 | browser 12 | .url(browser.launchUrl) 13 | .assert.title('Home Page - BlogPlayground') 14 | .waitForElementVisible('body', browser.globals.navigation_timeout) 15 | .assert.containsText('.body-content #myCarousel .item:first-child', 'Learn how to build ASP.NET apps that can run anywhere.'); 16 | }, 17 | 18 | 'can logout': function (browser) { 19 | browser 20 | .assert.containsText('.navbar form#logout-form', 'Hello Tester!') 21 | .click('.navbar form#logout-form button[type=submit]') 22 | .waitForElementVisible('.navbar .navbar-login', browser.globals.initial_load_timeout) 23 | .assert.containsText('.navbar .navbar-login', 'Log in') 24 | .assert.attributeContains('.navbar .navbar-login .login-link', 'href', browser.globals.login_url); 25 | } 26 | }; -------------------------------------------------------------------------------- /BlogPlayground.E2ETest/tests/articles.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const testArticle = { 4 | title: 'Testing with Nightwatch', 5 | abstract: 'This is an automated test', 6 | contents: 'Verifying articles can be added' 7 | } 8 | 9 | module.exports = { 10 | '@tags': ['articles-page'], 11 | 12 | 'Articles can be opened with its url': function (browser) { 13 | browser 14 | // Open the articles list page 15 | .url(`${browser.launchUrl}/Articles`) 16 | .assert.title('Articles - BlogPlayground') 17 | .waitForElementVisible('body', browser.globals.navigation_timeout) 18 | // Verify at least the 2 default articles show up in the list 19 | .expect.element('.body-content .article-list li:nth-child(1)').to.be.present; 20 | }, 21 | 22 | 'New Articles can be added': function (browser) { 23 | browser 24 | // Go to the create page 25 | .url(`${browser.launchUrl}/Articles/Create`) 26 | .assert.title('Create - BlogPlayground') 27 | .waitForElementVisible('body', browser.globals.navigation_timeout) 28 | // Enter the details and submit 29 | .setValue('.body-content input[name=Title]', testArticle.title) 30 | .setValue('.body-content textarea[name=Abstract]', testArticle.abstract) 31 | .setValue('.body-content textarea[name=Contents]', testArticle.contents) 32 | .click('.body-content input[type=submit]') 33 | // Verify we go back to the articles list 34 | .pause(browser.globals.navigation_timeout) 35 | .assert.title('Articles - BlogPlayground'); 36 | }, 37 | 38 | 'New Articles show in the Articles page': function (browser) { 39 | browser 40 | .assert.containsText('.body-content .article-list li:first-child', testArticle.title) 41 | .assert.containsText('.body-content .article-list li:first-child', testArticle.abstract) 42 | .assert.containsText('.body-content .article-list li:first-child .author-name', 'Tester'); 43 | }, 44 | 45 | 'Articles can be read in their details page': function (browser) { 46 | browser 47 | // Open the article from the lisdt 48 | .click('.body-content .article-list li:first-child h4 a') 49 | // Verify navigation to the article details and the right contents are displayed 50 | .pause(browser.globals.navigation_timeout) 51 | .assert.title(`${testArticle.title} - BlogPlayground`) 52 | .assert.containsText('.body-content .article-summary', testArticle.title) 53 | .assert.containsText('.body-content .article-summary', testArticle.abstract) 54 | .assert.containsText('.body-content .article-summary .author-name', 'Tester') 55 | .assert.containsText('.body-content .markdown-contents', testArticle.contents); 56 | }, 57 | 58 | 'Articles can be removed': function (browser) { 59 | browser 60 | // Click remove on article details 61 | .click('.body-content .sidebar button.dropdown-toggle') 62 | .waitForElementVisible('.body-content .sidebar ul.dropdown-menu', browser.globals.navigation_timeout) 63 | .click('.body-content .sidebar ul.dropdown-menu li:last-child a') 64 | // Verify navigation to the confirmation page and click delete 65 | .pause(browser.globals.navigation_timeout) 66 | .assert.title('Delete - BlogPlayground') 67 | .click('.body-content input[type=submit]') 68 | // Verify navigation to articles list and that it disappeared from the list 69 | .pause(browser.globals.navigation_timeout) 70 | .assert.title('Articles - BlogPlayground') 71 | .assert.containsText('.body-content .article-list li:first-child', 'Test Article 2'); 72 | } 73 | }; -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/ArticlesApiTest.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.IntegrationTest.Data; 2 | using BlogPlayground.Models; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace BlogPlayground.IntegrationTest 12 | { 13 | public class ArticlesApiTest : TestFixture 14 | { 15 | [Fact] 16 | public async Task GetArticles_ReturnsArticlesList() 17 | { 18 | // Act 19 | var response = await _client.GetAsync("/api/articles"); 20 | 21 | // Assert 22 | response.EnsureSuccessStatusCode(); 23 | var responseString = await response.Content.ReadAsStringAsync(); 24 | var articles = JsonConvert.DeserializeObject(responseString); 25 | Assert.NotStrictEqual(PredefinedData.Articles, articles); 26 | } 27 | 28 | [Fact] 29 | public async Task GetArticle_ReturnsSpecifiedArticle() 30 | { 31 | // Act 32 | var response = await _client.GetAsync($"/api/articles/{PredefinedData.Articles[0].ArticleId}"); 33 | 34 | // Assert 35 | response.EnsureSuccessStatusCode(); 36 | var responseString = await response.Content.ReadAsStringAsync(); 37 | var article = JsonConvert.DeserializeObject
(responseString); 38 | Assert.NotStrictEqual(PredefinedData.Articles[0], article); 39 | } 40 | 41 | [Fact] 42 | public async Task AddArticle_ReturnsAddedArticle() 43 | { 44 | // Arrange 45 | await EnsureAuthenticationCookie(); 46 | await EnsureAntiforgeryTokenHeader(); 47 | var article = new Article { Title = "mock title", Abstract = "mock abstract", Contents = "mock contents" }; 48 | 49 | // Act 50 | var contents = new StringContent(JsonConvert.SerializeObject(article), Encoding.UTF8, "application/json"); 51 | var response = await _client.PostAsync("/api/articles", contents); 52 | 53 | // Assert 54 | response.EnsureSuccessStatusCode(); 55 | var responseString = await response.Content.ReadAsStringAsync(); 56 | var addedArticle = JsonConvert.DeserializeObject
(responseString); 57 | Assert.True(addedArticle.ArticleId > 0, "Expected added article to have a valid id"); 58 | } 59 | 60 | [Fact] 61 | public async Task DeleteConfirmation_RedirectsToList_AfterDeletingArticle() 62 | { 63 | // Arrange 64 | await EnsureAuthenticationCookie(); 65 | await EnsureAntiforgeryTokenHeader(); 66 | 67 | // Act 68 | var request = new HttpRequestMessage 69 | { 70 | Method = HttpMethod.Delete, 71 | RequestUri = new Uri($"/api/articles/{PredefinedData.Articles[0].ArticleId}", UriKind.Relative), 72 | }; 73 | var response = await _client.SendAsync(request); 74 | 75 | // Assert 76 | Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/ArticlesTest.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.IntegrationTest.Data; 2 | using Microsoft.AspNetCore; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.TestHost; 5 | using Microsoft.Extensions.PlatformAbstractions; 6 | using Microsoft.Net.Http.Headers; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Net; 12 | using System.Net.Http; 13 | using System.Text.RegularExpressions; 14 | using System.Threading.Tasks; 15 | using Xunit; 16 | 17 | namespace BlogPlayground.IntegrationTest 18 | { 19 | public class ArticlesTest : TestFixture 20 | { 21 | [Fact] 22 | public async Task Index_Get_ReturnsIndexHtmlPage_ListingEveryArticle() 23 | { 24 | // Act 25 | var response = await _client.GetAsync("/Articles"); 26 | 27 | // Assert 28 | response.EnsureSuccessStatusCode(); 29 | var responseString = await response.Content.ReadAsStringAsync(); 30 | foreach (var article in PredefinedData.Articles) 31 | { 32 | Assert.Contains($"
  • ", responseString); 33 | } 34 | } 35 | 36 | [Fact] 37 | public async Task Details_Get_ReturnsHtmlPage() 38 | { 39 | // Act 40 | var response = await _client.GetAsync($"/Articles/Details/{PredefinedData.Articles[0].ArticleId}"); 41 | 42 | // Assert 43 | response.EnsureSuccessStatusCode(); 44 | var responseString = await response.Content.ReadAsStringAsync(); 45 | Assert.Contains(PredefinedData.Articles[0].Contents, responseString); 46 | } 47 | 48 | [Fact] 49 | public async Task Create_Get_ReturnsHtmlPage() 50 | { 51 | // Arrange 52 | await EnsureAuthenticationCookie(); 53 | 54 | // Act 55 | var response = await _client.GetAsync("/Articles/Create"); 56 | 57 | // Assert 58 | response.EnsureSuccessStatusCode(); 59 | var responseString = await response.Content.ReadAsStringAsync(); 60 | Assert.Contains("

    Create new article

    ", responseString); 61 | } 62 | 63 | [Fact] 64 | public async Task Create_Post_RedirectsToList_AfterCreatingArticle() 65 | { 66 | // Arrange 67 | await EnsureAuthenticationCookie(); 68 | var formData = await EnsureAntiforgeryTokenForm(new Dictionary 69 | { 70 | { "Title", "mock title" }, 71 | { "Abstract", "mock abstract" }, 72 | { "Contents", "mock contents" } 73 | }); 74 | 75 | // Act 76 | var response = await _client.PostAsync("/Articles/Create", new FormUrlEncodedContent(formData)); 77 | 78 | // Assert 79 | Assert.Equal(HttpStatusCode.Found, response.StatusCode); 80 | Assert.Equal("/Articles", response.Headers.Location.ToString()); 81 | } 82 | 83 | [Fact] 84 | public async Task Delete_Get_ReturnsHtmlPage() 85 | { 86 | // Arrange 87 | await EnsureAuthenticationCookie(); 88 | 89 | // Act 90 | var response = await _client.GetAsync($"/Articles/Delete/{PredefinedData.Articles[0].ArticleId}"); 91 | 92 | // Assert 93 | response.EnsureSuccessStatusCode(); 94 | var responseString = await response.Content.ReadAsStringAsync(); 95 | Assert.Contains("

    Are you sure you want to delete this?

    ", responseString); 96 | } 97 | 98 | [Fact] 99 | public async Task DeleteConfirmation_RedirectsToList_AfterDeletingArticle() 100 | { 101 | // Arrange 102 | await EnsureAuthenticationCookie(); 103 | var formData = await EnsureAntiforgeryTokenForm(); 104 | 105 | // Act 106 | var response = await _client.PostAsync($"/Articles/Delete/{PredefinedData.Articles[0].ArticleId}", new FormUrlEncodedContent(formData)); 107 | 108 | // Assert 109 | Assert.Equal(HttpStatusCode.Found, response.StatusCode); 110 | Assert.Equal("/Articles", response.Headers.Location.ToString()); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/BlogPlayground.IntegrationTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/Data/DatabaseSeeder.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.Data; 2 | using BlogPlayground.Models; 3 | using Microsoft.AspNetCore.Identity; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace BlogPlayground.IntegrationTest.Data 11 | { 12 | public class DatabaseSeeder 13 | { 14 | private readonly UserManager _userManager; 15 | private readonly ApplicationDbContext _context; 16 | 17 | public DatabaseSeeder(ApplicationDbContext context, UserManager userManager) 18 | { 19 | _context = context; 20 | _userManager = userManager; 21 | } 22 | 23 | public async Task Seed() 24 | { 25 | // Add all the predefined profiles using the predefined password 26 | foreach (var profile in PredefinedData.Profiles) 27 | { 28 | await _userManager.CreateAsync(profile, PredefinedData.Password); 29 | // Set the AuthorId navigation property 30 | if (profile.Email == "author@test.com") 31 | { 32 | PredefinedData.Articles.ToList().ForEach(a => a.AuthorId = profile.Id); 33 | } 34 | } 35 | 36 | // Add all the predefined articles 37 | _context.Article.AddRange(PredefinedData.Articles); 38 | _context.SaveChanges(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/Data/PredefinedData.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace BlogPlayground.IntegrationTest.Data 7 | { 8 | public static class PredefinedData 9 | { 10 | public static string Password = @"!Covfefe123"; 11 | 12 | public static ApplicationUser[] Profiles = new[] { 13 | new ApplicationUser { Email = "tester@test.com", UserName = "tester@test.com", FullName = "Tester" }, 14 | new ApplicationUser { Email = "author@test.com", UserName = "author@test.com", FullName = "Tester" } 15 | }; 16 | 17 | public static Article[] Articles = new[] { 18 | new Article { ArticleId = 111, Title = "Test Article 1", Abstract = "Abstract 1", Contents = "Contents 1", CreatedDate = DateTime.Now.Subtract(TimeSpan.FromMinutes(60)) }, 19 | new Article { ArticleId = 222, Title = "Test Article 2", Abstract = "Abstract 2", Contents = "Contents 2", CreatedDate = DateTime.Now } 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/HomeTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.TestHost; 4 | using Microsoft.Extensions.PlatformAbstractions; 5 | using System; 6 | using System.IO; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace BlogPlayground.IntegrationTest 12 | { 13 | public class HomeTest: TestFixture 14 | { 15 | [Fact] 16 | public async Task Index_Get_ReturnsIndexHtmlPage() 17 | { 18 | // Act 19 | var response = await _client.GetAsync("/"); 20 | 21 | // Assert 22 | response.EnsureSuccessStatusCode(); 23 | var responseString = await response.Content.ReadAsStringAsync(); 24 | Assert.Contains("Home Page - BlogPlayground", responseString); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/TestFixture.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.IntegrationTest.Data; 2 | using Microsoft.AspNetCore; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.TestHost; 5 | using Microsoft.Extensions.PlatformAbstractions; 6 | using Microsoft.Net.Http.Headers; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Net; 12 | using System.Net.Http; 13 | using System.Text.RegularExpressions; 14 | using System.Threading.Tasks; 15 | using Xunit; 16 | 17 | namespace BlogPlayground.IntegrationTest 18 | { 19 | public class TestFixture: IDisposable 20 | { 21 | protected readonly TestServer _server; 22 | protected readonly HttpClient _client; 23 | 24 | public TestFixture() 25 | { 26 | // To avoid hardcoding path to project, see: https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing#integration-testing 27 | var integrationTestsPath = PlatformServices.Default.Application.ApplicationBasePath; // integration_tests/bin/Debug/netcoreapp2.0 28 | var applicationPath = Path.GetFullPath(Path.Combine(integrationTestsPath, "../../../../BlogPlayground")); 29 | 30 | _server = new TestServer(WebHost.CreateDefaultBuilder() 31 | .UseStartup() 32 | .UseContentRoot(applicationPath) 33 | .UseEnvironment("Development")); 34 | _client = _server.CreateClient(); 35 | } 36 | 37 | public void Dispose() 38 | { 39 | _client.Dispose(); 40 | _server.Dispose(); 41 | } 42 | 43 | protected static string AUTHENTICATION_COOKIE = ".AspNetCore.Identity."; 44 | protected static string ANTIFORGERY_COOKIE = ".AspNetCore.AntiForgery."; 45 | protected static string ANTIFORGERY_TOKEN_FORM = "__RequestVerificationToken"; 46 | protected static string ANTIFORGERTY_TOKEN_HEADER = "XSRF-TOKEN"; 47 | protected static Regex AntiforgeryFormFieldRegex = new Regex(@"\"); 48 | 49 | protected SetCookieHeaderValue _authenticationCookie; 50 | protected SetCookieHeaderValue _antiforgeryCookie; 51 | protected string _antiforgeryToken; 52 | 53 | public async Task EnsureAntiforgeryToken() 54 | { 55 | if (_antiforgeryToken != null) return _antiforgeryToken; 56 | 57 | var response = await _client.GetAsync("/Account/Login"); 58 | response.EnsureSuccessStatusCode(); 59 | if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable values)) 60 | { 61 | _antiforgeryCookie = SetCookieHeaderValue.ParseList(values.ToList()).SingleOrDefault(c => c.Name.StartsWith(ANTIFORGERY_COOKIE, StringComparison.InvariantCultureIgnoreCase)); 62 | } 63 | Assert.NotNull(_antiforgeryCookie); 64 | _client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(_antiforgeryCookie.Name, _antiforgeryCookie.Value).ToString()); 65 | 66 | var responseHtml = await response.Content.ReadAsStringAsync(); 67 | var match = AntiforgeryFormFieldRegex.Match(responseHtml); 68 | _antiforgeryToken = match.Success ? match.Groups[1].Captures[0].Value : null; 69 | Assert.NotNull(_antiforgeryToken); 70 | 71 | return _antiforgeryToken; 72 | } 73 | 74 | public async Task> EnsureAntiforgeryTokenForm(Dictionary formData = null) 75 | { 76 | if (formData == null) formData = new Dictionary(); 77 | 78 | formData.Add(ANTIFORGERY_TOKEN_FORM, await EnsureAntiforgeryToken()); 79 | return formData; 80 | } 81 | 82 | public async Task EnsureAntiforgeryTokenHeader() 83 | { 84 | _client.DefaultRequestHeaders.Add(ANTIFORGERTY_TOKEN_HEADER, await EnsureAntiforgeryToken()); 85 | } 86 | 87 | public async Task EnsureAuthenticationCookie() 88 | { 89 | if (_authenticationCookie != null) return; 90 | 91 | var formData = await EnsureAntiforgeryTokenForm(new Dictionary 92 | { 93 | { "Email", PredefinedData.Profiles[0].Email }, 94 | { "Password", PredefinedData.Password } 95 | }); 96 | var response = await _client.PostAsync("/Account/Login", new FormUrlEncodedContent(formData)); 97 | Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); 98 | 99 | if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable values)) 100 | { 101 | _authenticationCookie = SetCookieHeaderValue.ParseList(values.ToList()).SingleOrDefault(c => c.Name.StartsWith(AUTHENTICATION_COOKIE, StringComparison.InvariantCultureIgnoreCase)); 102 | } 103 | Assert.NotNull(_authenticationCookie); 104 | _client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(_authenticationCookie.Name, _authenticationCookie.Value).ToString()); 105 | 106 | // The current pair of antiforgery cookie-token is not valid anymore 107 | // Since the tokens are generated based on the authenticated user! 108 | // We need a new token after authentication (The cookie can stay the same) 109 | _antiforgeryToken = null; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /BlogPlayground.IntegrationTest/TestStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using BlogPlayground.Data; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using BlogPlayground.IntegrationTest.Data; 12 | 13 | namespace BlogPlayground.IntegrationTest 14 | { 15 | public class TestStartup : Startup 16 | { 17 | public TestStartup(IConfiguration configuration) : base(configuration) 18 | { 19 | } 20 | 21 | public override void ConfigureDatabase(IServiceCollection services) 22 | { 23 | // Replace default database connection with In-Memory database 24 | services.AddDbContext(options => 25 | options.UseInMemoryDatabase("blogplayground_test_db")); 26 | 27 | // Register the database seeder 28 | services.AddTransient(); 29 | } 30 | 31 | public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 32 | { 33 | // Perform all the configuration in the base class 34 | base.Configure(app, env, loggerFactory); 35 | 36 | // Now seed the database 37 | using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) 38 | { 39 | var seeder = serviceScope.ServiceProvider.GetService(); 40 | seeder.Seed().Wait(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BlogPlayground.Test/BlogPlayground.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /BlogPlayground.Test/Controllers/ArticlesApiControllerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using Moq; 4 | using BlogPlayground.Data; 5 | using BlogPlayground.Services; 6 | using BlogPlayground.Controllers; 7 | using BlogPlayground.Models; 8 | using Microsoft.AspNetCore.Mvc; 9 | using System.Collections.Generic; 10 | using System.Threading.Tasks; 11 | 12 | namespace BlogPlayground.Test 13 | { 14 | public class ArticlesApiControllerTest 15 | { 16 | private Mock articlesRepoMock; 17 | private Mock requestUserProviderMock; 18 | private ArticlesApiController controller; 19 | 20 | public ArticlesApiControllerTest() 21 | { 22 | articlesRepoMock = new Mock(); 23 | requestUserProviderMock = new Mock(); 24 | controller = new ArticlesApiController(articlesRepoMock.Object, requestUserProviderMock.Object); 25 | } 26 | 27 | [Fact] 28 | public async Task GetArticlesTest_RetursArticlesList() 29 | { 30 | // Arrange 31 | var mockArticlesList = new List
    32 | { 33 | new Article { Title = "mock article 1" }, 34 | new Article { Title = "mock article 2" } 35 | }; 36 | articlesRepoMock.Setup(repo => repo.GetAll()).Returns(Task.FromResult(mockArticlesList)); 37 | 38 | // Act 39 | var result = await controller.GetArticles(); 40 | 41 | // Assert 42 | Assert.Equal(mockArticlesList, result); 43 | } 44 | 45 | [Fact] 46 | public async Task GetArticleTest_ReturnsNotFound_WhenArticleDoesNorExists() 47 | { 48 | // Arrange 49 | var mockId = 42; 50 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult
    (null)); 51 | 52 | // Act 53 | var result = await controller.GetArticle(mockId); 54 | 55 | // Assert 56 | var viewResult = Assert.IsType(result); 57 | } 58 | 59 | [Fact] 60 | public async Task GetArticleTest_ReturnsArticle_WhenArticleExists() 61 | { 62 | // Arrange 63 | var mockId = 42; 64 | var mockArticle = new Article { Title = "mock article" }; 65 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult(mockArticle)); 66 | 67 | // Act 68 | var result = await controller.GetArticle(mockId); 69 | 70 | // Assert 71 | var actionResult = Assert.IsType(result); 72 | Assert.Equal(mockArticle, actionResult.Value); 73 | } 74 | 75 | [Fact] 76 | public async Task AddArticleTest_ReturnsBadRequest_WhenModelStateIsInvalid() 77 | { 78 | // Arrange 79 | var mockArticle = new Article { Title = "mock article" }; 80 | controller.ModelState.AddModelError("Description", "This field is required"); 81 | 82 | // Act 83 | var result = await controller.AddArticle(mockArticle); 84 | 85 | // Assert 86 | var actionResult = Assert.IsType(result); 87 | Assert.Equal(new SerializableError(controller.ModelState), actionResult.Value); 88 | } 89 | 90 | [Fact] 91 | public async Task AddArticleTest_ReturnsArticleSuccessfullyAdded() 92 | { 93 | // Arrange 94 | var mockArticle = new Article { Title = "mock article" }; 95 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 96 | 97 | // Act 98 | var result = await controller.AddArticle(mockArticle); 99 | 100 | // Assert 101 | articlesRepoMock.Verify(repo => repo.Add(mockArticle)); 102 | var actionResult = Assert.IsType(result); 103 | Assert.Equal(mockArticle, actionResult.Value); 104 | } 105 | 106 | [Fact] 107 | public async Task AddArticleTest_SetsAuthorId_BeforeAddingArticleToRepository() 108 | { 109 | // Arrange 110 | var mockArticle = new Article { Title = "mock article" }; 111 | var mockAuthorId = "mockAuthorId"; 112 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 113 | requestUserProviderMock.Setup(provider => provider.GetUserId()).Returns(mockAuthorId); 114 | 115 | // Act 116 | var result = await controller.AddArticle(mockArticle); 117 | 118 | // Assert 119 | articlesRepoMock.Verify(repo => 120 | repo.Add(It.Is
    (article => 121 | article == mockArticle 122 | && article.AuthorId == mockAuthorId))); 123 | } 124 | 125 | [Fact] 126 | public async Task AddArticleTest_SetsCreatedDate_BeforeAddingArticleToRepository() 127 | { 128 | // Arrange 129 | var mockArticle = new Article { Title = "mock article" }; 130 | var startTime = DateTime.Now; 131 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 132 | 133 | // Act 134 | var result = await controller.AddArticle(mockArticle); 135 | var endTime = DateTime.Now; 136 | 137 | // Assert 138 | articlesRepoMock.Verify(repo => 139 | repo.Add(It.Is
    (article => 140 | article == mockArticle 141 | && article.CreatedDate >= startTime 142 | && article.CreatedDate <= endTime))); 143 | } 144 | 145 | [Fact] 146 | public async Task DeleteArticleTest_ReturnsNotFound_WhenArticleDoesNorExists() 147 | { 148 | // Arrange 149 | var mockId = 42; 150 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult
    (null)); 151 | 152 | // Act 153 | var result = await controller.DeleteArticle(mockId); 154 | 155 | // Assert 156 | var viewResult = Assert.IsType(result); 157 | } 158 | 159 | [Fact] 160 | public async Task DeleteArticleTest_ReturnsSuccessCode_AfterRemovingArticleFromRepository() 161 | { 162 | // Arrange 163 | var mockId = 42; 164 | var mockArticle = new Article { Title = "mock article" }; 165 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult(mockArticle)); 166 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 167 | 168 | // Act 169 | var result = await controller.DeleteArticle(mockId); 170 | 171 | // Assert 172 | articlesRepoMock.Verify(repo => repo.Remove(mockArticle)); 173 | Assert.IsType(result); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /BlogPlayground.Test/Controllers/ArticlesControllerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using Moq; 4 | using BlogPlayground.Data; 5 | using BlogPlayground.Services; 6 | using BlogPlayground.Controllers; 7 | using System.Collections.Generic; 8 | using BlogPlayground.Models; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Mvc; 11 | using System.Linq; 12 | 13 | namespace BlogPlayground.Test 14 | { 15 | public class ArticlesControllerTest 16 | { 17 | private Mock articlesRepoMock; 18 | private Mock requestUserProviderMock; 19 | private ArticlesController controller; 20 | 21 | public ArticlesControllerTest() 22 | { 23 | articlesRepoMock = new Mock(); 24 | requestUserProviderMock = new Mock(); 25 | controller = new ArticlesController(articlesRepoMock.Object, requestUserProviderMock.Object); 26 | } 27 | 28 | [Fact] 29 | public async Task IndexTest_ReturnsViewWithArticlesList() 30 | { 31 | // Arrange 32 | var mockArticlesList = new List
    33 | { 34 | new Article { Title = "mock article 1" }, 35 | new Article { Title = "mock article 2" } 36 | }; 37 | articlesRepoMock.Setup(repo => repo.GetAll()).Returns(Task.FromResult(mockArticlesList)); 38 | 39 | // Act 40 | var result = await controller.Index(); 41 | 42 | // Assert 43 | var viewResult = Assert.IsType(result); 44 | var model = Assert.IsAssignableFrom>(viewResult.ViewData.Model); 45 | Assert.Equal(2, model.Count()); 46 | } 47 | 48 | [Fact] 49 | public async Task DetailsTest_ReturnsNotFound_WhenNoIdProvided() 50 | { 51 | // Act 52 | var result = await controller.Details(null); 53 | 54 | // Assert 55 | var viewResult = Assert.IsType(result); 56 | } 57 | 58 | [Fact] 59 | public async Task DetailsTest_ReturnsNotFound_WhenArticleDoesNorExists() 60 | { 61 | // Arrange 62 | var mockId = 42; 63 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult
    (null)); 64 | 65 | // Act 66 | var result = await controller.Details(mockId); 67 | 68 | // Assert 69 | var viewResult = Assert.IsType(result); 70 | } 71 | 72 | [Fact] 73 | public async Task DetailsTest_ReturnsDetailsView_WhenArticleExists() 74 | { 75 | // Arrange 76 | var mockId = 42; 77 | var mockArticle = new Article { Title = "mock article" }; 78 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult(mockArticle)); 79 | 80 | // Act 81 | var result = await controller.Details(mockId); 82 | 83 | // Assert 84 | var viewResult = Assert.IsType(result); 85 | Assert.Equal(mockArticle, viewResult.ViewData.Model); 86 | } 87 | 88 | [Fact] 89 | public void CreateTest_Get_ReturnsView() 90 | { 91 | // Act 92 | var result = controller.Create(); 93 | 94 | // Assert 95 | var viewResult = Assert.IsType(result); 96 | } 97 | 98 | [Fact] 99 | public async Task CreateTest_Post_ReturnsCreateView_WhenModelStateIsInvalid() 100 | { 101 | // Arrange 102 | var mockArticle = new Article { Title = "mock article" }; 103 | controller.ModelState.AddModelError("Description", "This field is required"); 104 | 105 | // Act 106 | var result = await controller.Create(mockArticle); 107 | 108 | // Assert 109 | var viewResult = Assert.IsType(result); 110 | Assert.Equal(mockArticle, viewResult.ViewData.Model); 111 | } 112 | 113 | [Fact] 114 | public async Task CreateTest_Post_AddsArticleToRepository_AndRedirectsToIndex() 115 | { 116 | // Arrange 117 | var mockArticle = new Article { Title = "mock article" }; 118 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 119 | 120 | // Act 121 | var result = await controller.Create(mockArticle); 122 | 123 | // Assert 124 | articlesRepoMock.Verify(repo => repo.Add(mockArticle)); 125 | var viewResult = Assert.IsType(result); 126 | Assert.Equal("Index", viewResult.ActionName); 127 | } 128 | 129 | [Fact] 130 | public async Task CreateTest_Post_SetsAuthorId_BeforeAddingArticleToRepository() 131 | { 132 | // Arrange 133 | var mockArticle = new Article { Title = "mock article" }; 134 | var mockAuthorId = "mockAuthorId"; 135 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 136 | requestUserProviderMock.Setup(provider => provider.GetUserId()).Returns(mockAuthorId); 137 | 138 | // Act 139 | var result = await controller.Create(mockArticle); 140 | 141 | // Assert 142 | articlesRepoMock.Verify(repo => 143 | repo.Add(It.Is
    (article => 144 | article == mockArticle 145 | && article.AuthorId == mockAuthorId))); 146 | } 147 | 148 | [Fact] 149 | public async Task CreateTest_Post_SetsCreatedDate_BeforeAddingArticleToRepository() 150 | { 151 | // Arrange 152 | var mockArticle = new Article { Title = "mock article" }; 153 | var startTime = DateTime.Now; 154 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 155 | 156 | // Act 157 | var result = await controller.Create(mockArticle); 158 | var endTime = DateTime.Now; 159 | 160 | // Assert 161 | articlesRepoMock.Verify(repo => 162 | repo.Add(It.Is
    (article => 163 | article == mockArticle 164 | && article.CreatedDate >= startTime 165 | && article.CreatedDate <= endTime))); 166 | } 167 | 168 | [Fact] 169 | public async Task DeleteTest_ReturnsNotFound_WhenNoIdProvided() 170 | { 171 | // Act 172 | var result = await controller.Delete(null); 173 | 174 | // Assert 175 | var viewResult = Assert.IsType(result); 176 | } 177 | 178 | [Fact] 179 | public async Task DeleteTest_ReturnsNotFound_WhenArticleDoesNorExists() 180 | { 181 | // Arrange 182 | var mockId = 42; 183 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult
    (null)); 184 | 185 | // Act 186 | var result = await controller.Delete(mockId); 187 | 188 | // Assert 189 | var viewResult = Assert.IsType(result); 190 | } 191 | 192 | [Fact] 193 | public async Task DeleteTest_ReturnsDeleteView_WhenArticleExists() 194 | { 195 | // Arrange 196 | var mockId = 42; 197 | var mockArticle = new Article { Title = "mock article" }; 198 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult(mockArticle)); 199 | 200 | // Act 201 | var result = await controller.Delete(mockId); 202 | 203 | // Assert 204 | var viewResult = Assert.IsType(result); 205 | Assert.Equal(mockArticle, viewResult.ViewData.Model); 206 | } 207 | 208 | [Fact] 209 | public async Task DeleteConfirmedTest_RemovesArticleFromRepository_AndRedirectsToIndex() 210 | { 211 | // Arrange 212 | var mockId = 42; 213 | var mockArticle = new Article { Title = "mock article" }; 214 | articlesRepoMock.Setup(repo => repo.GetOne(mockId)).Returns(Task.FromResult(mockArticle)); 215 | articlesRepoMock.Setup(repo => repo.SaveChanges()).Returns(Task.CompletedTask); 216 | 217 | // Act 218 | var result = await controller.DeleteConfirmed(mockId); 219 | 220 | // Assert 221 | articlesRepoMock.Verify(repo => repo.Remove(mockArticle)); 222 | var viewResult = Assert.IsType(result); 223 | Assert.Equal("Index", viewResult.ActionName); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /BlogPlayground.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2009 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlogPlayground", "BlogPlayground\BlogPlayground.csproj", "{0723636E-4E08-41FA-AF4A-5A0FF9EFD926}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlogPlayground.Test", "BlogPlayground.Test\BlogPlayground.Test.csproj", "{B2C5E0EA-80B2-4128-B67F-0CBFB0D9BB1F}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlogPlayground.IntegrationTest", "BlogPlayground.IntegrationTest\BlogPlayground.IntegrationTest.csproj", "{AA518DB8-5510-4C6A-B326-A119E72871AE}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlogPlayground.E2ETest", "BlogPlayground.E2ETest\BlogPlayground.E2ETest.csproj", "{1E5151B3-87F3-4843-BE91-1F637279197E}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {0723636E-4E08-41FA-AF4A-5A0FF9EFD926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {0723636E-4E08-41FA-AF4A-5A0FF9EFD926}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {0723636E-4E08-41FA-AF4A-5A0FF9EFD926}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {0723636E-4E08-41FA-AF4A-5A0FF9EFD926}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {B2C5E0EA-80B2-4128-B67F-0CBFB0D9BB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {B2C5E0EA-80B2-4128-B67F-0CBFB0D9BB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {B2C5E0EA-80B2-4128-B67F-0CBFB0D9BB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {B2C5E0EA-80B2-4128-B67F-0CBFB0D9BB1F}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {AA518DB8-5510-4C6A-B326-A119E72871AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {AA518DB8-5510-4C6A-B326-A119E72871AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {AA518DB8-5510-4C6A-B326-A119E72871AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {AA518DB8-5510-4C6A-B326-A119E72871AE}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {1E5151B3-87F3-4843-BE91-1F637279197E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {1E5151B3-87F3-4843-BE91-1F637279197E}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {1E5151B3-87F3-4843-BE91-1F637279197E}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {1E5151B3-87F3-4843-BE91-1F637279197E}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {1572A51A-994A-4748-917D-95D14E2C6194} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /BlogPlayground/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "wwwroot/lib" 3 | } 4 | -------------------------------------------------------------------------------- /BlogPlayground/BlogPlayground.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | true 6 | BlogPlayground 7 | Exe 8 | BlogPlayground 9 | aspnet-BlogPlayground-2f5f8325-05ef-496b-98a4-abbfaf143aac 10 | $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /BlogPlayground/Controllers/ArticlesApiController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using BlogPlayground.Data; 8 | using BlogPlayground.Services; 9 | using BlogPlayground.Models; 10 | using Microsoft.AspNetCore.Authorization; 11 | 12 | namespace BlogPlayground.Controllers 13 | { 14 | [Produces("application/json")] 15 | [Route("api/articles")] 16 | public class ArticlesApiController : Controller 17 | { 18 | private readonly IArticlesRepository _articlesRepository; 19 | private readonly IRequestUserProvider _requestUserProvider; 20 | 21 | public ArticlesApiController(IArticlesRepository articlesRepository, IRequestUserProvider requestUserProvider) 22 | { 23 | _articlesRepository = articlesRepository; 24 | _requestUserProvider = requestUserProvider; 25 | } 26 | 27 | [HttpGet()] 28 | public async Task> GetArticles() 29 | { 30 | return await _articlesRepository.GetAll(); 31 | } 32 | 33 | [HttpGet("{id}")] 34 | public async Task GetArticle(int id) 35 | { 36 | var article = await _articlesRepository.GetOne(id); 37 | if (article == null) return NotFound(); 38 | 39 | return Ok(article); 40 | } 41 | 42 | [HttpPost()] 43 | [ValidateAntiForgeryToken] 44 | [Authorize] 45 | public async Task AddArticle([FromBody]Article article) 46 | { 47 | if (!ModelState.IsValid) return BadRequest(ModelState); 48 | 49 | article.AuthorId = _requestUserProvider.GetUserId(); 50 | article.CreatedDate = DateTime.Now; 51 | _articlesRepository.Add(article); 52 | await _articlesRepository.SaveChanges(); 53 | return Ok(article); 54 | } 55 | 56 | [HttpDelete("{id}")] 57 | [ValidateAntiForgeryToken] 58 | [Authorize] 59 | public async Task DeleteArticle(int id) 60 | { 61 | var article = await _articlesRepository.GetOne(id); 62 | if (article == null) return NotFound(); 63 | 64 | _articlesRepository.Remove(article); 65 | await _articlesRepository.SaveChanges(); 66 | 67 | return NoContent(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /BlogPlayground/Controllers/ArticlesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Rendering; 7 | using Microsoft.EntityFrameworkCore; 8 | using BlogPlayground.Data; 9 | using BlogPlayground.Models; 10 | using Microsoft.AspNetCore.Identity; 11 | using Microsoft.AspNetCore.Authorization; 12 | using BlogPlayground.Services; 13 | 14 | namespace BlogPlayground.Controllers 15 | { 16 | public class ArticlesController : Controller 17 | { 18 | private readonly IArticlesRepository _articlesRepository; 19 | private readonly IRequestUserProvider _requestUserProvider; 20 | 21 | public ArticlesController(IArticlesRepository articlesRepository, IRequestUserProvider requestUserProvider) 22 | { 23 | _articlesRepository = articlesRepository; 24 | _requestUserProvider = requestUserProvider; 25 | } 26 | 27 | // GET: Articles 28 | public async Task Index() 29 | { 30 | return View(await _articlesRepository.GetAll()); 31 | } 32 | 33 | // GET: Articles/Details/5 34 | public async Task Details(int? id) 35 | { 36 | if (id == null) 37 | { 38 | return NotFound(); 39 | } 40 | 41 | var article = await _articlesRepository.GetOne(id.Value); 42 | if (article == null) 43 | { 44 | return NotFound(); 45 | } 46 | 47 | return View(article); 48 | } 49 | 50 | // GET: Articles/Create 51 | [Authorize] 52 | public IActionResult Create() 53 | { 54 | return View(); 55 | } 56 | 57 | // POST: Articles/Create 58 | // To protect from overposting attacks, please enable the specific properties you want to bind to, for 59 | // more details see http://go.microsoft.com/fwlink/?LinkId=317598. 60 | [HttpPost] 61 | [ValidateAntiForgeryToken] 62 | [Authorize] 63 | public async Task Create([Bind("Title, Abstract,Contents")] Article article) 64 | { 65 | if (ModelState.IsValid) 66 | { 67 | article.AuthorId = _requestUserProvider.GetUserId(); 68 | article.CreatedDate = DateTime.Now; 69 | _articlesRepository.Add(article); 70 | await _articlesRepository.SaveChanges(); 71 | return RedirectToAction("Index"); 72 | } 73 | return View(article); 74 | } 75 | 76 | // GET: Articles/Delete/5 77 | [Authorize] 78 | public async Task Delete(int? id) 79 | { 80 | if (id == null) 81 | { 82 | return NotFound(); 83 | } 84 | 85 | var article = await _articlesRepository.GetOne(id.Value); 86 | if (article == null) 87 | { 88 | return NotFound(); 89 | } 90 | 91 | return View(article); 92 | } 93 | 94 | // POST: Articles/Delete/5 95 | [HttpPost, ActionName("Delete")] 96 | [ValidateAntiForgeryToken] 97 | [Authorize] 98 | public async Task DeleteConfirmed(int id) 99 | { 100 | var article = await _articlesRepository.GetOne(id); 101 | _articlesRepository.Remove(article); 102 | await _articlesRepository.SaveChanges(); 103 | return RedirectToAction("Index"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /BlogPlayground/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace BlogPlayground.Controllers 8 | { 9 | public class HomeController : Controller 10 | { 11 | public IActionResult Index() 12 | { 13 | return View(); 14 | } 15 | 16 | public IActionResult About() 17 | { 18 | ViewData["Message"] = "Your application description page."; 19 | 20 | return View(); 21 | } 22 | 23 | public IActionResult Contact() 24 | { 25 | ViewData["Message"] = "Your contact page."; 26 | 27 | return View(); 28 | } 29 | 30 | public IActionResult Error() 31 | { 32 | return View(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BlogPlayground/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore; 7 | using BlogPlayground.Models; 8 | 9 | namespace BlogPlayground.Data 10 | { 11 | public class ApplicationDbContext : IdentityDbContext 12 | { 13 | public ApplicationDbContext(DbContextOptions options) 14 | : base(options) 15 | { 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder builder) 19 | { 20 | base.OnModelCreating(builder); 21 | // Customize the ASP.NET Identity model and override the defaults if needed. 22 | // For example, you can rename the ASP.NET Identity table names and more. 23 | // Add your customizations after calling base.OnModelCreating(builder); 24 | } 25 | 26 | public DbSet
    Article { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BlogPlayground/Data/ArticlesRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using BlogPlayground.Models; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace BlogPlayground.Data 9 | { 10 | public class ArticlesRepository : IArticlesRepository 11 | { 12 | private readonly ApplicationDbContext _context; 13 | 14 | public ArticlesRepository(ApplicationDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public Task> GetAll() => 20 | _context.Article.Include(a => a.Author).OrderByDescending(a => a.CreatedDate).ToListAsync(); 21 | 22 | public Task> GetLatest(int num) => 23 | _context.Article.Include(a => a.Author).OrderByDescending(a => a.CreatedDate).Take(num).ToListAsync(); 24 | 25 | public Task
    GetOne(int id) => 26 | _context.Article.Include(a => a.Author).SingleOrDefaultAsync(m => m.ArticleId == id); 27 | 28 | public void Add(Article article) => 29 | _context.Article.Add(article); 30 | 31 | public void Remove(Article article) => 32 | _context.Article.Remove(article); 33 | 34 | public Task SaveChanges() => 35 | _context.SaveChangesAsync(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BlogPlayground/Data/IArticlesRepository.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Data 8 | { 9 | public interface IArticlesRepository 10 | { 11 | Task> GetAll(); 12 | Task> GetLatest(int num); 13 | Task
    GetOne(int id); 14 | void Add(Article article); 15 | void Remove(Article article); 16 | Task SaveChanges(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BlogPlayground/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.EntityFrameworkCore.Metadata; 8 | using Microsoft.EntityFrameworkCore.Migrations; 9 | 10 | namespace BlogPlayground.Data.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("00000000000000_CreateIdentitySchema")] 14 | partial class CreateIdentitySchema 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "1.0.0-rc3") 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => 23 | { 24 | b.Property("Id"); 25 | 26 | b.Property("ConcurrencyStamp") 27 | .IsConcurrencyToken(); 28 | 29 | b.Property("Name") 30 | .HasAnnotation("MaxLength", 256); 31 | 32 | b.Property("NormalizedName") 33 | .HasAnnotation("MaxLength", 256); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.HasIndex("NormalizedName") 38 | .HasName("RoleNameIndex"); 39 | 40 | b.ToTable("AspNetRoles"); 41 | }); 42 | 43 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => 44 | { 45 | b.Property("Id") 46 | .ValueGeneratedOnAdd(); 47 | 48 | b.Property("ClaimType"); 49 | 50 | b.Property("ClaimValue"); 51 | 52 | b.Property("RoleId") 53 | .IsRequired(); 54 | 55 | b.HasKey("Id"); 56 | 57 | b.HasIndex("RoleId"); 58 | 59 | b.ToTable("AspNetRoleClaims"); 60 | }); 61 | 62 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => 63 | { 64 | b.Property("Id") 65 | .ValueGeneratedOnAdd(); 66 | 67 | b.Property("ClaimType"); 68 | 69 | b.Property("ClaimValue"); 70 | 71 | b.Property("UserId") 72 | .IsRequired(); 73 | 74 | b.HasKey("Id"); 75 | 76 | b.HasIndex("UserId"); 77 | 78 | b.ToTable("AspNetUserClaims"); 79 | }); 80 | 81 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => 82 | { 83 | b.Property("LoginProvider"); 84 | 85 | b.Property("ProviderKey"); 86 | 87 | b.Property("ProviderDisplayName"); 88 | 89 | b.Property("UserId") 90 | .IsRequired(); 91 | 92 | b.HasKey("LoginProvider", "ProviderKey"); 93 | 94 | b.HasIndex("UserId"); 95 | 96 | b.ToTable("AspNetUserLogins"); 97 | }); 98 | 99 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => 100 | { 101 | b.Property("UserId"); 102 | 103 | b.Property("RoleId"); 104 | 105 | b.HasKey("UserId", "RoleId"); 106 | 107 | b.HasIndex("RoleId"); 108 | 109 | b.HasIndex("UserId"); 110 | 111 | b.ToTable("AspNetUserRoles"); 112 | }); 113 | 114 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => 115 | { 116 | b.Property("UserId"); 117 | 118 | b.Property("LoginProvider"); 119 | 120 | b.Property("Name"); 121 | 122 | b.Property("Value"); 123 | 124 | b.HasKey("UserId", "LoginProvider", "Name"); 125 | 126 | b.ToTable("AspNetUserTokens"); 127 | }); 128 | 129 | modelBuilder.Entity("BlogPlayground.Models.ApplicationUser", b => 130 | { 131 | b.Property("Id"); 132 | 133 | b.Property("AccessFailedCount"); 134 | 135 | b.Property("ConcurrencyStamp") 136 | .IsConcurrencyToken(); 137 | 138 | b.Property("Email") 139 | .HasAnnotation("MaxLength", 256); 140 | 141 | b.Property("EmailConfirmed"); 142 | 143 | b.Property("LockoutEnabled"); 144 | 145 | b.Property("LockoutEnd"); 146 | 147 | b.Property("NormalizedEmail") 148 | .HasAnnotation("MaxLength", 256); 149 | 150 | b.Property("NormalizedUserName") 151 | .HasAnnotation("MaxLength", 256); 152 | 153 | b.Property("PasswordHash"); 154 | 155 | b.Property("PhoneNumber"); 156 | 157 | b.Property("PhoneNumberConfirmed"); 158 | 159 | b.Property("SecurityStamp"); 160 | 161 | b.Property("TwoFactorEnabled"); 162 | 163 | b.Property("UserName") 164 | .HasAnnotation("MaxLength", 256); 165 | 166 | b.HasKey("Id"); 167 | 168 | b.HasIndex("NormalizedEmail") 169 | .HasName("EmailIndex"); 170 | 171 | b.HasIndex("NormalizedUserName") 172 | .IsUnique() 173 | .HasName("UserNameIndex"); 174 | 175 | b.ToTable("AspNetUsers"); 176 | }); 177 | 178 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => 179 | { 180 | b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") 181 | .WithMany("Claims") 182 | .HasForeignKey("RoleId") 183 | .OnDelete(DeleteBehavior.Cascade); 184 | }); 185 | 186 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => 187 | { 188 | b.HasOne("BlogPlayground.Models.ApplicationUser") 189 | .WithMany("Claims") 190 | .HasForeignKey("UserId") 191 | .OnDelete(DeleteBehavior.Cascade); 192 | }); 193 | 194 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => 195 | { 196 | b.HasOne("BlogPlayground.Models.ApplicationUser") 197 | .WithMany("Logins") 198 | .HasForeignKey("UserId") 199 | .OnDelete(DeleteBehavior.Cascade); 200 | }); 201 | 202 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => 203 | { 204 | b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") 205 | .WithMany("Users") 206 | .HasForeignKey("RoleId") 207 | .OnDelete(DeleteBehavior.Cascade); 208 | 209 | b.HasOne("BlogPlayground.Models.ApplicationUser") 210 | .WithMany("Roles") 211 | .HasForeignKey("UserId") 212 | .OnDelete(DeleteBehavior.Cascade); 213 | }); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /BlogPlayground/Data/Migrations/20160910210414_update-profile.Designer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using BlogPlayground.Data; 7 | 8 | namespace BlogPlayground.Data.Migrations 9 | { 10 | [DbContext(typeof(ApplicationDbContext))] 11 | [Migration("20160910210414_update-profile")] 12 | partial class updateprofile 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "1.0.0-rtm-21431") 18 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 19 | 20 | modelBuilder.Entity("BlogPlayground.Models.ApplicationUser", b => 21 | { 22 | b.Property("Id"); 23 | 24 | b.Property("AccessFailedCount"); 25 | 26 | b.Property("ConcurrencyStamp") 27 | .IsConcurrencyToken(); 28 | 29 | b.Property("Email") 30 | .HasAnnotation("MaxLength", 256); 31 | 32 | b.Property("EmailConfirmed"); 33 | 34 | b.Property("FullName"); 35 | 36 | b.Property("LockoutEnabled"); 37 | 38 | b.Property("LockoutEnd"); 39 | 40 | b.Property("NormalizedEmail") 41 | .HasAnnotation("MaxLength", 256); 42 | 43 | b.Property("NormalizedUserName") 44 | .HasAnnotation("MaxLength", 256); 45 | 46 | b.Property("PasswordHash"); 47 | 48 | b.Property("PhoneNumber"); 49 | 50 | b.Property("PhoneNumberConfirmed"); 51 | 52 | b.Property("Picture"); 53 | 54 | b.Property("SecurityStamp"); 55 | 56 | b.Property("TwoFactorEnabled"); 57 | 58 | b.Property("UserName") 59 | .HasAnnotation("MaxLength", 256); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.HasIndex("NormalizedEmail") 64 | .HasName("EmailIndex"); 65 | 66 | b.HasIndex("NormalizedUserName") 67 | .IsUnique() 68 | .HasName("UserNameIndex"); 69 | 70 | b.ToTable("AspNetUsers"); 71 | }); 72 | 73 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => 74 | { 75 | b.Property("Id"); 76 | 77 | b.Property("ConcurrencyStamp") 78 | .IsConcurrencyToken(); 79 | 80 | b.Property("Name") 81 | .HasAnnotation("MaxLength", 256); 82 | 83 | b.Property("NormalizedName") 84 | .HasAnnotation("MaxLength", 256); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.HasIndex("NormalizedName") 89 | .HasName("RoleNameIndex"); 90 | 91 | b.ToTable("AspNetRoles"); 92 | }); 93 | 94 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => 95 | { 96 | b.Property("Id") 97 | .ValueGeneratedOnAdd(); 98 | 99 | b.Property("ClaimType"); 100 | 101 | b.Property("ClaimValue"); 102 | 103 | b.Property("RoleId") 104 | .IsRequired(); 105 | 106 | b.HasKey("Id"); 107 | 108 | b.HasIndex("RoleId"); 109 | 110 | b.ToTable("AspNetRoleClaims"); 111 | }); 112 | 113 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => 114 | { 115 | b.Property("Id") 116 | .ValueGeneratedOnAdd(); 117 | 118 | b.Property("ClaimType"); 119 | 120 | b.Property("ClaimValue"); 121 | 122 | b.Property("UserId") 123 | .IsRequired(); 124 | 125 | b.HasKey("Id"); 126 | 127 | b.HasIndex("UserId"); 128 | 129 | b.ToTable("AspNetUserClaims"); 130 | }); 131 | 132 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => 133 | { 134 | b.Property("LoginProvider"); 135 | 136 | b.Property("ProviderKey"); 137 | 138 | b.Property("ProviderDisplayName"); 139 | 140 | b.Property("UserId") 141 | .IsRequired(); 142 | 143 | b.HasKey("LoginProvider", "ProviderKey"); 144 | 145 | b.HasIndex("UserId"); 146 | 147 | b.ToTable("AspNetUserLogins"); 148 | }); 149 | 150 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => 151 | { 152 | b.Property("UserId"); 153 | 154 | b.Property("RoleId"); 155 | 156 | b.HasKey("UserId", "RoleId"); 157 | 158 | b.HasIndex("RoleId"); 159 | 160 | b.HasIndex("UserId"); 161 | 162 | b.ToTable("AspNetUserRoles"); 163 | }); 164 | 165 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => 166 | { 167 | b.Property("UserId"); 168 | 169 | b.Property("LoginProvider"); 170 | 171 | b.Property("Name"); 172 | 173 | b.Property("Value"); 174 | 175 | b.HasKey("UserId", "LoginProvider", "Name"); 176 | 177 | b.ToTable("AspNetUserTokens"); 178 | }); 179 | 180 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => 181 | { 182 | b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") 183 | .WithMany("Claims") 184 | .HasForeignKey("RoleId") 185 | .OnDelete(DeleteBehavior.Cascade); 186 | }); 187 | 188 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => 189 | { 190 | b.HasOne("BlogPlayground.Models.ApplicationUser") 191 | .WithMany("Claims") 192 | .HasForeignKey("UserId") 193 | .OnDelete(DeleteBehavior.Cascade); 194 | }); 195 | 196 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => 197 | { 198 | b.HasOne("BlogPlayground.Models.ApplicationUser") 199 | .WithMany("Logins") 200 | .HasForeignKey("UserId") 201 | .OnDelete(DeleteBehavior.Cascade); 202 | }); 203 | 204 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => 205 | { 206 | b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") 207 | .WithMany("Users") 208 | .HasForeignKey("RoleId") 209 | .OnDelete(DeleteBehavior.Cascade); 210 | 211 | b.HasOne("BlogPlayground.Models.ApplicationUser") 212 | .WithMany("Roles") 213 | .HasForeignKey("UserId") 214 | .OnDelete(DeleteBehavior.Cascade); 215 | }); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /BlogPlayground/Data/Migrations/20160910210414_update-profile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace BlogPlayground.Data.Migrations 6 | { 7 | public partial class updateprofile : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "FullName", 13 | table: "AspNetUsers", 14 | nullable: true); 15 | 16 | migrationBuilder.AddColumn( 17 | name: "PictureUrl", 18 | table: "AspNetUsers", 19 | nullable: true); 20 | } 21 | 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "FullName", 26 | table: "AspNetUsers"); 27 | 28 | migrationBuilder.DropColumn( 29 | name: "PictureUrl", 30 | table: "AspNetUsers"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BlogPlayground/Data/Migrations/20160913175647_articles.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | 6 | namespace BlogPlayground.Data.Migrations 7 | { 8 | public partial class articles : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Article", 14 | columns: table => new 15 | { 16 | ArticleId = table.Column(nullable: false) 17 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 18 | Abstract = table.Column(nullable: false), 19 | AuthorId = table.Column(nullable: true), 20 | Contents = table.Column(nullable: false), 21 | CreatedDate = table.Column(nullable: false), 22 | Title = table.Column(nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_Article", x => x.ArticleId); 27 | table.ForeignKey( 28 | name: "FK_Article_AspNetUsers_AuthorId", 29 | column: x => x.AuthorId, 30 | principalTable: "AspNetUsers", 31 | principalColumn: "Id", 32 | onDelete: ReferentialAction.Restrict); 33 | }); 34 | 35 | migrationBuilder.CreateIndex( 36 | name: "IX_Article_AuthorId", 37 | table: "Article", 38 | column: "AuthorId"); 39 | } 40 | 41 | protected override void Down(MigrationBuilder migrationBuilder) 42 | { 43 | migrationBuilder.DropTable( 44 | name: "Article"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class ExternalLoginConfirmationViewModel 10 | { 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/ForgotPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class ForgotPasswordViewModel 10 | { 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class LoginViewModel 10 | { 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | 15 | [Required] 16 | [DataType(DataType.Password)] 17 | public string Password { get; set; } 18 | 19 | [Display(Name = "Remember me?")] 20 | public bool RememberMe { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/RegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class RegisterViewModel 10 | { 11 | [Required] 12 | [EmailAddress] 13 | [Display(Name = "Email")] 14 | public string Email { get; set; } 15 | 16 | [Required] 17 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 18 | [DataType(DataType.Password)] 19 | [Display(Name = "Password")] 20 | public string Password { get; set; } 21 | 22 | [DataType(DataType.Password)] 23 | [Display(Name = "Confirm password")] 24 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 25 | public string ConfirmPassword { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/ResetPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class ResetPasswordViewModel 10 | { 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | 15 | [Required] 16 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 17 | [DataType(DataType.Password)] 18 | public string Password { get; set; } 19 | 20 | [DataType(DataType.Password)] 21 | [Display(Name = "Confirm password")] 22 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 23 | public string ConfirmPassword { get; set; } 24 | 25 | public string Code { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/SendCodeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class SendCodeViewModel 10 | { 11 | public string SelectedProvider { get; set; } 12 | 13 | public ICollection Providers { get; set; } 14 | 15 | public string ReturnUrl { get; set; } 16 | 17 | public bool RememberMe { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlogPlayground/Models/AccountViewModels/VerifyCodeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.AccountViewModels 8 | { 9 | public class VerifyCodeViewModel 10 | { 11 | [Required] 12 | public string Provider { get; set; } 13 | 14 | [Required] 15 | public string Code { get; set; } 16 | 17 | public string ReturnUrl { get; set; } 18 | 19 | [Display(Name = "Remember this browser?")] 20 | public bool RememberBrowser { get; set; } 21 | 22 | [Display(Name = "Remember me?")] 23 | public bool RememberMe { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | namespace BlogPlayground.Models 8 | { 9 | // Add profile data for application users by adding properties to the ApplicationUser class 10 | public class ApplicationUser : IdentityUser 11 | { 12 | public string FullName { get; set; } 13 | public string PictureUrl { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlogPlayground/Models/Article.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models 8 | { 9 | public class Article 10 | { 11 | public int ArticleId { get; set; } 12 | [Required] 13 | public string Title { get; set; } 14 | [Required] 15 | public string Abstract { get; set; } 16 | [Required] 17 | public string Contents { get; set; } 18 | public DateTime CreatedDate { get; set; } 19 | 20 | //EF relationships should pick this relationship between Article and ApplicationUser tables: http://ef.readthedocs.io/en/latest/modeling/relationships.html 21 | public string AuthorId { get; set; } 22 | public virtual ApplicationUser Author { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/AddPhoneNumberViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class AddPhoneNumberViewModel 10 | { 11 | [Required] 12 | [Phone] 13 | [Display(Name = "Phone number")] 14 | public string PhoneNumber { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/ChangePasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class ChangePasswordViewModel 10 | { 11 | [Required] 12 | [DataType(DataType.Password)] 13 | [Display(Name = "Current password")] 14 | public string OldPassword { get; set; } 15 | 16 | [Required] 17 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 18 | [DataType(DataType.Password)] 19 | [Display(Name = "New password")] 20 | public string NewPassword { get; set; } 21 | 22 | [DataType(DataType.Password)] 23 | [Display(Name = "Confirm new password")] 24 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 25 | public string ConfirmPassword { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class ConfigureTwoFactorViewModel 10 | { 11 | public string SelectedProvider { get; set; } 12 | 13 | public ICollection Providers { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/FactorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace BlogPlayground.Models.ManageViewModels 7 | { 8 | public class FactorViewModel 9 | { 10 | public string Purpose { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class IndexViewModel 10 | { 11 | public bool HasPassword { get; set; } 12 | 13 | public IList Logins { get; set; } 14 | 15 | public string PhoneNumber { get; set; } 16 | 17 | public bool TwoFactor { get; set; } 18 | 19 | public bool BrowserRemembered { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/ManageLoginsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Authentication; 7 | 8 | namespace BlogPlayground.Models.ManageViewModels 9 | { 10 | public class ManageLoginsViewModel 11 | { 12 | public IList CurrentLogins { get; set; } 13 | 14 | public IList OtherLogins { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/RemoveLoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class RemoveLoginViewModel 10 | { 11 | public string LoginProvider { get; set; } 12 | public string ProviderKey { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/SetPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class SetPasswordViewModel 10 | { 11 | [Required] 12 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 13 | [DataType(DataType.Password)] 14 | [Display(Name = "New password")] 15 | public string NewPassword { get; set; } 16 | 17 | [DataType(DataType.Password)] 18 | [Display(Name = "Confirm new password")] 19 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 20 | public string ConfirmPassword { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlogPlayground/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlogPlayground.Models.ManageViewModels 8 | { 9 | public class VerifyPhoneNumberViewModel 10 | { 11 | [Required] 12 | public string Code { get; set; } 13 | 14 | [Required] 15 | [Phone] 16 | [Display(Name = "Phone number")] 17 | public string PhoneNumber { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlogPlayground/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore; 8 | 9 | namespace BlogPlayground 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | BuildWebHost(args).Run(); 16 | } 17 | 18 | public static IWebHost BuildWebHost(string[] args) => 19 | WebHost.CreateDefaultBuilder(args) 20 | .UseStartup() 21 | .Build(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BlogPlayground/Project_Readme.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Welcome to ASP.NET Core 6 | 127 | 128 | 129 | 130 | 138 | 139 |
    140 |
    141 |

    This application consists of:

    142 |
      143 |
    • Sample pages using ASP.NET Core MVC
    • 144 |
    • Bower for managing client-side libraries
    • 145 |
    • Theming using Bootstrap
    • 146 |
    147 |
    148 | 160 | 172 |
    173 |

    Run & Deploy

    174 | 179 |
    180 | 181 | 184 |
    185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /BlogPlayground/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:64917/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BlogPlayground": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /BlogPlayground/Services/GooglePictureLocator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlogPlayground.Services 10 | { 11 | public class GooglePictureLocator: IGooglePictureLocator 12 | { 13 | public async Task GetProfilePictureAsync(ExternalLoginInfo info) 14 | { 15 | var accessToken = info.AuthenticationTokens.SingleOrDefault(t => t.Name == "access_token"); 16 | Uri apiRequestUri = new Uri("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + accessToken.Value); 17 | //request profile image 18 | using (var client = new HttpClient()) 19 | { 20 | var stringResponse = await client.GetStringAsync(apiRequestUri); 21 | dynamic profile = JsonConvert.DeserializeObject(stringResponse); 22 | return profile.picture; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlogPlayground/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace BlogPlayground.Services 7 | { 8 | public interface IEmailSender 9 | { 10 | Task SendEmailAsync(string email, string subject, string message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlogPlayground/Services/IGooglePictureLocator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Identity; 3 | 4 | namespace BlogPlayground.Services 5 | { 6 | public interface IGooglePictureLocator 7 | { 8 | Task GetProfilePictureAsync(ExternalLoginInfo info); 9 | } 10 | } -------------------------------------------------------------------------------- /BlogPlayground/Services/IRequestUserProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace BlogPlayground.Services 7 | { 8 | public interface IRequestUserProvider 9 | { 10 | string GetUserId(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlogPlayground/Services/ISmsSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace BlogPlayground.Services 7 | { 8 | public interface ISmsSender 9 | { 10 | Task SendSmsAsync(string number, string message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlogPlayground/Services/MessageServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace BlogPlayground.Services 7 | { 8 | // This class is used by the application to send Email and SMS 9 | // when you turn on two-factor authentication in ASP.NET Identity. 10 | // For more details see this link http://go.microsoft.com/fwlink/?LinkID=532713 11 | public class AuthMessageSender : IEmailSender, ISmsSender 12 | { 13 | public Task SendEmailAsync(string email, string subject, string message) 14 | { 15 | // Plug in your email service here to send an email. 16 | return Task.FromResult(0); 17 | } 18 | 19 | public Task SendSmsAsync(string number, string message) 20 | { 21 | // Plug in your SMS service here to send a text message. 22 | return Task.FromResult(0); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BlogPlayground/Services/RequestUserProvider.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.Models; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Identity; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlogPlayground.Services 10 | { 11 | public class RequestUserProvider: IRequestUserProvider 12 | { 13 | private readonly IHttpContextAccessor _contextAccessor; 14 | private readonly UserManager _userManager; 15 | 16 | public RequestUserProvider(IHttpContextAccessor contextAccessor, UserManager userManager) 17 | { 18 | _contextAccessor = contextAccessor; 19 | _userManager = userManager; 20 | } 21 | 22 | public string GetUserId() 23 | { 24 | return _userManager.GetUserId(_contextAccessor.HttpContext.User); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Mvc.Infrastructure; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.DependencyInjection.Extensions; 13 | using Microsoft.Extensions.Logging; 14 | using BlogPlayground.Data; 15 | using BlogPlayground.Models; 16 | using BlogPlayground.Services; 17 | 18 | namespace BlogPlayground 19 | { 20 | public class Startup 21 | { 22 | public Startup(IConfiguration configuration) 23 | { 24 | Configuration = configuration; 25 | } 26 | 27 | public IConfiguration Configuration { get; } 28 | 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | // Add framework services. 33 | ConfigureDatabase(services); 34 | 35 | services.AddIdentity() 36 | .AddEntityFrameworkStores() 37 | .AddDefaultTokenProviders(); 38 | 39 | services.AddAuthentication() 40 | .AddGoogle(options => 41 | { 42 | options.ClientId = Configuration["GoogleClientId"] ?? "MissingClientId"; 43 | options.ClientSecret = Configuration["GoogleClientSecret"] ?? "MissingClientSecret"; 44 | options.SaveTokens = true; // So we get the access token and can use it later to retrieve the user profile including its picture 45 | //options.CallbackPath = "/signin-google" DEFAULT VALUE 46 | }); 47 | 48 | services.AddAntiforgery(opts => opts.HeaderName = "XSRF-TOKEN"); 49 | 50 | services.AddMvc(); 51 | services.AddResponseCompression(); 52 | 53 | //Needed for accessing action context in the tag helpers 54 | services.TryAddSingleton(); 55 | 56 | // Add application repositories as scoped dependencies so they are shared per every request. 57 | services.AddScoped(); 58 | 59 | // Add application services. 60 | services.AddTransient(); 61 | services.AddTransient(); 62 | services.AddTransient(); 63 | services.AddTransient(); 64 | } 65 | 66 | public virtual void ConfigureDatabase(IServiceCollection services) 67 | { 68 | services.AddDbContext(options => 69 | options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 70 | } 71 | 72 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 73 | public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 74 | { 75 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 76 | loggerFactory.AddDebug(); 77 | 78 | if (env.IsDevelopment()) 79 | { 80 | app.UseDeveloperExceptionPage(); 81 | app.UseDatabaseErrorPage(); 82 | app.UseBrowserLink(); 83 | } 84 | else 85 | { 86 | app.UseExceptionHandler("/Home/Error"); 87 | } 88 | 89 | app.UseResponseCompression(); 90 | app.UseStaticFiles(); 91 | app.UseAuthentication(); 92 | 93 | app.UseMvc(routes => 94 | { 95 | routes.MapRoute( 96 | name: "default", 97 | template: "{controller=Home}/{action=Index}/{id?}"); 98 | }); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /BlogPlayground/TagHelpers/MarkdownTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | using HeyRed.MarkdownSharp; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlogPlayground.TagHelpers 9 | { 10 | public class MarkdownTagHelper : TagHelper 11 | { 12 | public string Source { get; set; } 13 | 14 | public override void Process(TagHelperContext context, TagHelperOutput output) 15 | { 16 | //add wrapper span with well known class 17 | output.TagName = "div"; 18 | output.TagMode = TagMode.StartTagAndEndTag; 19 | output.Attributes.SetAttribute("class", "markdown-contents"); 20 | 21 | //Add processed markup as the inner contents. Notice the usage of SetHtmlContent so the processed markup is not encoded 22 | var markdown = new Markdown(); 23 | output.Content.SetHtmlContent(markdown.Transform(this.Source)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlogPlayground/TagHelpers/ProfilePictureTagHelper.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.Models; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Infrastructure; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | using Microsoft.AspNetCore.Mvc.Routing; 7 | using Microsoft.AspNetCore.Razor.TagHelpers; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace BlogPlayground.TagHelpers 14 | { 15 | public class ProfilePictureTagHelper : TagHelper 16 | { 17 | private readonly IUrlHelperFactory urlHelperFactory; 18 | private readonly IActionContextAccessor actionAccessor; 19 | 20 | public ProfilePictureTagHelper(IUrlHelperFactory urlHelperFactory, IActionContextAccessor actionAccessor) 21 | { 22 | this.urlHelperFactory = urlHelperFactory; 23 | this.actionAccessor = actionAccessor; 24 | } 25 | 26 | public ApplicationUser Profile { get; set; } 27 | public int? SizePx { get; set; } 28 | private bool IsDefaultPicture => String.IsNullOrWhiteSpace(this.Profile.PictureUrl); 29 | private IUrlHelper UrlHelper => this.urlHelperFactory.GetUrlHelper(this.actionAccessor.ActionContext); 30 | 31 | public override void Process(TagHelperContext context, TagHelperOutput output) 32 | { 33 | if (this.Profile == null) 34 | { 35 | output.SuppressOutput(); 36 | return; 37 | } 38 | 39 | //add wrapper span with well known class 40 | output.TagName = "span"; 41 | output.TagMode = TagMode.StartTagAndEndTag; 42 | output.Attributes.SetAttribute("class", "profile-picture"); 43 | 44 | //Add inner img element 45 | var img = new TagBuilder("img"); 46 | img.TagRenderMode = TagRenderMode.SelfClosing; 47 | img.Attributes.Add("title", this.GetAlternateText()); 48 | img.Attributes.Add("src", this.GetPictureUrl()); 49 | if (this.IsDefaultPicture && this.SizePx.HasValue) { 50 | img.Attributes.Add("style", $"height:{this.SizePx.Value}px;width:{this.SizePx.Value}px"); 51 | } 52 | output.Content.SetHtmlContent(img); 53 | } 54 | 55 | private string GetAlternateText() 56 | { 57 | return this.Profile.FullName ?? this.Profile.UserName; 58 | } 59 | 60 | private string GetPictureUrl() 61 | { 62 | if (this.IsDefaultPicture) 63 | { 64 | return this.UrlHelper.Content("~/images/placeholder.png"); 65 | } 66 | 67 | var imgUriBuilder = new UriBuilder(this.Profile.PictureUrl); 68 | if (this.SizePx.HasValue) 69 | { 70 | var query = QueryString.FromUriComponent(imgUriBuilder.Query); 71 | query = query.Add("sz", this.SizePx.Value.ToString()); 72 | imgUriBuilder.Query = query.ToString(); 73 | } 74 | 75 | return imgUriBuilder.Uri.ToString(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /BlogPlayground/ViewComponents/LatestArticlesViewComponent.cs: -------------------------------------------------------------------------------- 1 | using BlogPlayground.Data; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlogPlayground.ViewComponents 10 | { 11 | public class LatestArticlesViewComponent: ViewComponent 12 | { 13 | private readonly IArticlesRepository _repository; 14 | 15 | public LatestArticlesViewComponent(IArticlesRepository repository) 16 | { 17 | _repository = repository; 18 | } 19 | 20 | public async Task InvokeAsync(int howMany = 2) 21 | { 22 | return View(await _repository.GetLatest(howMany)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Confirm Email"; 3 | } 4 | 5 |

    @ViewData["Title"].

    6 |
    7 |

    8 | Thank you for confirming your email. Please Click here to Log in. 9 |

    10 |
    11 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ExternalLoginConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model ExternalLoginConfirmationViewModel 2 | @{ 3 | ViewData["Title"] = "Register"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 |

    Associate your @ViewData["LoginProvider"] account.

    8 | 9 |
    10 |

    Association Form

    11 |
    12 |
    13 | 14 |

    15 | You've successfully authenticated with @ViewData["LoginProvider"]. 16 | Please enter an email address for this site below and click the Register button to finish 17 | logging in. 18 |

    19 |
    20 | 21 |
    22 | 23 | 24 |
    25 |
    26 |
    27 |
    28 | 29 |
    30 |
    31 |
    32 | 33 | @section Scripts { 34 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 35 | } 36 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ExternalLoginFailure.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Login Failure"; 3 | } 4 | 5 |
    6 |

    @ViewData["Title"].

    7 |

    Unsuccessful login with service.

    8 |
    9 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ForgotPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model ForgotPasswordViewModel 2 | @{ 3 | ViewData["Title"] = "Forgot your password?"; 4 | } 5 | 6 |

    @ViewData["Title"]

    7 |

    8 | For more information on how to enable reset password please see this article. 9 |

    10 | 11 | @*
    12 |

    Enter your email.

    13 |
    14 |
    15 |
    16 | 17 |
    18 | 19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 |
    *@ 28 | 29 | @section Scripts { 30 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 31 | } 32 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Forgot Password Confirmation"; 3 | } 4 | 5 |

    @ViewData["Title"].

    6 |

    7 | Please check your email to reset your password. 8 |

    9 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Locked out"; 3 | } 4 | 5 |
    6 |

    Locked out.

    7 |

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

    8 |
    9 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Collections.Generic 2 | @using Microsoft.AspNetCore.Http 3 | @using Microsoft.AspNetCore.Http.Authentication 4 | @model LoginViewModel 5 | @inject SignInManager SignInManager 6 | 7 | @{ 8 | ViewData["Title"] = "Log in"; 9 | } 10 | 11 |

    @ViewData["Title"].

    12 |
    13 |
    14 |
    15 | 55 |
    56 |
    57 |
    58 |
    59 |

    Use another service to log in.

    60 |
    61 | @{ 62 | var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); 63 | if (loginProviders.Count == 0) 64 | { 65 |
    66 |

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

    70 |
    71 | } 72 | else 73 | { 74 |
    75 |
    76 |

    77 | @foreach (var provider in loginProviders) 78 | { 79 | 80 | } 81 |

    82 |
    83 |
    84 | } 85 | } 86 |
    87 |
    88 |
    89 | 90 | @section Scripts { 91 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 92 | } 93 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @model RegisterViewModel 2 | @{ 3 | ViewData["Title"] = "Register"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 | 8 |
    9 |

    Create a new account.

    10 |
    11 |
    12 |
    13 | 14 |
    15 | 16 | 17 |
    18 |
    19 |
    20 | 21 |
    22 | 23 | 24 |
    25 |
    26 |
    27 | 28 |
    29 | 30 | 31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 | 40 | @section Scripts { 41 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 42 | } 43 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model ResetPasswordViewModel 2 | @{ 3 | ViewData["Title"] = "Reset password"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 | 8 |
    9 |

    Reset your password.

    10 |
    11 |
    12 | 13 |
    14 | 15 |
    16 | 17 | 18 |
    19 |
    20 |
    21 | 22 |
    23 | 24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 | 31 | 32 |
    33 |
    34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 | 41 | @section Scripts { 42 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 43 | } 44 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Reset password confirmation"; 3 | } 4 | 5 |

    @ViewData["Title"].

    6 |

    7 | Your password has been reset. Please Click here to log in. 8 |

    9 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/SendCode.cshtml: -------------------------------------------------------------------------------- 1 | @model SendCodeViewModel 2 | @{ 3 | ViewData["Title"] = "Send Verification Code"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 | 8 |
    9 | 10 |
    11 |
    12 | Select Two-Factor Authentication Provider: 13 | 14 | 15 |
    16 |
    17 |
    18 | 19 | @section Scripts { 20 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 21 | } 22 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Account/VerifyCode.cshtml: -------------------------------------------------------------------------------- 1 | @model VerifyCodeViewModel 2 | @{ 3 | ViewData["Title"] = "Verify"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 | 8 |
    9 |
    10 | 11 | 12 |

    @ViewData["Status"]

    13 |
    14 |
    15 | 16 |
    17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 |
    24 | 25 | 26 |
    27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 |
    35 | 36 | @section Scripts { 37 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 38 | } 39 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Articles/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model BlogPlayground.Models.Article 2 | 3 | @{ 4 | ViewData["Title"] = "Create"; 5 | } 6 | 7 |
    8 |
    9 |

    Create new article

    10 |
    11 |
    12 |
    13 | 14 |
    15 | 16 | 17 |
    18 |
    19 |
    20 | 21 |
    22 | 23 | 24 |
    25 |
    26 |
    27 | 28 |
    29 | 30 | 31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 | Back to List 43 |
    44 | 45 | @section Scripts { 46 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} 47 | } 48 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Articles/Delete.cshtml: -------------------------------------------------------------------------------- 1 | @model BlogPlayground.Models.Article 2 | 3 | @{ 4 | ViewData["Title"] = "Delete"; 5 | } 6 | 7 |

    Delete

    8 | 9 |

    Are you sure you want to delete this?

    10 |
    11 |

    Article

    12 |
    13 |
    14 |
    15 | @Html.DisplayNameFor(model => model.Abstract) 16 |
    17 |
    18 | @Html.DisplayFor(model => model.Abstract) 19 |
    20 |
    21 | @Html.DisplayNameFor(model => model.Contents) 22 |
    23 |
    24 | @Html.DisplayFor(model => model.Contents) 25 |
    26 |
    27 | @Html.DisplayNameFor(model => model.CreatedDate) 28 |
    29 |
    30 | @Html.DisplayFor(model => model.CreatedDate) 31 |
    32 |
    33 | 34 |
    35 |
    36 | | 37 | Back to List 38 |
    39 |
    40 |
    41 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Articles/Details.cshtml: -------------------------------------------------------------------------------- 1 | @model BlogPlayground.Models.Article 2 | 3 | @{ 4 | ViewData["Title"] = Model.Title; 5 | } 6 | 7 |
    8 |
    9 | @Html.Partial("_ArticleSummary", Model) 10 |
    11 | 12 |
    13 | 14 |
    15 | 35 |
    36 | 37 |
    38 | 39 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Articles/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | 3 | @{ 4 | ViewData["Title"] = "Articles"; 5 | } 6 | 7 |

    This is what we have been writing...

    8 | 9 |
    10 |
      11 | @foreach (var article in Model) 12 | { 13 |
    • 14 | @Html.Partial("_ArticleSummary", article) 15 |
    • 16 | } 17 |
    18 | 19 |
    20 | 30 |
    31 | 32 |
    33 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Articles/_ArticleSummary.cshtml: -------------------------------------------------------------------------------- 1 | @model BlogPlayground.Models.Article 2 | 3 |
    4 |
    5 | 6 |

    7 | @(Model.Author.FullName ?? Model.Author.UserName) 8 |

    9 |

    10 | @Model.CreatedDate.ToString("dd MMM yyyy") 11 |

    12 |
    13 |
    14 |

    15 | @Model.Title 16 |

    17 |
    18 |

    @Model.Abstract

    19 |
    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "About"; 3 | } 4 |

    @ViewData["Title"].

    5 |

    @ViewData["Message"]

    6 | 7 |

    Use this area to provide additional information.

    8 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Contact"; 3 | } 4 |

    @ViewData["Title"].

    5 |

    @ViewData["Message"]

    6 | 7 |
    8 | One Microsoft Way
    9 | Redmond, WA 98052-6399
    10 | P: 11 | 425.555.0100 12 |
    13 | 14 |
    15 | Support: Support@example.com
    16 | Marketing: Marketing@example.com 17 |
    18 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | 67 | 68 | 110 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Manage/AddPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model AddPhoneNumberViewModel 2 | @{ 3 | ViewData["Title"] = "Add Phone Number"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 |
    8 |

    Add a phone number.

    9 |
    10 |
    11 |
    12 | 13 |
    14 | 15 | 16 |
    17 |
    18 |
    19 |
    20 | 21 |
    22 |
    23 |
    24 | 25 | @section Scripts { 26 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 27 | } 28 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @model ChangePasswordViewModel 2 | @{ 3 | ViewData["Title"] = "Change Password"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 | 8 |
    9 |

    Change Password Form

    10 |
    11 |
    12 |
    13 | 14 |
    15 | 16 | 17 |
    18 |
    19 |
    20 | 21 |
    22 | 23 | 24 |
    25 |
    26 |
    27 | 28 |
    29 | 30 | 31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 | 40 | @section Scripts { 41 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 42 | } 43 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model IndexViewModel 2 | @{ 3 | ViewData["Title"] = "Manage your account"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 |

    @ViewData["StatusMessage"]

    8 | 9 |
    10 |

    Change your account settings

    11 |
    12 |
    13 |
    Password:
    14 |
    15 | @if (Model.HasPassword) 16 | { 17 | Change 18 | } 19 | else 20 | { 21 | Create 22 | } 23 |
    24 |
    External Logins:
    25 |
    26 | 27 | @Model.Logins.Count Manage 28 |
    29 |
    Phone Number:
    30 |
    31 |

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

    36 | @*@(Model.PhoneNumber ?? "None") 37 | @if (Model.PhoneNumber != null) 38 | { 39 |
    40 | Change 41 |
    42 | [] 43 |
    44 | } 45 | else 46 | { 47 | Add 48 | }*@ 49 |
    50 | 51 |
    Two-Factor Authentication:
    52 |
    53 |

    54 | There are no two-factor authentication providers configured. See this article 55 | for setting up this application to support two-factor authentication. 56 |

    57 | @*@if (Model.TwoFactor) 58 | { 59 |
    60 | Enabled 61 |
    62 | } 63 | else 64 | { 65 |
    66 | Disabled 67 |
    68 | }*@ 69 |
    70 |
    71 |
    72 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Manage/ManageLogins.cshtml: -------------------------------------------------------------------------------- 1 | @model ManageLoginsViewModel 2 | @using Microsoft.AspNetCore.Http.Authentication 3 | @{ 4 | ViewData["Title"] = "Manage your external logins"; 5 | } 6 | 7 |

    @ViewData["Title"].

    8 | 9 |

    @ViewData["StatusMessage"]

    10 | @if (Model.CurrentLogins.Count > 0) 11 | { 12 |

    Registered Logins

    13 | 14 | 15 | @for (var index = 0; index < Model.CurrentLogins.Count; index++) 16 | { 17 | 18 | 19 | 35 | 36 | } 37 | 38 |
    @Model.CurrentLogins[index].LoginProvider 20 | @if ((bool)ViewData["ShowRemoveButton"]) 21 | { 22 |
    23 |
    24 | 25 | 26 | 27 |
    28 |
    29 | } 30 | else 31 | { 32 | @:   33 | } 34 |
    39 | } 40 | @if (Model.OtherLogins.Count > 0) 41 | { 42 |

    Add another service to log in.

    43 |
    44 |
    45 |
    46 |

    47 | @foreach (var scheme in Model.OtherLogins) 48 | { 49 | 50 | } 51 |

    52 |
    53 |
    54 | } 55 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SetPasswordViewModel 2 | @{ 3 | ViewData["Title"] = "Set Password"; 4 | } 5 | 6 |

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

    10 | 11 |
    12 |

    Set your password

    13 |
    14 |
    15 |
    16 | 17 |
    18 | 19 | 20 |
    21 |
    22 |
    23 | 24 |
    25 | 26 | 27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 |
    35 | 36 | @section Scripts { 37 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 38 | } 39 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Manage/VerifyPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model VerifyPhoneNumberViewModel 2 | @{ 3 | ViewData["Title"] = "Verify Phone Number"; 4 | } 5 | 6 |

    @ViewData["Title"].

    7 | 8 |
    9 | 10 |

    Add a phone number.

    11 |
    @ViewData["Status"]
    12 |
    13 |
    14 |
    15 | 16 |
    17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 | 28 | @section Scripts { 29 | @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 30 | } 31 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Shared/Components/LatestArticles/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | 3 |
    4 |

    Latest articles:

    5 |
      6 | @foreach (var article in Model) 7 | { 8 |
    • 9 | @Html.Partial("_ArticleSummary", article) 10 |
    • 11 | } 12 |
    13 |
    14 | 15 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

    Error.

    6 |

    An error occurred while processing your request.

    7 | 8 |

    Development Mode

    9 |

    10 | Swapping to Development environment will display more detailed information about the error that occurred. 11 |

    12 |

    13 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 14 |

    15 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - BlogPlayground 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 42 |
    43 | @RenderBody() 44 |
    45 |
    46 |

    © 2016 - BlogPlayground

    47 |
    48 |
    49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 64 | 65 | 66 | 67 | @RenderSection("scripts", required: false) 68 | 69 | 70 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using BlogPlayground.Models 3 | 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | 7 | @if (SignInManager.IsSignedIn(User)) 8 | { 9 | var userProfile = await UserManager.GetUserAsync(User); 10 | 11 |
    12 | 23 |
    24 | } 25 | else 26 | { 27 | 31 | } 32 | -------------------------------------------------------------------------------- /BlogPlayground/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /BlogPlayground/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using BlogPlayground 2 | @using BlogPlayground.Models 3 | @using BlogPlayground.Models.AccountViewModels 4 | @using BlogPlayground.Models.ManageViewModels 5 | @using Microsoft.AspNetCore.Identity 6 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 7 | @addTagHelper *, BlogPlayground 8 | -------------------------------------------------------------------------------- /BlogPlayground/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /BlogPlayground/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-BlogPlayground-BBC5BE87-7BB2-4F2A-BB97-AD897EF1EFDD;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "IncludeScopes": false, 7 | "LogLevel": { 8 | "Default": "Debug", 9 | "System": "Information", 10 | "Microsoft": "Information" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlogPlayground/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asp.net", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap": "3.3.6", 6 | "jquery": "2.2.0", 7 | "jquery-validation": "1.14.0", 8 | "jquery-validation-unobtrusive": "3.2.6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BlogPlayground/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optinally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /BlogPlayground/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Set widths on the form inputs since otherwise they're 100% wide */ 14 | input, 15 | select { 16 | max-width: 280px; 17 | } 18 | 19 | /* Carousel */ 20 | .carousel-caption p { 21 | font-size: 20px; 22 | line-height: 1.4; 23 | } 24 | 25 | /* buttons and links extension to use brackets: [ click me ] */ 26 | .btn-bracketed::before { 27 | display:inline-block; 28 | content: "["; 29 | padding-right: 0.5em; 30 | } 31 | .btn-bracketed::after { 32 | display:inline-block; 33 | content: "]"; 34 | padding-left: 0.5em; 35 | } 36 | 37 | /* Make .svg files in the carousel display properly in older browsers */ 38 | .carousel-inner .item img[src$=".svg"] 39 | { 40 | width: 100%; 41 | } 42 | 43 | /*Profile picture*/ 44 | .profile-picture img{ 45 | vertical-align: middle; 46 | border-radius: 50%; 47 | } 48 | 49 | /*Navbar*/ 50 | .navbar .profile-picture { 51 | line-height: 50px; 52 | } 53 | 54 | /*Articles*/ 55 | .article-summary .profile-picture img{ 56 | display: block; 57 | margin: 0 auto; 58 | } 59 | 60 | .article-summary .col-md-4{ 61 | margin-top: 5px; 62 | } 63 | 64 | .article-summary .author-name{ 65 | margin: 10px 0 0 0; 66 | } 67 | 68 | .article-list{ 69 | margin-top: 10px; 70 | } 71 | 72 | .article-list, 73 | .article-details { 74 | border-right: 1px solid #eee; 75 | } 76 | 77 | .article-page{ 78 | margin-top: 20px; 79 | } 80 | 81 | .last-articles { 82 | margin-top: 10px; 83 | padding-top: 10px; 84 | } 85 | 86 | .last-articles-list blockquote { 87 | font-size: 14px; 88 | } 89 | 90 | /* Hide/rearrange for smaller screens */ 91 | @media screen and (max-width: 767px) { 92 | /* Hide captions */ 93 | .carousel-caption { 94 | display: none 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}input,select,textarea{max-width:280px}.carousel-caption p{font-size:20px;line-height:1.4}.btn-bracketed::before{display:inline-block;content:"[";padding-right:.5em}.btn-bracketed::after{display:inline-block;content:"]";padding-left:.5em}.carousel-inner .item img[src$=".svg"]{width:100%}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/favicon.ico -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/images/placeholder.png -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your Javascript code. 2 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/js/site.min.js -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 2" 33 | }, 34 | "version": "3.3.6", 35 | "_release": "3.3.6", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.6", 39 | "commit": "81df608a40bf0629a1dc08e584849bb1e43e0b7a" 40 | }, 41 | "_source": "git://github.com/twbs/bootstrap.git", 42 | "_target": "3.3.6", 43 | "_originalSource": "bootstrap" 44 | } -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2015 Twitter, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaniJG/BlogPlayground/c8eb6183706d03d2edbf46a1d2e9927cc9fc5ee6/BlogPlayground/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "version": "3.2.6", 4 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 5 | "description": "Add-on to jQuery Validation to enable unobtrusive validation options in data-* attributes.", 6 | "main": [ 7 | "jquery.validate.unobtrusive.js" 8 | ], 9 | "ignore": [ 10 | "**/.*", 11 | "*.json", 12 | "*.md", 13 | "*.txt", 14 | "gulpfile.js" 15 | ], 16 | "keywords": [ 17 | "jquery", 18 | "asp.net", 19 | "mvc", 20 | "validation", 21 | "unobtrusive" 22 | ], 23 | "authors": [ 24 | "Microsoft" 25 | ], 26 | "license": "http://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm", 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/aspnet/jquery-validation-unobtrusive.git" 30 | }, 31 | "dependencies": { 32 | "jquery-validation": ">=1.8", 33 | "jquery": ">=1.8" 34 | }, 35 | "_release": "3.2.6", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.2.6", 39 | "commit": "13386cd1b5947d8a5d23a12b531ce3960be1eba7" 40 | }, 41 | "_source": "git://github.com/aspnet/jquery-validation-unobtrusive.git", 42 | "_target": "3.2.6", 43 | "_originalSource": "jquery-validation-unobtrusive" 44 | } -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** Unobtrusive validation support library for jQuery and jQuery Validate 3 | ** Copyright (C) Microsoft Corporation. All rights reserved. 4 | */ 5 | !function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function m(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=p.unobtrusive.options||{},m=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),m("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),m("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),m("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var u,p=a.validator,v="unobtrusiveValidation";p.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=m(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){p.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=m(this);a&&a.attachValidation()})}},u=p.unobtrusive.adapters,u.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},u.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},u.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},u.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},p.addMethod("__dummy__",function(a,e,n){return!0}),p.addMethod("regex",function(a,e,n){var t;return this.optional(e)?!0:(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),p.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),p.methods.extension?(u.addSingleVal("accept","mimtype"),u.addSingleVal("extension","extension")):u.addSingleVal("extension","extension","accept"),u.addSingleVal("regex","pattern"),u.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),u.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),u.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),u.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),u.add("required",function(a){("INPUT"!==a.element.tagName.toUpperCase()||"CHECKBOX"!==a.element.type.toUpperCase())&&e(a,"required",!0)}),u.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),u.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),a(function(){p.unobtrusive.parse(document)})}(jQuery); -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "http://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jzaefferer/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.14.0", 31 | "_release": "1.14.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.14.0", 35 | "commit": "c1343fb9823392aa9acbe1c3ffd337b8c92fed48" 36 | }, 37 | "_source": "git://github.com/jzaefferer/jquery-validation.git", 38 | "_target": ">=1.8", 39 | "_originalSource": "jquery-validation" 40 | } -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "2.2.0", 16 | "_release": "2.2.0", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "2.2.0", 20 | "commit": "6fc01e29bdad0964f62ef56d01297039cdcadbe5" 21 | }, 22 | "_source": "git://github.com/jquery/jquery-dist.git", 23 | "_target": "2.2.0", 24 | "_originalSource": "jquery" 25 | } -------------------------------------------------------------------------------- /BlogPlayground/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlogPlayground 2 | 3 | This repo contains the code of the article _ASP.NET Core: Clean Reusable Front-end code_ originally published in [the DNC Magazine](http://www.dotnetcurry.com/aspnet/1321/aspnet-core-clean-frontend-code). 4 | --------------------------------------------------------------------------------