├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── ENHANCEMENT.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── LICENSE ├── NOTICE ├── README.adoc ├── UnicornStore.sln ├── UnicornStore ├── .dockerignore ├── Areas │ └── Admin │ │ ├── Controllers │ │ └── StoreManagerController.cs │ │ └── Views │ │ ├── StoreManager │ │ ├── Create.cshtml │ │ ├── Details.cshtml │ │ ├── Edit.cshtml │ │ ├── Index.cshtml │ │ └── RemoveBlessing.cshtml │ │ └── _ViewStart.cshtml ├── Components │ ├── CartSummaryComponent.cs │ ├── GenreMenuComponent.cs │ ├── ISystemClock.cs │ └── SystemClock.cs ├── Controllers │ ├── AccountController.cs │ ├── CheckoutController.cs │ ├── HomeController.cs │ ├── ManageController.cs │ ├── ShoppingCartController.cs │ └── StoreController.cs ├── Data │ └── DbInitializer.cs ├── Dockerfile ├── MessageServices.cs ├── Models │ ├── AccountViewModels.cs │ ├── Blessing.cs │ ├── CartItem.cs │ ├── Genre.cs │ ├── ManageViewModels.cs │ ├── MusicStoreContext.cs │ ├── Order.cs │ ├── OrderDetail.cs │ ├── ShoppingCart.cs │ └── Unicorn.cs ├── Pages │ └── PageThatThrows.cshtml ├── Platform.cs ├── Program.cs ├── Properties │ ├── AppSettings.cs │ └── launchSettings.json ├── Scripts │ └── _references.js ├── Startup.cs ├── UnicornStore.csproj ├── ViewModels │ ├── BlessingData.cs │ ├── ShoppingCartRemoveViewModel.cs │ └── ShoppingCartViewModel.cs ├── Views │ ├── Account │ │ ├── ConfirmEmail.cshtml │ │ ├── ExternalLoginConfirmation.cshtml │ │ ├── ExternalLoginFailure.cshtml │ │ ├── ForgotPassword.cshtml │ │ ├── ForgotPasswordConfirmation.cshtml │ │ ├── Login.cshtml │ │ ├── Register.cshtml │ │ ├── RegisterConfirmation.cshtml │ │ ├── ResetPassword.cshtml │ │ ├── ResetPasswordConfirmation.cshtml │ │ ├── SendCode.cshtml │ │ ├── VerifyCode.cshtml │ │ └── _ExternalLoginsListPartial.cshtml │ ├── Checkout │ │ ├── AddressAndPayment.cshtml │ │ └── Complete.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Manage │ │ ├── AddPhoneNumber.cshtml │ │ ├── ChangePassword.cshtml │ │ ├── Index.cshtml │ │ ├── ManageLogins.cshtml │ │ ├── SetPassword.cshtml │ │ └── VerifyPhoneNumber.cshtml │ ├── Shared │ │ ├── AccessDenied.cshtml │ │ ├── Components │ │ │ ├── Announcement │ │ │ │ └── Default.cshtml │ │ │ ├── CartSummary │ │ │ │ └── Default.cshtml │ │ │ └── GenreMenu │ │ │ │ └── Default.cshtml │ │ ├── DemoLinkDisplay.cshtml │ │ ├── Error.cshtml │ │ ├── Lockout.cshtml │ │ ├── StatusCodePage.cshtml │ │ ├── _Layout.cshtml │ │ ├── _LoginPartial.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── ShoppingCart │ │ └── Index.cshtml │ ├── Store │ │ ├── Browse.cshtml │ │ ├── Details.cshtml │ │ └── Index.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.Production.json ├── appsettings.json └── wwwroot │ ├── Content │ ├── Site.css │ ├── bootstrap.css │ └── bootstrap.min.css │ ├── Images │ ├── logo.svg │ ├── main-unicorn.png │ ├── placeholder.png │ ├── placeholder.svg │ └── unicorn-two.png │ ├── Scripts │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-2.0.3.intellisense.js │ ├── jquery-2.0.3.js │ ├── jquery-2.0.3.min.js │ ├── jquery-2.0.3.min.map │ ├── jquery.signalR-2.0.1.js │ ├── jquery.signalR-2.0.1.min.js │ ├── jquery.validate-vsdoc.js │ ├── jquery.validate.js │ ├── jquery.validate.min.js │ ├── jquery.validate.unobtrusive.js │ ├── jquery.validate.unobtrusive.min.js │ ├── modernizr-2.6.2.js │ ├── respond.js │ └── respond.min.js │ ├── favicon.ico │ └── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── content └── secrets │ ├── _index.md │ ├── accessing-secrets.md │ ├── cfn │ ├── player-template.yaml │ └── player-vpc-template.yaml │ ├── container-registry.md │ ├── containerize-unicornstore.md │ ├── create-secrets.md │ ├── fargate.md │ ├── introduction.md │ ├── prerequisites │ ├── _index.md │ ├── aws_event │ │ ├── _index.md │ │ └── portal.md │ ├── getting-started.md │ └── self_paced │ │ ├── _index.md │ │ ├── account.md │ │ └── cloudformation.md │ ├── secrets.md │ ├── taskdefinitions.files │ └── modernization-unicorn-store-task-definition.json │ └── taskdefinitions.md ├── docker-compose.development.yml ├── docker-compose.production.yml ├── docker-compose.yml └── static ├── 640px-Amazon_Web_Services_Logo.svg.png ├── AWS-Logo.svg ├── Amazon_Web_Services_Logo.svg └── images ├── secrets ├── cloud9-dotnet.png ├── connectionstring.png ├── csproj-usersecretsid.png ├── docker-compose-development.png ├── examplesecret.png ├── modernization-unicorn-store-task-definition.png ├── prerequisites │ ├── bashrc.png │ ├── iam-1-create-user.png │ ├── iam-2-attach-policy.png │ ├── iam-3-create-user.png │ ├── iam-4-save-url.png │ ├── portal_buttons.png │ └── portal_login.png ├── program-createdefaultbuilder.png ├── retrieveunicornsecret-policy.png ├── secret-appsettings.png ├── secret-final.png ├── secrets-manager-architecture.png ├── startup-configuration.png ├── unicornstore-docker-images.png ├── unicornstore-ecr.png └── unicornstore-prod.png └── unicornstore.png /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | For this bug report, be sure to include: 2 | * A short, descriptive title. Ideally, other community members should be able to get a good idea of the issue just from reading the title. 3 | * A detailed description of the problem you're experiencing. This should include: 4 | * Expected behavior of the workshop and the actual behavior exhibited. 5 | * Any details of your application environment that may be relevant. 6 | * Commands and output used to reproduce the issue. 7 | * [Markdown][https://guides.github.com/features/mastering-markdown/] formatting as appropriate to make the report easier to read; for example use code blocks when pasting a code snippet and exception stacktraces. 8 | 9 | *Description:* 10 | 11 | 12 | *Expected vs. Actual Behavior:* 13 | 14 | 15 | *Environment Details:* 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ENHANCEMENT.md: -------------------------------------------------------------------------------- 1 | For this enhancement, be sure to include: 2 | * A short, descriptive title. Ideally, other community members should be able to get a good idea of the enhancement just from reading the title. 3 | * A detailed description of the the proposed enhancement. Include justification for why it should be added to the workshop, and possibly example code to illustrate how it should work. 4 | * link:https://guides.github.com/features/mastering-markdown/[Markdown] formatting as appropriate to make the request easier to read. 5 | * If you intend to implement this enhancement, indicate that you'd like to the issue to be assigned to you. 6 | 7 | 8 | *Description:* 9 | 10 | 11 | *Justification (Why should this be added?):* 12 | 13 | 14 | *Assigned to you? (Y/N)* 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available (include [keywords](https://help.github.com/articles/closing-issues-using-keywords/) to close issue as applicable, e.g. "fixes <##>"):* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | msbuild.log 25 | msbuild.err 26 | msbuild.wrn 27 | 28 | # Visual Studio 29 | .vs/ 30 | .vscode/ 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/modernization-unicorn-store/issues), or [recently closed](https://github.com/aws-samples/modernization-unicorn-store/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/modernization-unicorn-store/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/modernization-unicorn-store/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(DefaultItemExcludes);$(MSBuildProjectDirectory)/obj/**/* 5 | $(DefaultItemExcludes);$(MSBuildProjectDirectory)/bin/**/* 6 | 7 | 8 | 9 | $(MSBuildProjectDirectory)/obj/container/ 10 | $(MSBuildProjectDirectory)/bin/container/ 11 | 12 | 13 | 14 | $(MSBuildProjectDirectory)/obj/local/ 15 | $(MSBuildProjectDirectory)/bin/local/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | modernization-unicorn-store 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = modernization-unicorn-store 2 | 3 | :imagesdir: static 4 | 5 | AWS Sample Application for containerizing a .NET application 6 | 7 | image::images/unicornstore.png[Unicorn Store] 8 | 9 | == License 10 | 11 | This library is licensed under the Apache 2.0 License. 12 | 13 | == Prerequisites 14 | 15 | To get started you will need: 16 | 17 | - https://docs.docker.com/install/[Docker] installed on your local machine. 18 | - https://docs.docker.com/compose/install/[Docker Compose] 19 | - https://dotnet.microsoft.com/download[.NET Core 3] 20 | - An IDE for building .NET Core applications such as https://visualstudio.microsoft.com/[Visual Studio] 21 | 22 | Once you have the prerequisites installed on your local development machine you should be able to run the Unicorn Store locally either in your IDE or in a local container. The Unicorn Store uses https://www.microsoft.com/en-us/sql-server/default.aspx[SQL Server] for the backend by default and is seeded with data on startup. You will need to either provision or have access to a database. The below workshops like link:content/secrets/_index.md[Securing Your .NET Container Secrets] handle this for you via https://aws.amazon.com/cloudformation/[CloudFromation]. 23 | 24 | == Workshops 25 | 26 | - link:content/secrets/_index.md[Securing Your .NET Container Secrets] 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /UnicornStore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicornStore", "UnicornStore/UnicornStore.csproj", "{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|x64.ActiveCfg = Debug|Any CPU 21 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|x64.Build.0 = Debug|Any CPU 22 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|x86.ActiveCfg = Debug|Any CPU 23 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|x86.Build.0 = Debug|Any CPU 24 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|x64.ActiveCfg = Release|Any CPU 27 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|x64.Build.0 = Release|Any CPU 28 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|x86.ActiveCfg = Release|Any CPU 29 | {3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|x86.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {C9A7C5A2-5C90-4AD9-ABB8-6F2D2364D5AF} 36 | EndGlobalSection 37 | EndGlobal 38 | 39 | -------------------------------------------------------------------------------- /UnicornStore/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .vs 4 | .vscode 5 | **/bin 6 | **/Dockerfile 7 | **/obj 8 | -------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Controllers/StoreManagerController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Cors; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Rendering; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Caching.Memory; 11 | using Microsoft.Extensions.Options; 12 | using UnicornStore.Models; 13 | using UnicornStore.ViewModels; 14 | 15 | namespace UnicornStore.Areas.Admin.Controllers 16 | { 17 | [Area("Admin")] 18 | [Authorize("ManageStore")] 19 | public class StoreManagerController : Controller 20 | { 21 | private readonly AppSettings _appSettings; 22 | 23 | public StoreManagerController(UnicornStoreContext dbContext, IOptions options) 24 | { 25 | DbContext = dbContext; 26 | _appSettings = options.Value; 27 | } 28 | 29 | public UnicornStoreContext DbContext { get; } 30 | 31 | // 32 | // GET: /StoreManager/ 33 | public async Task Index() 34 | { 35 | var blessings = await DbContext.Blessings 36 | .Include(a => a.Genre) 37 | .Include(a => a.Unicorn) 38 | .ToListAsync(); 39 | 40 | return View(blessings); 41 | } 42 | 43 | // 44 | // GET: /StoreManager/Details/5 45 | public async Task Details( 46 | [FromServices] IMemoryCache cache, 47 | int id) 48 | { 49 | var cacheKey = GetCacheKey(id); 50 | 51 | Blessing blessing; 52 | if (!cache.TryGetValue(cacheKey, out blessing)) 53 | { 54 | blessing = await DbContext.Blessings 55 | .Where(a => a.BlessingId == id) 56 | .Include(a => a.Unicorn) 57 | .Include(a => a.Genre) 58 | .FirstOrDefaultAsync(); 59 | 60 | if (blessing != null) 61 | { 62 | if (_appSettings.CacheDbResults) 63 | { 64 | //Remove it from cache if not retrieved in last 10 minutes. 65 | cache.Set( 66 | cacheKey, 67 | blessing, 68 | new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10))); 69 | } 70 | } 71 | } 72 | 73 | if (blessing == null) 74 | { 75 | cache.Remove(cacheKey); 76 | return NotFound(); 77 | } 78 | 79 | return View(blessing); 80 | } 81 | 82 | // 83 | // GET: /StoreManager/Create 84 | public IActionResult Create() 85 | { 86 | ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name"); 87 | ViewBag.UnicornId = new SelectList(DbContext.Unicorns, "UnicornId", "Name"); 88 | return View(); 89 | } 90 | 91 | // POST: /StoreManager/Create 92 | [HttpPost] 93 | [ValidateAntiForgeryToken] 94 | public async Task Create( 95 | Blessing blessing, 96 | [FromServices] IMemoryCache cache, 97 | CancellationToken requestAborted) 98 | { 99 | if (ModelState.IsValid) 100 | { 101 | DbContext.Blessings.Add(blessing); 102 | await DbContext.SaveChangesAsync(requestAborted); 103 | 104 | var blessingData = new BlessingData 105 | { 106 | Title = blessing.Title, 107 | Url = Url.Action("Details", "Store", new { id = blessing.BlessingId }) 108 | }; 109 | 110 | cache.Remove("latestBlessing"); 111 | return RedirectToAction("Index"); 112 | } 113 | 114 | ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name", blessing.GenreId); 115 | ViewBag.UnicornId = new SelectList(DbContext.Unicorns, "UnicornId", "Name", blessing.UnicornId); 116 | return View(blessing); 117 | } 118 | 119 | // 120 | // GET: /StoreManager/Edit/5 121 | public async Task Edit(int id) 122 | { 123 | var blessing = await DbContext.Blessings. 124 | Where(a => a.BlessingId == id). 125 | FirstOrDefaultAsync(); 126 | 127 | if (blessing == null) 128 | { 129 | return NotFound(); 130 | } 131 | 132 | ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name", blessing.GenreId); 133 | ViewBag.UnicornId = new SelectList(DbContext.Unicorns, "UnicornId", "Name", blessing.UnicornId); 134 | return View(blessing); 135 | } 136 | 137 | // 138 | // POST: /StoreManager/Edit/5 139 | [HttpPost] 140 | [ValidateAntiForgeryToken] 141 | public async Task Edit( 142 | [FromServices] IMemoryCache cache, 143 | Blessing blessing, 144 | CancellationToken requestAborted) 145 | { 146 | if (ModelState.IsValid) 147 | { 148 | DbContext.Update(blessing); 149 | await DbContext.SaveChangesAsync(requestAborted); 150 | //Invalidate the cache entry as it is modified 151 | cache.Remove(GetCacheKey(blessing.BlessingId)); 152 | return RedirectToAction("Index"); 153 | } 154 | 155 | ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name", blessing.GenreId); 156 | ViewBag.UnicornId = new SelectList(DbContext.Unicorns, "UnicornId", "Name", blessing.UnicornId); 157 | return View(blessing); 158 | } 159 | 160 | // 161 | // GET: /StoreManager/RemoveBlessing/5 162 | public async Task RemoveBlessing(int id) 163 | { 164 | var blessing = await DbContext.Blessings.Where(a => a.BlessingId == id).FirstOrDefaultAsync(); 165 | if (blessing == null) 166 | { 167 | return NotFound(); 168 | } 169 | 170 | return View(blessing); 171 | } 172 | 173 | // 174 | // POST: /StoreManager/RemoveBlessing/5 175 | [HttpPost, ActionName("RemoveBlessing")] 176 | public async Task RemoveBlessingConfirmed( 177 | [FromServices] IMemoryCache cache, 178 | int id, 179 | CancellationToken requestAborted) 180 | { 181 | var blessing = await DbContext.Blessings.Where(a => a.BlessingId == id).FirstOrDefaultAsync(); 182 | if (blessing == null) 183 | { 184 | return NotFound(); 185 | } 186 | 187 | DbContext.Blessings.Remove(blessing); 188 | await DbContext.SaveChangesAsync(requestAborted); 189 | //Remove the cache entry as it is removed 190 | cache.Remove(GetCacheKey(id)); 191 | 192 | return RedirectToAction("Index"); 193 | } 194 | 195 | private static string GetCacheKey(int id) 196 | { 197 | return string.Format("blessing_{0}", id); 198 | } 199 | 200 | // NOTE: this is used for end to end testing only 201 | // 202 | // GET: /StoreManager/GetBlessingIdFromName 203 | // Note: Added for automated testing purpose. Application does not use this. 204 | [HttpGet] 205 | [SkipStatusCodePages] 206 | [EnableCors("CorsPolicy")] 207 | public async Task GetBlessingIdFromName(string blessingName) 208 | { 209 | var blessing = await DbContext.Blessings.Where(a => a.Title == blessingName).FirstOrDefaultAsync(); 210 | 211 | if (blessing == null) 212 | { 213 | return NotFound(); 214 | } 215 | 216 | return Content(blessing.BlessingId.ToString()); 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Views/StoreManager/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model UnicornStore.Models.Blessing 2 | 3 | @{ 4 | ViewBag.Title = "Create"; 5 | } 6 | 7 |

Create

8 | 9 | @using (Html.BeginForm()) 10 | { 11 | @Html.AntiForgeryToken() 12 | 13 |
14 |

Blessing

15 |
16 | @Html.ValidationSummary(true) 17 | 18 |
19 | @Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" }) 20 |
21 | @Html.DropDownList("GenreId", String.Empty) 22 | @Html.ValidationMessageFor(model => model.GenreId) 23 |
24 |
25 | 26 |
27 | @Html.LabelFor(model => model.UnicornId, "UnicornId", new { @class = "control-label col-md-2" }) 28 |
29 | @Html.DropDownList("UnicornId", String.Empty) 30 | @Html.ValidationMessageFor(model => model.UnicornId) 31 |
32 |
33 | 34 |
35 | @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" }) 36 |
37 | @Html.EditorFor(model => model.Title) 38 | @Html.ValidationMessageFor(model => model.Title) 39 |
40 |
41 | 42 |
43 | @Html.LabelFor(model => model.Price, new { @class = "control-label col-md-2" }) 44 |
45 | @Html.EditorFor(model => model.Price) 46 | @Html.ValidationMessageFor(model => model.Price) 47 |
48 |
49 | 50 |
51 | @Html.LabelFor(model => model.BlessingArtUrl, new { @class = "control-label col-md-2" }) 52 |
53 | @Html.EditorFor(model => model.BlessingArtUrl) 54 | @Html.ValidationMessageFor(model => model.BlessingArtUrl) 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 | } 65 | 66 |
67 | @Html.ActionLink("Back to List", "Index") 68 |
69 | 70 | @section Scripts { 71 | @*TODO : Until script helpers are available, adding script references manually*@ 72 | @*@Scripts.Render("~/bundles/jqueryval")*@ 73 | 74 | 75 | } -------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Views/StoreManager/Details.cshtml: -------------------------------------------------------------------------------- 1 | @model UnicornStore.Models.Blessing 2 | 3 | @{ 4 | ViewBag.Title = "Details"; 5 | } 6 | 7 |

Details

8 | 9 |
10 |

Blessing

11 |
12 |
13 |
14 | @Html.DisplayNameFor(model => model.Unicorn.Name) 15 |
16 | 17 |
18 | @Html.DisplayFor(model => model.Unicorn.Name) 19 |
20 | 21 |
22 | @Html.DisplayNameFor(model => model.Genre.Name) 23 |
24 | 25 |
26 | @Html.DisplayFor(model => model.Genre.Name) 27 |
28 | 29 |
30 | @Html.DisplayNameFor(model => model.Title) 31 |
32 | 33 |
34 | @Html.DisplayFor(model => model.Title) 35 |
36 | 37 |
38 | @Html.DisplayNameFor(model => model.Price) 39 |
40 | 41 |
42 | @Html.DisplayFor(model => model.Price) 43 |
44 | 45 |
46 | @Html.DisplayNameFor(model => model.BlessingArtUrl) 47 |
48 | 49 |
50 | @Html.DisplayFor(model => model.BlessingArtUrl) 51 |
52 | 53 |
54 |
55 |

56 | @Html.ActionLink("Edit", "Edit", new { id = Model.BlessingId }) | 57 | @Html.ActionLink("Back to List", "Index") 58 |

-------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Views/StoreManager/Edit.cshtml: -------------------------------------------------------------------------------- 1 | @model UnicornStore.Models.Blessing 2 | 3 | @{ 4 | ViewBag.Title = "Edit"; 5 | } 6 | 7 |

Edit

8 | 9 | @using (Html.BeginForm()) 10 | { 11 | @Html.AntiForgeryToken() 12 | 13 |
14 |

Blessing

15 |
16 | @Html.ValidationSummary(true) 17 | @Html.HiddenFor(model => model.BlessingId) 18 | 19 |
20 | @Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" }) 21 |
22 | @Html.DropDownList("GenreId", String.Empty) 23 | @Html.ValidationMessageFor(model => model.GenreId) 24 |
25 |
26 | 27 |
28 | @Html.LabelFor(model => model.UnicornId, "UnicornId", new { @class = "control-label col-md-2" }) 29 |
30 | @Html.DropDownList("UnicornId", String.Empty) 31 | @Html.ValidationMessageFor(model => model.UnicornId) 32 |
33 |
34 | 35 |
36 | @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" }) 37 |
38 | @Html.EditorFor(model => model.Title) 39 | @Html.ValidationMessageFor(model => model.Title) 40 |
41 |
42 | 43 |
44 | @Html.LabelFor(model => model.Price, new { @class = "control-label col-md-2" }) 45 |
46 | @Html.EditorFor(model => model.Price) 47 | @Html.ValidationMessageFor(model => model.Price) 48 |
49 |
50 | 51 |
52 | @Html.LabelFor(model => model.BlessingArtUrl, new { @class = "control-label col-md-2" }) 53 |
54 | @Html.EditorFor(model => model.BlessingArtUrl) 55 | @Html.ValidationMessageFor(model => model.BlessingArtUrl) 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 |
65 | } 66 | 67 |
68 | @Html.ActionLink("Back to List", "Index") 69 |
70 | 71 | @section Scripts { 72 | @*TODO : Until script helpers are available, adding script references manually*@ 73 | @*@Scripts.Render("~/bundles/jqueryval")*@ 74 | 75 | 76 | } -------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Views/StoreManager/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | 3 | @{ 4 | ViewBag.Title = "Index"; 5 | } 6 | 7 |

Index

8 | 9 |

10 | @Html.ActionLink("Create New", "Create") 11 |

12 | 13 | 14 | 17 | 20 | 23 | 26 | 27 | 28 | 29 | @foreach (var item in Model) 30 | { 31 | 32 | 35 | 45 | 55 | 58 | 63 | 64 | } 65 | 66 |
15 | @Html.DisplayNameFor(model => model.Genre.Name) 16 | 18 | @Html.DisplayNameFor(model => model.FirstOrDefault().Unicorn.Name) 19 | 21 | @Html.DisplayNameFor(model => model.FirstOrDefault().Title) 22 | 24 | @Html.DisplayNameFor(model => model.FirstOrDefault().Price) 25 |
33 | @Html.DisplayFor(modelItem => item.Genre.Name) 34 | 36 | @if (item.Unicorn.Name.Length <= 25) 37 | { 38 | @item.Unicorn.Name 39 | } 40 | else 41 | { 42 | @item.Unicorn.Name.Substring(0, 25)... 43 | } 44 | 46 | @if (item.Title.Length <= 25) 47 | { 48 | @item.Title 49 | } 50 | else 51 | { 52 | @item.Title.Substring(0, 25)... 53 | } 54 | 56 | @Html.DisplayFor(modelItem => item.Price) 57 | 59 | @Html.ActionLink("Edit", "Edit", new { id = item.BlessingId }) | 60 | @Html.ActionLink("Details", "Details", new { id = item.BlessingId }) | 61 | @Html.ActionLink("Delete", "RemoveBlessing", new { id = item.BlessingId }) 62 |
-------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Views/StoreManager/RemoveBlessing.cshtml: -------------------------------------------------------------------------------- 1 | @model UnicornStore.Models.Blessing 2 | 3 | @{ 4 | ViewBag.Title = "Delete"; 5 | } 6 | 7 | @if (Model != null) 8 | { 9 |

Delete Confirmation

10 | 11 |

12 | Are you sure you want to delete the blessing titled 13 | @Model.Title? 14 |

15 | 16 | @using (Html.BeginForm()) 17 | { 18 |

19 | 20 |

21 |

22 | @Html.ActionLink("Back to List", "Index") 23 |

24 | } 25 | } 26 | else 27 | { 28 | @Html.Label(null, "Unable to locate the blessing") 29 | } -------------------------------------------------------------------------------- /UnicornStore/Areas/Admin/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } -------------------------------------------------------------------------------- /UnicornStore/Components/CartSummaryComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using UnicornStore.Models; 6 | 7 | namespace UnicornStore.Components 8 | { 9 | [ViewComponent(Name = "CartSummary")] 10 | public class CartSummaryComponent : ViewComponent 11 | { 12 | public CartSummaryComponent(UnicornStoreContext dbContext) 13 | { 14 | DbContext = dbContext; 15 | } 16 | 17 | private UnicornStoreContext DbContext { get; } 18 | 19 | public async Task InvokeAsync() 20 | { 21 | var cart = ShoppingCart.GetCart(DbContext, HttpContext); 22 | 23 | var cartItems = await cart.GetCartItems(); 24 | 25 | ViewBag.CartCount = cartItems.Sum(c => c.Count); 26 | ViewBag.CartSummary = string.Join("\n", cartItems.Select(c => c.Blessing.Title).Distinct()); 27 | 28 | return View(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /UnicornStore/Components/GenreMenuComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.EntityFrameworkCore; 6 | using UnicornStore.Models; 7 | 8 | namespace UnicornStore.Components 9 | { 10 | [ViewComponent(Name = "GenreMenu")] 11 | public class GenreMenuComponent : ViewComponent 12 | { 13 | public GenreMenuComponent(UnicornStoreContext dbContext) 14 | { 15 | DbContext = dbContext; 16 | } 17 | 18 | private UnicornStoreContext DbContext { get; } 19 | 20 | public async Task InvokeAsync() 21 | { 22 | // TODO use nested sum https://github.com/aspnet/EntityFramework/issues/3792 23 | //.OrderByDescending( 24 | // g => g.Blessings.Sum(a => a.OrderDetails.Sum(od => od.Quantity))) 25 | 26 | var genres = await DbContext.Genres.Select(g => g.Name).Take(9).ToListAsync(); 27 | 28 | return View(genres); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /UnicornStore/Components/ISystemClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnicornStore.Components 4 | { 5 | /// 6 | /// Abstracts the system clock to facilitate testing. 7 | /// 8 | public interface ISystemClock 9 | { 10 | /// 11 | /// Gets a DateTime object that is set to the current date and time on this computer, 12 | /// expressed as the Coordinated Universal Time(UTC) 13 | /// 14 | DateTime UtcNow { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /UnicornStore/Components/SystemClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnicornStore.Components 4 | { 5 | /// 6 | /// Provides access to the normal system clock. 7 | /// 8 | public class SystemClock : ISystemClock 9 | { 10 | /// 11 | public DateTime UtcNow 12 | { 13 | get 14 | { 15 | // The clock measures whole seconds only, and truncates the milliseconds, 16 | // because millisecond resolution is inconsistent among various underlying systems. 17 | DateTime utcNow = DateTime.UtcNow; 18 | return utcNow.AddMilliseconds(-utcNow.Millisecond); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /UnicornStore/Controllers/CheckoutController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Logging; 10 | using UnicornStore.Models; 11 | 12 | namespace UnicornStore.Controllers 13 | { 14 | [Authorize] 15 | public class CheckoutController : Controller 16 | { 17 | private const string PromoCode = "FREE"; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public CheckoutController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | // 27 | // GET: /Checkout/ 28 | public IActionResult AddressAndPayment() 29 | { 30 | return View(); 31 | } 32 | 33 | // 34 | // POST: /Checkout/AddressAndPayment 35 | 36 | [HttpPost] 37 | [ValidateAntiForgeryToken] 38 | public async Task AddressAndPayment( 39 | [FromServices] UnicornStoreContext dbContext, 40 | [FromForm] Order order, 41 | CancellationToken requestAborted) 42 | { 43 | if (!ModelState.IsValid) 44 | { 45 | return View(order); 46 | } 47 | 48 | var formCollection = await HttpContext.Request.ReadFormAsync(); 49 | 50 | try 51 | { 52 | if (string.Equals(formCollection["PromoCode"].FirstOrDefault(), PromoCode, 53 | StringComparison.OrdinalIgnoreCase) == false) 54 | { 55 | return View(order); 56 | } 57 | else 58 | { 59 | order.Username = HttpContext.User.Identity.Name; 60 | order.OrderDate = DateTime.Now; 61 | 62 | //Add the Order 63 | // TODO: investigate why intermediary SaveChangesAsync() is necessary. 64 | await dbContext.Orders.AddAsync(order); 65 | await dbContext.SaveChangesAsync(); 66 | 67 | //Process the order 68 | var cart = ShoppingCart.GetCart(dbContext, HttpContext); 69 | await cart.CreateOrder(order); 70 | 71 | // Save all changes 72 | await dbContext.SaveChangesAsync(requestAborted); 73 | 74 | _logger.LogInformation("User {userName} started checkout of {orderId}.", order.Username, order.OrderId); 75 | 76 | return RedirectToAction("Complete", new { id = order.OrderId }); 77 | } 78 | } 79 | catch (Exception ex) 80 | { 81 | _logger.LogError(ex, "Checkout failed"); 82 | //Invalid - redisplay with errors 83 | return View(order); 84 | } 85 | } 86 | 87 | // 88 | // GET: /Checkout/Complete 89 | 90 | public async Task Complete( 91 | [FromServices] UnicornStoreContext dbContext, 92 | int id) 93 | { 94 | var userName = HttpContext.User.Identity.Name; 95 | 96 | // Validate customer owns this order 97 | bool isValid = await dbContext.Orders.AnyAsync( 98 | o => o.OrderId == id && 99 | o.Username == userName); 100 | 101 | if (isValid) 102 | { 103 | _logger.LogInformation("User {userName} completed checkout on order {orderId}.", userName, id); 104 | return View(id); 105 | } 106 | else 107 | { 108 | _logger.LogError( 109 | "User {userName} tried to checkout with an order ({orderId}) that doesn't belong to them.", 110 | userName, 111 | id); 112 | return View("Error"); 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /UnicornStore/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 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Caching.Memory; 8 | using Microsoft.Extensions.Options; 9 | using UnicornStore.Models; 10 | 11 | namespace UnicornStore.Controllers 12 | { 13 | public class HomeController : Controller 14 | { 15 | private readonly AppSettings _appSettings; 16 | 17 | public HomeController(IOptions options) 18 | { 19 | _appSettings = options.Value; 20 | } 21 | // 22 | // GET: /Home/ 23 | public async Task Index( 24 | [FromServices] UnicornStoreContext dbContext, 25 | [FromServices] IMemoryCache cache) 26 | { 27 | // Get most popular blessings 28 | var cacheKey = "topselling"; 29 | List blessings; 30 | if (!cache.TryGetValue(cacheKey, out blessings)) 31 | { 32 | blessings = await GetTopSellingBlessingsAsync(dbContext, 6); 33 | 34 | if (blessings != null && blessings.Count > 0) 35 | { 36 | if (_appSettings.CacheDbResults) 37 | { 38 | // Refresh it every 10 minutes. 39 | // Let this be the last item to be removed by cache if cache GC kicks in. 40 | cache.Set( 41 | cacheKey, 42 | blessings, 43 | new MemoryCacheEntryOptions() 44 | .SetAbsoluteExpiration(TimeSpan.FromMinutes(10)) 45 | .SetPriority(CacheItemPriority.High)); 46 | } 47 | } 48 | } 49 | 50 | return View(blessings); 51 | } 52 | 53 | public IActionResult Error() 54 | { 55 | return View("~/Views/Shared/Error.cshtml"); 56 | } 57 | 58 | public IActionResult StatusCodePage() 59 | { 60 | return View("~/Views/Shared/StatusCodePage.cshtml"); 61 | } 62 | 63 | public IActionResult AccessDenied() 64 | { 65 | return View("~/Views/Shared/AccessDenied.cshtml"); 66 | } 67 | 68 | private Task> GetTopSellingBlessingsAsync(UnicornStoreContext dbContext, int count) 69 | { 70 | // Group the order details by blessing and return 71 | // the blessings with the highest count 72 | 73 | return dbContext.Blessings 74 | .OrderByDescending(a => a.OrderDetails.Count) 75 | .Take(count) 76 | .ToListAsync(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /UnicornStore/Controllers/ShoppingCartController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | using UnicornStore.Models; 8 | using UnicornStore.ViewModels; 9 | 10 | namespace UnicornStore.Controllers 11 | { 12 | public class ShoppingCartController : Controller 13 | { 14 | private readonly ILogger _logger; 15 | 16 | public ShoppingCartController(UnicornStoreContext dbContext, ILogger logger) 17 | { 18 | DbContext = dbContext; 19 | _logger = logger; 20 | } 21 | 22 | public UnicornStoreContext DbContext { get; } 23 | 24 | // 25 | // GET: /ShoppingCart/ 26 | public async Task Index() 27 | { 28 | var cart = ShoppingCart.GetCart(DbContext, HttpContext); 29 | 30 | // Set up our ViewModel 31 | var viewModel = new ShoppingCartViewModel 32 | { 33 | CartItems = await cart.GetCartItems(), 34 | CartTotal = await cart.GetTotal() 35 | }; 36 | 37 | // Return the view 38 | return View(viewModel); 39 | } 40 | 41 | // 42 | // GET: /ShoppingCart/AddToCart/5 43 | 44 | public async Task AddToCart(int id, CancellationToken requestAborted) 45 | { 46 | // Retrieve the blessing from the database 47 | var addedBlessing = await DbContext.Blessings 48 | .SingleAsync(blessing => blessing.BlessingId == id); 49 | 50 | // Add it to the shopping cart 51 | var cart = ShoppingCart.GetCart(DbContext, HttpContext); 52 | 53 | await cart.AddToCart(addedBlessing); 54 | 55 | await DbContext.SaveChangesAsync(requestAborted); 56 | _logger.LogInformation("Blessing {blessingId} was added to the cart.", addedBlessing.BlessingId); 57 | 58 | // Go back to the main store page for more shopping 59 | return RedirectToAction("Index"); 60 | } 61 | 62 | // 63 | // AJAX: /ShoppingCart/RemoveFromCart/5 64 | [HttpPost] 65 | [ValidateAntiForgeryToken] 66 | public async Task RemoveFromCart( 67 | int id, 68 | CancellationToken requestAborted) 69 | { 70 | // Retrieve the current user's shopping cart 71 | var cart = ShoppingCart.GetCart(DbContext, HttpContext); 72 | 73 | // Get the name of the blessing to display confirmation 74 | var cartItem = await DbContext.CartItems 75 | .Where(item => item.CartItemId == id) 76 | .Include(c => c.Blessing) 77 | .SingleOrDefaultAsync(); 78 | 79 | string message; 80 | int itemCount; 81 | if (cartItem != null) 82 | { 83 | // Remove from cart 84 | itemCount = cart.RemoveFromCart(id); 85 | 86 | await DbContext.SaveChangesAsync(requestAborted); 87 | 88 | string removed = (itemCount > 0) ? " 1 copy of " : string.Empty; 89 | message = removed + cartItem.Blessing.Title + " has been removed from your shopping cart."; 90 | } 91 | else 92 | { 93 | itemCount = 0; 94 | message = "Could not find this item, nothing has been removed from your shopping cart."; 95 | } 96 | 97 | // Display the confirmation message 98 | 99 | var results = new ShoppingCartRemoveViewModel 100 | { 101 | Message = message, 102 | CartTotal = await cart.GetTotal(), 103 | CartCount = await cart.GetCount(), 104 | ItemCount = itemCount, 105 | DeleteId = id 106 | }; 107 | 108 | _logger.LogInformation("Blessing {id} was removed from a cart.", id); 109 | 110 | return Json(results); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /UnicornStore/Controllers/StoreController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Microsoft.Extensions.Options; 8 | using UnicornStore.Models; 9 | 10 | namespace UnicornStore.Controllers 11 | { 12 | public class StoreController : Controller 13 | { 14 | private readonly AppSettings _appSettings; 15 | 16 | public StoreController(UnicornStoreContext dbContext, IOptions options) 17 | { 18 | DbContext = dbContext; 19 | _appSettings = options.Value; 20 | } 21 | 22 | public UnicornStoreContext DbContext { get; } 23 | 24 | // 25 | // GET: /Store/ 26 | public async Task Index() 27 | { 28 | var genres = await DbContext.Genres.ToListAsync(); 29 | 30 | return View(genres); 31 | } 32 | 33 | // 34 | // GET: /Store/Browse?genre=Disco 35 | public async Task Browse(string genre) 36 | { 37 | // Retrieve Genre genre and its Associated associated Blessings blessings from database 38 | var genreModel = await DbContext.Genres 39 | .Include(g => g.Blessings) 40 | .Where(g => g.Name == genre) 41 | .FirstOrDefaultAsync(); 42 | 43 | if (genreModel == null) 44 | { 45 | return NotFound(); 46 | } 47 | 48 | return View(genreModel); 49 | } 50 | 51 | public async Task Details( 52 | [FromServices] IMemoryCache cache, 53 | int id) 54 | { 55 | var cacheKey = string.Format("blessing_{0}", id); 56 | Blessing blessing; 57 | if (!cache.TryGetValue(cacheKey, out blessing)) 58 | { 59 | blessing = await DbContext.Blessings 60 | .Where(a => a.BlessingId == id) 61 | .Include(a => a.Unicorn) 62 | .Include(a => a.Genre) 63 | .FirstOrDefaultAsync(); 64 | 65 | if (blessing != null) 66 | { 67 | if (_appSettings.CacheDbResults) 68 | { 69 | //Remove it from cache if not retrieved in last 10 minutes 70 | cache.Set( 71 | cacheKey, 72 | blessing, 73 | new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10))); 74 | } 75 | } 76 | } 77 | 78 | if (blessing == null) 79 | { 80 | return NotFound(); 81 | } 82 | 83 | return View(blessing); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /UnicornStore/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.2-alpine3.11 AS base 2 | # https://github.com/dotnet/SqlClient/issues/220 3 | RUN apk add libgdiplus --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ --allow-untrusted && \ 4 | apk add terminus-font && \ 5 | apk add icu-libs 6 | ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false 7 | WORKDIR /app 8 | EXPOSE 80 9 | 10 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build 11 | WORKDIR /src 12 | COPY UnicornStore/UnicornStore.csproj UnicornStore/ 13 | RUN dotnet restore UnicornStore/UnicornStore.csproj 14 | COPY . . 15 | WORKDIR /src/UnicornStore 16 | RUN dotnet build UnicornStore.csproj -c Release -o /app 17 | 18 | FROM build AS publish 19 | RUN dotnet publish UnicornStore.csproj -c Release -o /app 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app . 24 | ENTRYPOINT ["dotnet", "UnicornStore.dll"] 25 | HEALTHCHECK --interval=30s --timeout=5s --retries=5 --start-period=30s CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1 26 | -------------------------------------------------------------------------------- /UnicornStore/MessageServices.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace UnicornStore 4 | { 5 | public static class MessageServices 6 | { 7 | public static Task SendEmailAsync(string email, string subject, string message) 8 | { 9 | // Plug in your email service 10 | return Task.FromResult(0); 11 | } 12 | 13 | public static Task SendSmsAsync(string number, string message) 14 | { 15 | // Plug in your sms service 16 | return Task.FromResult(0); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /UnicornStore/Models/AccountViewModels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using Microsoft.AspNetCore.Mvc.Rendering; 4 | 5 | namespace UnicornStore.Models 6 | { 7 | public class ExternalLoginConfirmationViewModel 8 | { 9 | [Required] 10 | [Display(Name = "Email")] 11 | public string Email { get; set; } 12 | } 13 | 14 | public class ExternalLoginListViewModel 15 | { 16 | public string ReturnUrl { get; set; } 17 | } 18 | 19 | public class SendCodeViewModel 20 | { 21 | public string SelectedProvider { get; set; } 22 | public ICollection Providers { get; set; } 23 | public string ReturnUrl { get; set; } 24 | public bool RememberMe { get; set; } 25 | } 26 | 27 | public class VerifyCodeViewModel 28 | { 29 | [Required] 30 | public string Provider { get; set; } 31 | 32 | [Required] 33 | [Display(Name = "Code")] 34 | public string Code { get; set; } 35 | public string ReturnUrl { get; set; } 36 | 37 | [Display(Name = "Remember this browser?")] 38 | public bool RememberBrowser { get; set; } 39 | 40 | public bool RememberMe { get; set; } 41 | } 42 | 43 | public class ForgotViewModel 44 | { 45 | [Required] 46 | [Display(Name = "Email")] 47 | public string Email { get; set; } 48 | } 49 | 50 | public class LoginViewModel 51 | { 52 | [Required] 53 | [Display(Name = "Email")] 54 | [EmailAddress] 55 | public string Email { get; set; } 56 | 57 | [Required] 58 | [DataType(DataType.Password)] 59 | [Display(Name = "Password")] 60 | public string Password { get; set; } 61 | 62 | [Display(Name = "Remember me?")] 63 | public bool RememberMe { get; set; } 64 | } 65 | 66 | public class RegisterViewModel 67 | { 68 | [Required] 69 | [EmailAddress] 70 | [Display(Name = "Email")] 71 | public string Email { get; set; } 72 | 73 | [Required] 74 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 75 | [DataType(DataType.Password)] 76 | [Display(Name = "Password")] 77 | public string Password { get; set; } 78 | 79 | [DataType(DataType.Password)] 80 | [Display(Name = "Confirm password")] 81 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 82 | public string ConfirmPassword { get; set; } 83 | } 84 | 85 | public class ResetPasswordViewModel 86 | { 87 | [Required] 88 | [EmailAddress] 89 | [Display(Name = "Email")] 90 | public string Email { get; set; } 91 | 92 | [Required] 93 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 94 | [DataType(DataType.Password)] 95 | [Display(Name = "Password")] 96 | public string Password { get; set; } 97 | 98 | [DataType(DataType.Password)] 99 | [Display(Name = "Confirm password")] 100 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 101 | public string ConfirmPassword { get; set; } 102 | 103 | public string Code { get; set; } 104 | } 105 | 106 | public class ForgotPasswordViewModel 107 | { 108 | [Required] 109 | [EmailAddress] 110 | [Display(Name = "Email")] 111 | public string Email { get; set; } 112 | } 113 | } -------------------------------------------------------------------------------- /UnicornStore/Models/Blessing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.AspNetCore.Mvc.ModelBinding; 6 | 7 | namespace UnicornStore.Models 8 | { 9 | public class Blessing 10 | { 11 | [ScaffoldColumn(false)] 12 | public int BlessingId { get; set; } 13 | 14 | public int GenreId { get; set; } 15 | 16 | public int UnicornId { get; set; } 17 | 18 | [Required] 19 | [StringLength(160, MinimumLength = 2)] 20 | public string Title { get; set; } 21 | 22 | [Required] 23 | [Range(0.01, 100.00)] 24 | 25 | [DataType(DataType.Currency)] 26 | [Column(TypeName = "decimal(18,2)")] 27 | public decimal Price { get; set; } 28 | 29 | [Display(Name = "Blessing Art URL")] 30 | [StringLength(1024)] 31 | public string BlessingArtUrl { get; set; } 32 | 33 | public virtual Genre Genre { get; set; } 34 | public virtual Unicorn Unicorn { get; set; } 35 | public virtual List OrderDetails { get; set; } 36 | 37 | [ScaffoldColumn(false)] 38 | [BindNever] 39 | [Required] 40 | public DateTime Created { get; set; } 41 | 42 | /// 43 | /// TODO: Temporary hack to populate the orderdetails until EF does this automatically. 44 | /// 45 | public Blessing() 46 | { 47 | OrderDetails = new List(); 48 | Created = DateTime.UtcNow; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /UnicornStore/Models/CartItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace UnicornStore.Models 5 | { 6 | public class CartItem 7 | { 8 | [Key] 9 | public int CartItemId { get; set; } 10 | 11 | [Required] 12 | public string CartId { get; set; } 13 | public int BlessingId { get; set; } 14 | public int Count { get; set; } 15 | 16 | [DataType(DataType.DateTime)] 17 | public DateTime DateCreated { get; set; } 18 | 19 | public virtual Blessing Blessing { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /UnicornStore/Models/Genre.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace UnicornStore.Models 5 | { 6 | public class Genre 7 | { 8 | public int GenreId { get; set; } 9 | 10 | [Required] 11 | public string Name { get; set; } 12 | 13 | public string Description { get; set; } 14 | 15 | public List Blessings { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /UnicornStore/Models/ManageViewModels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace UnicornStore.Models 8 | { 9 | public class IndexViewModel 10 | { 11 | public bool HasPassword { get; set; } 12 | public IList Logins { get; set; } 13 | public string PhoneNumber { get; set; } 14 | public bool TwoFactor { get; set; } 15 | public bool BrowserRemembered { get; set; } 16 | } 17 | 18 | public class ManageLoginsViewModel 19 | { 20 | public IList CurrentLogins { get; set; } 21 | public IList OtherLogins { get; set; } 22 | } 23 | 24 | public class FactorViewModel 25 | { 26 | public string Purpose { get; set; } 27 | } 28 | 29 | public class SetPasswordViewModel 30 | { 31 | [Required] 32 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 33 | [DataType(DataType.Password)] 34 | [Display(Name = "New password")] 35 | public string NewPassword { get; set; } 36 | 37 | [DataType(DataType.Password)] 38 | [Display(Name = "Confirm new password")] 39 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 40 | public string ConfirmPassword { get; set; } 41 | } 42 | 43 | public class ChangePasswordViewModel 44 | { 45 | [Required] 46 | [DataType(DataType.Password)] 47 | [Display(Name = "Current password")] 48 | public string OldPassword { get; set; } 49 | 50 | [Required] 51 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 52 | [DataType(DataType.Password)] 53 | [Display(Name = "New password")] 54 | public string NewPassword { get; set; } 55 | 56 | [DataType(DataType.Password)] 57 | [Display(Name = "Confirm new password")] 58 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 59 | public string ConfirmPassword { get; set; } 60 | } 61 | 62 | public class AddPhoneNumberViewModel 63 | { 64 | [Required] 65 | [Phone] 66 | [Display(Name = "Phone Number")] 67 | public string Number { get; set; } 68 | } 69 | 70 | public class VerifyPhoneNumberViewModel 71 | { 72 | [Required] 73 | [Display(Name = "Code")] 74 | public string Code { get; set; } 75 | 76 | [Required] 77 | [Phone] 78 | [Display(Name = "Phone Number")] 79 | public string PhoneNumber { get; set; } 80 | } 81 | 82 | public class ConfigureTwoFactorViewModel 83 | { 84 | public string SelectedProvider { get; set; } 85 | public ICollection Providers { get; set; } 86 | } 87 | } -------------------------------------------------------------------------------- /UnicornStore/Models/MusicStoreContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace UnicornStore.Models 6 | { 7 | public class ApplicationUser : IdentityUser { } 8 | 9 | public class UnicornStoreContext : IdentityDbContext 10 | { 11 | public UnicornStoreContext(DbContextOptions options) 12 | : base(options) 13 | { 14 | // TODO: #639 15 | //ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 16 | } 17 | 18 | public DbSet Blessings { get; set; } 19 | public DbSet Unicorns { get; set; } 20 | public DbSet Orders { get; set; } 21 | public DbSet Genres { get; set; } 22 | public DbSet CartItems { get; set; } 23 | public DbSet OrderDetails { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /UnicornStore/Models/Order.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using Microsoft.AspNetCore.Mvc.ModelBinding; 5 | 6 | namespace UnicornStore.Models 7 | { 8 | //[Bind(Include = "FirstName,LastName,Address,City,State,PostalCode,Country,Phone,Email")] 9 | public class Order 10 | { 11 | [BindNever] 12 | [ScaffoldColumn(false)] 13 | public int OrderId { get; set; } 14 | 15 | [BindNever] 16 | [ScaffoldColumn(false)] 17 | public System.DateTime OrderDate { get; set; } 18 | 19 | [BindNever] 20 | [ScaffoldColumn(false)] 21 | public string Username { get; set; } 22 | 23 | [Required] 24 | [Display(Name = "First Name")] 25 | [StringLength(160)] 26 | public string FirstName { get; set; } 27 | 28 | [Required] 29 | [Display(Name = "Last Name")] 30 | [StringLength(160)] 31 | public string LastName { get; set; } 32 | 33 | [Required] 34 | [StringLength(70, MinimumLength = 3)] 35 | public string Address { get; set; } 36 | 37 | [Required] 38 | [StringLength(40)] 39 | public string City { get; set; } 40 | 41 | [Required] 42 | [StringLength(40)] 43 | public string State { get; set; } 44 | 45 | [Required] 46 | [Display(Name = "Postal Code")] 47 | [StringLength(10, MinimumLength = 5)] 48 | public string PostalCode { get; set; } 49 | 50 | [Required] 51 | [StringLength(40)] 52 | public string Country { get; set; } 53 | 54 | [Required] 55 | [StringLength(24)] 56 | [DataType(DataType.PhoneNumber)] 57 | public string Phone { get; set; } 58 | 59 | [Required] 60 | [Display(Name = "Email Address")] 61 | [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", 62 | ErrorMessage = "Email is not valid.")] 63 | [DataType(DataType.EmailAddress)] 64 | public string Email { get; set; } 65 | 66 | [BindNever] 67 | [ScaffoldColumn(false)] 68 | [Column(TypeName = "decimal(18,2)")] 69 | public decimal Total { get; set; } 70 | 71 | [BindNever] 72 | public List OrderDetails { get; set; } 73 | } 74 | } -------------------------------------------------------------------------------- /UnicornStore/Models/OrderDetail.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace UnicornStore.Models 4 | { 5 | public class OrderDetail 6 | { 7 | public int OrderDetailId { get; set; } 8 | 9 | public int OrderId { get; set; } 10 | 11 | public int BlessingId { get; set; } 12 | 13 | public int Quantity { get; set; } 14 | 15 | [Column(TypeName = "decimal(18,2)")] 16 | public decimal UnitPrice { get; set; } 17 | 18 | public virtual Blessing Blessing { get; set; } 19 | 20 | public virtual Order Order { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /UnicornStore/Models/ShoppingCart.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.EntityFrameworkCore; 7 | 8 | namespace UnicornStore.Models 9 | { 10 | public class ShoppingCart 11 | { 12 | private readonly UnicornStoreContext _dbContext; 13 | private readonly string _shoppingCartId; 14 | 15 | private ShoppingCart(UnicornStoreContext dbContext, string id) 16 | { 17 | _dbContext = dbContext; 18 | _shoppingCartId = id; 19 | } 20 | 21 | public static ShoppingCart GetCart(UnicornStoreContext db, HttpContext context) 22 | => GetCart(db, GetCartId(context)); 23 | 24 | public static ShoppingCart GetCart(UnicornStoreContext db, string cartId) 25 | => new ShoppingCart(db, cartId); 26 | 27 | public async Task AddToCart(Blessing blessing) 28 | { 29 | // Get the matching cart and blessing instances 30 | var cartItem = await _dbContext.CartItems.SingleOrDefaultAsync( 31 | c => c.CartId == _shoppingCartId 32 | && c.BlessingId == blessing.BlessingId); 33 | 34 | if (cartItem == null) 35 | { 36 | // Create a new cart item if no cart item exists 37 | cartItem = new CartItem 38 | { 39 | BlessingId = blessing.BlessingId, 40 | CartId = _shoppingCartId, 41 | Count = 1, 42 | DateCreated = DateTime.Now 43 | }; 44 | 45 | _dbContext.CartItems.Add(cartItem); 46 | } 47 | else 48 | { 49 | // If the item does exist in the cart, then add one to the quantity 50 | cartItem.Count++; 51 | } 52 | } 53 | 54 | public int RemoveFromCart(int id) 55 | { 56 | // Get the cart 57 | var cartItem = _dbContext.CartItems.SingleOrDefault( 58 | cart => cart.CartId == _shoppingCartId 59 | && cart.CartItemId == id); 60 | 61 | int itemCount = 0; 62 | 63 | if (cartItem != null) 64 | { 65 | if (cartItem.Count > 1) 66 | { 67 | cartItem.Count--; 68 | itemCount = cartItem.Count; 69 | } 70 | else 71 | { 72 | _dbContext.CartItems.Remove(cartItem); 73 | } 74 | } 75 | 76 | return itemCount; 77 | } 78 | 79 | public async Task EmptyCart() 80 | { 81 | var cartItems = await _dbContext 82 | .CartItems 83 | .Where(cart => cart.CartId == _shoppingCartId) 84 | .ToArrayAsync(); 85 | 86 | _dbContext.CartItems.RemoveRange(cartItems); 87 | } 88 | 89 | public Task> GetCartItems() 90 | { 91 | return _dbContext 92 | .CartItems 93 | .Where(cart => cart.CartId == _shoppingCartId) 94 | .Include(c => c.Blessing) 95 | .ToListAsync(); 96 | } 97 | 98 | public Task> GetCartBlessingTitles() 99 | { 100 | return _dbContext 101 | .CartItems 102 | .Where(cart => cart.CartId == _shoppingCartId) 103 | .Select(c => c.Blessing.Title) 104 | .OrderBy(n => n) 105 | .ToListAsync(); 106 | } 107 | 108 | public Task GetCount() 109 | { 110 | // Get the count of each item in the cart and sum them up 111 | return _dbContext 112 | .CartItems 113 | .Where(c => c.CartId == _shoppingCartId) 114 | .Select(c => c.Count) 115 | .SumAsync(); 116 | } 117 | 118 | public Task GetTotal() 119 | { 120 | // Multiply blessing price by count of that blessing to get 121 | // the current price for each of those blessings in the cart 122 | // sum all blessing price totals to get the cart total 123 | 124 | return _dbContext 125 | .CartItems 126 | .Where(c => c.CartId == _shoppingCartId) 127 | .Select(c => c.Blessing.Price * c.Count) 128 | .SumAsync(); 129 | } 130 | 131 | public async Task CreateOrder(Order order) 132 | { 133 | decimal orderTotal = 0; 134 | 135 | var cartItems = await GetCartItems(); 136 | 137 | // Iterate over the items in the cart, adding the order details for each 138 | foreach (var item in cartItems) 139 | { 140 | //var blessing = _db.Blessings.Find(item.BlessingId); 141 | var blessing = await _dbContext.Blessings.SingleAsync(a => a.BlessingId == item.BlessingId); 142 | 143 | var orderDetail = new OrderDetail 144 | { 145 | BlessingId = item.BlessingId, 146 | OrderId = order.OrderId, 147 | UnitPrice = blessing.Price, 148 | Quantity = item.Count, 149 | }; 150 | 151 | // Set the order total of the shopping cart 152 | orderTotal += (item.Count * blessing.Price); 153 | 154 | _dbContext.OrderDetails.Add(orderDetail); 155 | } 156 | 157 | // Set the order's total to the orderTotal count 158 | order.Total = orderTotal; 159 | 160 | // Empty the shopping cart 161 | await EmptyCart(); 162 | 163 | // Return the OrderId as the confirmation number 164 | return order.OrderId; 165 | } 166 | 167 | // We're using HttpContextBase to allow access to sessions. 168 | private static string GetCartId(HttpContext context) 169 | { 170 | var cartId = context.Session.GetString("Session"); 171 | 172 | if (cartId == null) 173 | { 174 | //A GUID to hold the cartId. 175 | cartId = Guid.NewGuid().ToString(); 176 | 177 | // Send cart Id as a cookie to the client. 178 | context.Session.SetString("Session", cartId); 179 | } 180 | 181 | return cartId; 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /UnicornStore/Models/Unicorn.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace UnicornStore.Models 4 | { 5 | public class Unicorn 6 | { 7 | public int UnicornId { get; set; } 8 | 9 | [Required] 10 | public string Name { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /UnicornStore/Pages/PageThatThrows.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @{ throw new InvalidOperationException(); } 3 | -------------------------------------------------------------------------------- /UnicornStore/Platform.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace UnicornStore 5 | { 6 | internal class Platform 7 | { 8 | // Defined in winnt.h 9 | private const int PRODUCT_NANO_SERVER = 0x0000006D; 10 | private const int PRODUCT_DATACENTER_NANO_SERVER = 0x0000008F; 11 | private const int PRODUCT_STANDARD_NANO_SERVER = 0x00000090; 12 | 13 | [DllImport("api-ms-win-core-sysinfo-l1-2-1.dll", SetLastError = false)] 14 | private static extern bool GetProductInfo( 15 | int dwOSMajorVersion, 16 | int dwOSMinorVersion, 17 | int dwSpMajorVersion, 18 | int dwSpMinorVersion, 19 | out int pdwReturnedProductType); 20 | 21 | private bool? _isNano; 22 | private bool? _isWindows; 23 | 24 | public bool IsRunningOnWindows 25 | { 26 | get 27 | { 28 | if (_isWindows == null) 29 | { 30 | _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 31 | } 32 | 33 | return _isWindows.Value; 34 | } 35 | } 36 | 37 | public bool IsRunningOnNanoServer 38 | { 39 | get 40 | { 41 | if (_isNano == null) 42 | { 43 | var osVersion = new Version(RtlGetVersion() ?? string.Empty); 44 | 45 | try 46 | { 47 | int productType; 48 | if (GetProductInfo(osVersion.Major, osVersion.Minor, 0, 0, out productType)) 49 | { 50 | _isNano = productType == PRODUCT_NANO_SERVER || 51 | productType == PRODUCT_DATACENTER_NANO_SERVER || 52 | productType == PRODUCT_STANDARD_NANO_SERVER; 53 | } 54 | else 55 | { 56 | _isNano = false; 57 | } 58 | } 59 | catch 60 | { 61 | // If the API call fails, the API set is not there which means 62 | // that we are definetely not running on Nano 63 | _isNano = false; 64 | } 65 | } 66 | 67 | return _isNano.Value; 68 | } 69 | } 70 | 71 | // Sql client not available on mono, non-windows, or nano 72 | public bool UseInMemoryStore 73 | { 74 | get 75 | { 76 | return !IsRunningOnWindows || IsRunningOnNanoServer; 77 | } 78 | } 79 | 80 | [StructLayout(LayoutKind.Sequential)] 81 | internal struct RTL_OSVERSIONINFOEX 82 | { 83 | internal uint dwOSVersionInfoSize; 84 | internal uint dwMajorVersion; 85 | internal uint dwMinorVersion; 86 | internal uint dwBuildNumber; 87 | internal uint dwPlatformId; 88 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] 89 | internal string szCSDVersion; 90 | } 91 | 92 | // This call avoids the shimming Windows does to report old versions 93 | [DllImport("ntdll")] 94 | private static extern int RtlGetVersion(out RTL_OSVERSIONINFOEX lpVersionInformation); 95 | 96 | internal static string RtlGetVersion() 97 | { 98 | RTL_OSVERSIONINFOEX osvi = new RTL_OSVERSIONINFOEX(); 99 | osvi.dwOSVersionInfoSize = (uint)Marshal.SizeOf(osvi); 100 | if (RtlGetVersion(out osvi) == 0) 101 | { 102 | return $"{osvi.dwMajorVersion}.{osvi.dwMinorVersion}.{osvi.dwBuildNumber}"; 103 | } 104 | else 105 | { 106 | return null; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /UnicornStore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using UnicornStore.Data; 9 | using UnicornStore.Models; 10 | 11 | namespace UnicornStore 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | var host = CreateWebHostBuilder(args).Build(); 18 | 19 | using (var scope = host.Services.CreateScope()) 20 | { 21 | var services = scope.ServiceProvider; 22 | try 23 | { 24 | var context = services.GetRequiredService(); 25 | var env = services.GetRequiredService(); 26 | //Populates the UnicornStore sample data and creates the DB if it doesn't exist. 27 | DbInitializer.Initialize(context, services, env).Wait(); 28 | } 29 | catch (Exception ex) 30 | { 31 | var logger = services.GetRequiredService>(); 32 | logger.LogError(ex, "An error occurred while seeding the database."); 33 | } 34 | } 35 | host.Run(); 36 | } 37 | 38 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 39 | WebHost.CreateDefaultBuilder(args) 40 | .ConfigureAppConfiguration((hostingContext, config) => 41 | { 42 | config.SetBasePath(Directory.GetCurrentDirectory()); 43 | config.AddCommandLine(args); 44 | }) 45 | .UseStartup(); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /UnicornStore/Properties/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace UnicornStore 2 | { 3 | public class AppSettings 4 | { 5 | public string SiteTitle { get; set; } 6 | 7 | public bool CacheDbResults { get; set; } = true; 8 | } 9 | } -------------------------------------------------------------------------------- /UnicornStore/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iis": { 6 | "applicationUrl": "http://localhost:39584", 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 | "Docker": { 19 | "commandName": "Docker", 20 | "launchBrowser": true, 21 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}" 22 | }, 23 | "UnicornStore": { 24 | "commandName": "Project", 25 | "launchBrowser": true, 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | }, 29 | "applicationUrl": "http://localhost:8080" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /UnicornStore/Scripts/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | -------------------------------------------------------------------------------- /UnicornStore/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Globalization; 3 | using System.Runtime.InteropServices; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Localization; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using UnicornStore.Components; 13 | using UnicornStore.Models; 14 | using System.Text.Json; 15 | using HealthChecks.UI.Client; 16 | using Microsoft.Extensions.Diagnostics.HealthChecks; 17 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 18 | 19 | namespace UnicornStore 20 | { 21 | public class Startup 22 | { 23 | private string _connection = null; 24 | 25 | public Startup(IConfiguration configuration) 26 | { 27 | Configuration = configuration; 28 | } 29 | 30 | public IConfiguration Configuration { get; private set; } 31 | 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | // The UNICORNSTORE_DBSECRET is stored in AWS Secrets Manager 35 | // The value is loaded as an Environment Variable in a JSON string 36 | // The key/value pairs are mapped to the Configuration 37 | if (Configuration["UNICORNSTORE_DBSECRET"] != null) 38 | { 39 | var unicorn_envvariables = Configuration["UNICORNSTORE_DBSECRET"]; 40 | var document = JsonDocument.Parse(unicorn_envvariables); 41 | var root = document.RootElement; 42 | Configuration["UNICORNSTORE_DBSECRET:username"] = root.GetProperty("username").GetString(); 43 | Configuration["UNICORNSTORE_DBSECRET:password"] = root.GetProperty("password").GetString(); 44 | Configuration["UNICORNSTORE_DBSECRET:host"] = root.GetProperty("host").GetString(); 45 | } 46 | 47 | var sqlconnectionbuilder = new SqlConnectionStringBuilder( 48 | Configuration.GetConnectionString("UnicornStore")); 49 | sqlconnectionbuilder.Password = Configuration["UNICORNSTORE_DBSECRET:password"]; 50 | sqlconnectionbuilder.UserID = Configuration["UNICORNSTORE_DBSECRET:username"]; 51 | sqlconnectionbuilder.DataSource = Configuration["UNICORNSTORE_DBSECRET:host"]; 52 | _connection = sqlconnectionbuilder.ConnectionString; 53 | 54 | services.AddDbContext(options => 55 | options.UseSqlServer(_connection)); 56 | 57 | 58 | // Add Identity services to the services container 59 | services.AddIdentity() 60 | .AddEntityFrameworkStores() 61 | .AddDefaultTokenProviders(); 62 | 63 | services.ConfigureApplicationCookie(options => options.AccessDeniedPath = "/Home/AccessDenied"); 64 | 65 | services.AddCors(options => 66 | { 67 | options.AddPolicy("CorsPolicy", builder => 68 | { 69 | builder.WithOrigins("http://example.com"); 70 | }); 71 | }); 72 | 73 | services.AddLogging(); 74 | 75 | // Add MVC services to the services container 76 | services.AddControllersWithViews(); 77 | services.AddRazorPages(); 78 | 79 | services.AddOptions(); 80 | 81 | // Add the Healthchecks 82 | // https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks 83 | // AspNetCore.Diagnostics.HealthChecks isn't maintained or supported by Microsoft. 84 | services.AddHealthChecks() 85 | .AddCheck("self", () => HealthCheckResult.Healthy()) 86 | .AddSqlServer(_connection, 87 | name: "UnicornDB-check", 88 | tags: new string[] { "UnicornDB" }); 89 | 90 | // Add memory cache services 91 | services.AddMemoryCache(); 92 | services.AddDistributedMemoryCache(); 93 | 94 | // Add session related services. 95 | services.AddSession(); 96 | 97 | // Add the system clock service 98 | services.AddSingleton(); 99 | 100 | // Configure Auth 101 | services.AddAuthorization(options => 102 | { 103 | options.AddPolicy( 104 | "ManageStore", 105 | authBuilder => 106 | { 107 | authBuilder.RequireClaim("ManageStore", "Allowed"); 108 | }); 109 | }); 110 | } 111 | 112 | 113 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 114 | { 115 | //This is invoked when ASPNETCORE_ENVIRONMENT is 'Development' or is not defined 116 | //The allowed values are Development,Staging and Production 117 | if (env.IsDevelopment()) 118 | { 119 | // StatusCode pages to gracefully handle status codes 400-599. 120 | app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage"); 121 | 122 | // Display custom error page in production when error occurs 123 | // During development use the ErrorPage middleware to display error information in the browser 124 | app.UseDeveloperExceptionPage(); 125 | 126 | app.UseDatabaseErrorPage(); 127 | } 128 | 129 | //This is invoked when ASPNETCORE_ENVIRONMENT is 'Production' or 'Staging' 130 | if (env.IsProduction() || env.IsStaging()) 131 | { 132 | // StatusCode pages to gracefully handle status codes 400-599. 133 | app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage"); 134 | 135 | app.UseExceptionHandler("/Home/Error"); 136 | } 137 | 138 | app.UseHealthChecks("/health", new HealthCheckOptions() 139 | { 140 | Predicate = _ => true, 141 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 142 | }); 143 | 144 | app.UseHealthChecks("/liveness", new HealthCheckOptions 145 | { 146 | Predicate = r => r.Name.Contains("self") 147 | }); 148 | 149 | // force the en-US culture, so that the app behaves the same even on machines with different default culture 150 | var supportedCultures = new[] { new CultureInfo("en-US") }; 151 | 152 | app.UseRequestLocalization(new RequestLocalizationOptions 153 | { 154 | DefaultRequestCulture = new RequestCulture("en-US"), 155 | SupportedCultures = supportedCultures, 156 | SupportedUICultures = supportedCultures 157 | }); 158 | 159 | app.Use((context, next) => 160 | { 161 | context.Response.Headers["Arch"] = RuntimeInformation.ProcessArchitecture.ToString(); 162 | return next(); 163 | }); 164 | 165 | // Configure Session. 166 | app.UseSession(); 167 | 168 | // Add static files to the request pipeline 169 | app.UseStaticFiles(); 170 | 171 | // Add the endpoint routing matcher middleware to the request pipeline 172 | app.UseRouting(); 173 | 174 | // Add cookie-based authentication to the request pipeline 175 | app.UseAuthentication(); 176 | 177 | // Add the authorization middleware to the request pipeline 178 | app.UseAuthorization(); 179 | 180 | app.UseEndpoints(endpoints => 181 | { 182 | endpoints.MapControllers(); 183 | endpoints.MapAreaControllerRoute( 184 | "admin", 185 | "admin", 186 | "Admin/{controller=Home}/{action=Index}/{id?}"); 187 | 188 | endpoints.MapControllerRoute( 189 | name: "default", 190 | pattern: "{controller=Home}/{action=Index}/{id?}"); 191 | 192 | endpoints.MapControllerRoute( 193 | name: "api", 194 | pattern: "{controller=Home}/{id?}"); 195 | }); 196 | } 197 | 198 | } 199 | } -------------------------------------------------------------------------------- /UnicornStore/UnicornStore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unicorn store application on ASP.NET Core 5 | netcoreapp3.1 6 | 45b651b1-da6a-44fb-af93-525b292efddb 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /UnicornStore/ViewModels/BlessingData.cs: -------------------------------------------------------------------------------- 1 | namespace UnicornStore.ViewModels 2 | { 3 | public class BlessingData 4 | { 5 | public string Title { get; set; } 6 | 7 | public string Url { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /UnicornStore/ViewModels/ShoppingCartRemoveViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace UnicornStore.ViewModels 2 | { 3 | public class ShoppingCartRemoveViewModel 4 | { 5 | public string Message { get; set; } 6 | public decimal CartTotal { get; set; } 7 | public int CartCount { get; set; } 8 | public int ItemCount { get; set; } 9 | public int DeleteId { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /UnicornStore/ViewModels/ShoppingCartViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnicornStore.Models; 3 | 4 | namespace UnicornStore.ViewModels 5 | { 6 | public class ShoppingCartViewModel 7 | { 8 | public List CartItems { get; set; } 9 | public decimal CartTotal { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UnicornStore/Views/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Confirm Email"; 3 | } 4 | 5 |

@ViewBag.Title.

6 |
7 |

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

10 |
-------------------------------------------------------------------------------- /UnicornStore/Views/Account/ExternalLoginConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model ExternalLoginConfirmationViewModel 2 | @{ 3 | ViewBag.Title = "Register"; 4 | } 5 |

@ViewBag.Title.

6 |

Associate your @ViewBag.LoginProvider account.

7 | 8 |
9 |

Association Form

10 |
11 |
12 | 13 |

14 | You've successfully authenticated with @ViewBag.LoginProvider. 15 | Please enter a user name for this site below and click the Register button to finish 16 | logging in. 17 |

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

@ViewBag.Title.

7 |

Unsuccessful login with service.

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

@ViewBag.Title.

7 | 8 |
9 |

Enter your email.

10 |
11 |
12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | @section Scripts { 27 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 28 | } 29 | -------------------------------------------------------------------------------- /UnicornStore/Views/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Forgot Password Confirmation"; 3 | } 4 | 5 |
6 |

@ViewBag.Title.

7 |
8 |
9 |

10 | Please check your email to reset your password. 11 |

12 |

13 | For demo purpose only: Click here to reset the password 14 |

15 |
-------------------------------------------------------------------------------- /UnicornStore/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginViewModel 2 | 3 | @{ 4 | ViewBag.Title = "Log in"; 5 | } 6 | 7 |

@ViewBag.Title.

8 |
9 |
10 |
11 |
12 |

Use a local account to log in.

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 |

43 | Register as a new user? 44 |

45 |

46 | Forgot your password? 47 |

48 |
49 |
50 |
51 |
52 |
53 | @await Html.PartialAsync("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) 54 |
55 |
56 |
57 | 58 | @section Scripts { 59 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 60 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @model RegisterViewModel 2 | @{ 3 | ViewBag.Title = "Register"; 4 | } 5 | 6 |

@ViewBag.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 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Account/RegisterConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Register Confirmation"; 3 | } 4 | 5 |
6 |

@ViewBag.Title.

7 |
8 |
9 |

10 | Please check your email to activate your account. 11 |

12 |

13 | Demo/testing purposes only: The sample displays the code and user id in the page: Click here to confirm your email: 14 |

15 |
-------------------------------------------------------------------------------- /UnicornStore/Views/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model ResetPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Reset password"; 4 | } 5 | 6 |

@ViewBag.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 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Reset password confirmation"; 3 | } 4 | 5 |
6 |

@ViewBag.Title.

7 |
8 |
9 |

10 | Your password has been reset. Please Click here to log in. 11 |

12 |
-------------------------------------------------------------------------------- /UnicornStore/Views/Account/SendCode.cshtml: -------------------------------------------------------------------------------- 1 | @model SendCodeViewModel 2 | @{ 3 | ViewBag.Title = "Send Verification Code"; 4 | } 5 | 6 |

@ViewBag.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 | -------------------------------------------------------------------------------- /UnicornStore/Views/Account/VerifyCode.cshtml: -------------------------------------------------------------------------------- 1 | @model VerifyCodeViewModel 2 | @{ 3 | ViewBag.Title = "Verify"; 4 | } 5 | 6 |

@ViewBag.Title.

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

Enter verification code

13 |

14 | For DEMO only: You can type in this code in the below text box to proceed: [ @ViewBag.Code ] 15 |
16 | Please change this code to register an SMS/Email service in IdentityConfig to send a message. 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 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Account/_ExternalLoginsListPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Authentication 2 | @model ExternalLoginListViewModel 3 | @inject IAuthenticationSchemeProvider SchemeProvider 4 |

Use another service to log in.

5 |
6 | @{ 7 | var schemes = await SchemeProvider.GetAllSchemesAsync(); 8 | var loginProviders = schemes.ToList(); 9 | if (!loginProviders.Any()) 10 | { 11 |
12 |

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

16 |
17 | } 18 | else 19 | { 20 |
21 |
22 |

23 | @foreach (var p in loginProviders) 24 | { 25 | 26 | } 27 |

28 |
29 |
30 | } 31 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Checkout/AddressAndPayment.cshtml: -------------------------------------------------------------------------------- 1 | @model Order 2 | 3 | @{ 4 | ViewBag.Title = "Address And Payment"; 5 | } 6 | 7 | @section Scripts { 8 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 9 | } 10 | 11 |
12 |

Address And Payment

13 |
14 |
15 |
16 | Shipping Information 17 | 18 | @Html.EditorForModel() 19 |
20 |
21 | Payment 22 |

We're running a promotion: all unicorns are free with the promo code: "FREE"

23 | 24 |
25 | 26 |
27 |
28 | @Html.TextBox("PromoCode") 29 |
30 |
31 | 32 | 33 |
-------------------------------------------------------------------------------- /UnicornStore/Views/Checkout/Complete.cshtml: -------------------------------------------------------------------------------- 1 | @model int 2 | 3 | @{ 4 | ViewBag.Title = "Checkout Complete"; 5 | } 6 | 7 |

Checkout Complete

8 | 9 |

Thanks for your order! Your order number is: @Model

10 | 11 |

12 | How about shopping for some more unicorns in our 13 | Store 14 |

-------------------------------------------------------------------------------- /UnicornStore/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Configuration 2 | @inject IConfiguration Configuration 3 | @{ 4 | ViewBag.Title = "Home Page"; 5 | } 6 | 7 |
8 |

@Configuration["AppSettings:SiteTitle"]

9 | 10 |
11 | 12 | 23 | -------------------------------------------------------------------------------- /UnicornStore/Views/Manage/AddPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model AddPhoneNumberViewModel 2 | @{ 3 | ViewBag.Title = "Add Phone Number"; 4 | } 5 | 6 |

@ViewBag.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 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @model ChangePasswordViewModel 2 | @{ 3 | ViewBag.Title = "Change Password"; 4 | } 5 | 6 |

@ViewBag.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 | -------------------------------------------------------------------------------- /UnicornStore/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 | @Model.Logins.Count [  Manage  ] 27 |
28 |
Phone Number:
29 |
30 |

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

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

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

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

@ViewBag.Title.

7 | 8 |

@ViewBag.StatusMessage

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

Registered Logins

12 | 13 | 14 | @foreach (var account in Model.CurrentLogins) 15 | { 16 | 17 | 18 | 34 | 35 | } 36 | 37 |
@account.LoginProvider 19 | @if (ViewBag.ShowRemoveButton) 20 | { 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | } 29 | else 30 | { 31 | @:   32 | } 33 |
38 | } 39 | @if (Model.OtherLogins.Any()) 40 | { 41 |

Add another service to log in.

42 |
43 |
44 |
45 |

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

51 |
52 |
53 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SetPasswordViewModel 2 | @{ 3 | ViewBag.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 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Manage/VerifyPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model VerifyPhoneNumberViewModel 2 | @{ 3 | ViewBag.Title = "Verify Phone Number"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 |
9 | 10 |

Enter verification code

11 |

12 | For DEMO only: You can type in this code in the below text box to proceed: @ViewBag.Code 13 |
14 | Please change this code to register an SMS service in IdentityConfig to send a text message. 15 |

16 |
17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | @section Scripts { 33 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } 34 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Access denied due to insufficient permissions"; 3 | } 4 | 5 |

Access denied due to insufficient permissions.

-------------------------------------------------------------------------------- /UnicornStore/Views/Shared/Components/Announcement/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model Blessing 2 | 3 | @if (Model != null) 4 | { 5 |
  • 6 |
    7 | 8 | @Model.Title 9 |
  • 10 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/Components/CartSummary/Default.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewBag.CartCount > 0) 2 | { 3 |
  • 4 | 5 | 6 | 7 | @ViewBag.CartCount 8 | 9 | 10 |
  • 11 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/Components/GenreMenu/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | 3 | 18 | -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/DemoLinkDisplay.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Demo link display page - Not for production use"; 3 | } 4 | 5 |
    6 |

    @ViewBag.Title.

    7 |
    8 |
    9 |

    10 | Demo link display page - Not for production use. 11 |

    12 | 13 | @if (ViewBag.Link != null) 14 | { 15 |

    16 | For DEMO only: You can click this link to confirm the email: [[link]] 17 |
    18 | Please change this code to register an email service in IdentityConfig to send an email. 19 |

    20 | } 21 |
    -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Error"; 3 | } 4 | 5 |

    Error.

    6 |

    An error occurred while processing your request.

    -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Locked Out"; 3 | } 4 | 5 |
    6 |

    Locked out.

    7 |

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

    8 |
    -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/StatusCodePage.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Item not found"; 3 | } 4 | 5 |

    Item not found.

    6 |

    Unable to find the item you are searching for. Please try again.

    -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Configuration 2 | @inject IConfiguration Configuration 3 | 4 | 5 | 6 | 7 | 8 | @ViewBag.Title - @Configuration["AppSettings:SiteTitle"] 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 49 |
    50 | @RenderBody() 51 |
    52 | 57 |
    58 | 59 | 60 | 61 | 62 | 63 | 64 | 67 | 70 | 71 | 72 | @RenderSection("scripts", required: false) 73 | 74 | 75 | -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using UnicornStore.Models 3 | 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | 7 | @if (SignInManager.IsSignedIn(User)) 8 | { 9 | 17 | } 18 | else if (User.Identity.IsAuthenticated) 19 | { 20 | //This code block necessary only for NTLM authentication 21 | 26 | } 27 | else 28 | { 29 | 33 | } -------------------------------------------------------------------------------- /UnicornStore/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 10 | 14 | -------------------------------------------------------------------------------- /UnicornStore/Views/ShoppingCart/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model UnicornStore.ViewModels.ShoppingCartViewModel 2 | @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf 3 | @{ 4 | ViewBag.Title = "Shopping Cart"; 5 | } 6 | 7 | @functions 8 | { 9 | public string GetAntiXsrfRequestToken() 10 | { 11 | return Xsrf.GetAndStoreTokens(Context).RequestToken; 12 | } 13 | } 14 | 15 | @section Scripts { 16 | 53 | } 54 | 55 |

    56 | Review your cart: 57 |

    58 |

    59 | Checkout >> 60 |

    61 |
    62 |
    63 | 64 | 65 | 68 | 71 | 74 | 75 | 76 | @foreach (var item in Model.CartItems) 77 | { 78 | 79 | 82 | 85 | 88 | 94 | 95 | } 96 | 97 | 100 | 101 | 102 | 105 | 106 |
    66 | Blessing Name 67 | 69 | Price (each) 70 | 72 | Quantity 73 |
    80 | @item.Blessing.Title 81 | 83 | @item.Blessing.Price 84 | 86 | @item.Count 87 | 89 | 91 | Remove from cart 92 | 93 |
    98 | Total 99 | 103 | @Model.CartTotal 104 |
    -------------------------------------------------------------------------------- /UnicornStore/Views/Store/Browse.cshtml: -------------------------------------------------------------------------------- 1 | @model Genre 2 | @{ 3 | ViewBag.Title = "Unicorns"; 4 | } 5 |
    6 |

    7 | @Model.Name Unicorns 8 |

    9 | 10 | 24 |
    -------------------------------------------------------------------------------- /UnicornStore/Views/Store/Details.cshtml: -------------------------------------------------------------------------------- 1 | @model Blessing 2 | 3 | @{ 4 | ViewBag.Title = "Blessing - " + Model.Title; 5 | } 6 | 7 |

    @Model.Title

    8 | 9 |

    10 | @Model.Title 11 |

    12 | 13 |
    14 |

    15 | Genre: 16 | @Model.Genre.Name 17 |

    18 |

    19 | Unicorn: 20 | @Model.Unicorn.Name 21 |

    22 |

    23 | Price: 24 | 25 |

    26 |

    27 | Add to cart 28 |

    29 |
    -------------------------------------------------------------------------------- /UnicornStore/Views/Store/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | @{ 3 | ViewBag.Title = "Store"; 4 | } 5 |

    Browse Unicorns

    6 | 7 |

    8 | Select from @Model.Count() Unicorn genres: 9 |

    10 |
      11 | @foreach (var genre in Model) 12 | { 13 |
    • @genre.Name
    • 14 | } 15 |
    -------------------------------------------------------------------------------- /UnicornStore/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using UnicornStore 2 | @using UnicornStore.Models 3 | @using Microsoft.Extensions.Options 4 | @using Microsoft.AspNetCore.Identity 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | -------------------------------------------------------------------------------- /UnicornStore/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } -------------------------------------------------------------------------------- /UnicornStore/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "SiteTitle": "Modernization Unicorn Store Dev", 4 | "CacheDbResults": false 5 | }, 6 | "DefaultAdminUsername": "", 7 | "DefaultAdminPassword": "", 8 | "ConnectionStrings": { 9 | "UnicornStore": "Database=UnicornStore;Trusted_Connection=False;MultipleActiveResultSets=true;Connect Timeout=30;" 10 | }, 11 | "Logging": { 12 | "LogLevel": { 13 | "Default": "Debug", 14 | "System": "Information", 15 | "Microsoft": "Information" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /UnicornStore/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "SiteTitle": "Modernization Unicorn Store Prod", 4 | "CacheDbResults": false 5 | }, 6 | "DefaultAdminUsername": "", 7 | "DefaultAdminPassword": "", 8 | "ConnectionStrings": { 9 | "UnicornStore": "Database=UnicornStoreProd;Trusted_Connection=False;MultipleActiveResultSets=true;Connect Timeout=30;" 10 | }, 11 | "Logging": { 12 | "LogLevel": { 13 | "Default": "Information", 14 | "System": "Information", 15 | "Microsoft": "Information" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /UnicornStore/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /UnicornStore/wwwroot/Content/Site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Set padding to keep content from hitting the edges */ 7 | .body-content { 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | /* Set width on the form input elements since they're 100% wide by default */ 13 | input, 14 | select, 15 | textarea { 16 | max-width: 280px; 17 | } 18 | 19 | /* styles for validation helpers */ 20 | .field-validation-error { 21 | color: #b94a48; 22 | } 23 | 24 | .field-validation-valid { 25 | display: none; 26 | } 27 | 28 | input.input-validation-error { 29 | border: 1px solid #b94a48; 30 | } 31 | 32 | input[type="checkbox"].input-validation-error { 33 | border: 0 none; 34 | } 35 | 36 | .validation-summary-errors { 37 | color: #b94a48; 38 | } 39 | 40 | .validation-summary-valid { 41 | display: none; 42 | } 43 | 44 | 45 | /* Unicorn Store additions */ 46 | 47 | ul#blessing-list li { 48 | height: 160px; 49 | } 50 | 51 | ul#blessing-list li img:hover { 52 | box-shadow: 1px 1px 7px #777; 53 | } 54 | 55 | ul#blessing-list li img { 56 | box-shadow: 1px 1px 5px #999; 57 | border: none; 58 | padding: 0; 59 | } 60 | 61 | ul#blessing-list li a, ul#blessing-details li a { 62 | text-decoration:none; 63 | } 64 | 65 | ul#blessing-list li a:hover { 66 | background: none; 67 | -webkit-text-shadow: 1px 1px 2px #bbb; 68 | text-shadow: 1px 1px 2px #bbb; 69 | color: #363430; 70 | } -------------------------------------------------------------------------------- /UnicornStore/wwwroot/Images/main-unicorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/Images/main-unicorn.png -------------------------------------------------------------------------------- /UnicornStore/wwwroot/Images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/Images/placeholder.png -------------------------------------------------------------------------------- /UnicornStore/wwwroot/Images/unicorn-two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/Images/unicorn-two.png -------------------------------------------------------------------------------- /UnicornStore/wwwroot/Scripts/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /* 16 | ** Unobtrusive validation support library for jQuery and jQuery Validate 17 | ** Copyright (C) Microsoft Corporation. All rights reserved. 18 | */ 19 | (function(a){var d=a.validator,b,e="unobtrusiveValidation";function c(a,b,c){a.rules[b]=c;if(a.message)a.messages[b]=a.message}function j(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function f(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function h(a){return a.substr(0,a.lastIndexOf(".")+1)}function g(a,b){if(a.indexOf("*.")===0)a=a.replace("*.",b);return a}function m(c,e){var b=a(this).find("[data-valmsg-for='"+f(e[0].name)+"']"),d=b.attr("data-valmsg-replace"),g=d?a.parseJSON(d)!==false:null;b.removeClass("field-validation-valid").addClass("field-validation-error");c.data("unobtrusiveContainer",b);if(g){b.empty();c.removeClass("input-validation-error").appendTo(b)}else c.hide()}function l(e,d){var c=a(this).find("[data-valmsg-summary=true]"),b=c.find("ul");if(b&&b.length&&d.errorList.length){b.empty();c.addClass("validation-summary-errors").removeClass("validation-summary-valid");a.each(d.errorList,function(){a("
  • ").html(this.message).appendTo(b)})}}function k(d){var b=d.data("unobtrusiveContainer"),c=b.attr("data-valmsg-replace"),e=c?a.parseJSON(c):null;if(b){b.addClass("field-validation-valid").removeClass("field-validation-error");d.removeData("unobtrusiveContainer");e&&b.empty()}}function n(){var b=a(this);b.data("validator").resetForm();b.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors");b.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}function i(c){var b=a(c),d=b.data(e),f=a.proxy(n,c);if(!d){d={options:{errorClass:"input-validation-error",errorElement:"span",errorPlacement:a.proxy(m,c),invalidHandler:a.proxy(l,c),messages:{},rules:{},success:a.proxy(k,c)},attachValidation:function(){b.unbind("reset."+e,f).bind("reset."+e,f).validate(this.options)},validate:function(){b.validate();return b.valid()}};b.data(e,d)}return d}d.unobtrusive={adapters:[],parseElement:function(b,h){var d=a(b),f=d.parents("form")[0],c,e,g;if(!f)return;c=i(f);c.options.rules[b.name]=e={};c.options.messages[b.name]=g={};a.each(this.adapters,function(){var c="data-val-"+this.name,i=d.attr(c),h={};if(i!==undefined){c+="-";a.each(this.params,function(){h[this]=d.attr(c+this)});this.adapt({element:b,form:f,message:i,params:h,rules:e,messages:g})}});a.extend(e,{__dummy__:true});!h&&c.attachValidation()},parse:function(b){var c=a(b).parents("form").andSelf().add(a(b).find("form")).filter("form");a(b).find(":input").filter("[data-val=true]").each(function(){d.unobtrusive.parseElement(this,true)});c.each(function(){var a=i(this);a&&a.attachValidation()})}};b=d.unobtrusive.adapters;b.add=function(c,a,b){if(!b){b=a;a=[]}this.push({name:c,params:a,adapt:b});return this};b.addBool=function(a,b){return this.add(a,function(d){c(d,b||a,true)})};b.addMinMax=function(e,g,f,a,d,b){return this.add(e,[d||"min",b||"max"],function(b){var e=b.params.min,d=b.params.max;if(e&&d)c(b,a,[e,d]);else if(e)c(b,g,e);else d&&c(b,f,d)})};b.addSingleVal=function(a,b,d){return this.add(a,[b||"val"],function(e){c(e,d||a,e.params[b])})};d.addMethod("__dummy__",function(){return true});d.addMethod("regex",function(b,c,d){var a;if(this.optional(c))return true;a=(new RegExp(d)).exec(b);return a&&a.index===0&&a[0].length===b.length});d.addMethod("nonalphamin",function(c,d,b){var a;if(b){a=c.match(/\W/g);a=a&&a.length>=b}return a});if(d.methods.extension){b.addSingleVal("accept","mimtype");b.addSingleVal("extension","extension")}else b.addSingleVal("extension","extension","accept");b.addSingleVal("regex","pattern");b.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");b.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range");b.add("equalto",["other"],function(b){var i=h(b.element.name),j=b.params.other,d=g(j,i),e=a(b.form).find(":input").filter("[name='"+f(d)+"']")[0];c(b,"equalTo",e)});b.add("required",function(a){(a.element.tagName.toUpperCase()!=="INPUT"||a.element.type.toUpperCase()!=="CHECKBOX")&&c(a,"required",true)});b.add("remote",["url","type","additionalfields"],function(b){var d={url:b.params.url,type:b.params.type||"GET",data:{}},e=h(b.element.name);a.each(j(b.params.additionalfields||b.element.name),function(i,h){var c=g(h,e);d.data[c]=function(){return a(b.form).find(":input").filter("[name='"+f(c)+"']").val()}});c(b,"remote",d)});b.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&c(a,"minlength",a.params.min);a.params.nonalphamin&&c(a,"nonalphamin",a.params.nonalphamin);a.params.regex&&c(a,"regex",a.params.regex)});a(function(){d.unobtrusive.parse(document)})})(jQuery); 20 | -------------------------------------------------------------------------------- /UnicornStore/wwwroot/Scripts/respond.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ 16 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */ 17 | window.matchMedia=window.matchMedia||(function(e,f){var c,a=e.documentElement,b=a.firstElementChild||a.firstChild,d=e.createElement("body"),g=e.createElement("div");g.id="mq-test-1";g.style.cssText="position:absolute;top:-100em";d.style.background="none";d.appendChild(g);return function(h){g.innerHTML='­';a.insertBefore(d,b);c=g.offsetWidth==42;a.removeChild(d);return{matches:c,media:h}}})(document); 18 | 19 | /*! Respond.js v1.2.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 20 | (function(e){e.respond={};respond.update=function(){};respond.mediaQueriesSupported=e.matchMedia&&e.matchMedia("only all").matches;if(respond.mediaQueriesSupported){return}var w=e.document,s=w.documentElement,i=[],k=[],q=[],o={},h=30,f=w.getElementsByTagName("head")[0]||s,g=w.getElementsByTagName("base")[0],b=f.getElementsByTagName("link"),d=[],a=function(){var D=b,y=D.length,B=0,A,z,C,x;for(;B-1,minw:F.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:F.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}}j()},l,r,v=function(){var z,A=w.createElement("div"),x=w.body,y=false;A.style.cssText="position:absolute;font-size:1em;width:1em";if(!x){x=y=w.createElement("body");x.style.background="none"}x.appendChild(A);s.insertBefore(x,s.firstChild);z=A.offsetWidth;if(y){s.removeChild(x)}else{x.removeChild(A)}z=p=parseFloat(z);return z},p,j=function(I){var x="clientWidth",B=s[x],H=w.compatMode==="CSS1Compat"&&B||w.body[x]||B,D={},G=b[b.length-1],z=(new Date()).getTime();if(I&&l&&z-l-1?(p||v()):1)}if(!!J){J=parseFloat(J)*(J.indexOf(y)>-1?(p||v()):1)}if(!K.hasquery||(!A||!L)&&(A||H>=C)&&(L||H<=J)){if(!D[K.media]){D[K.media]=[]}D[K.media].push(k[K.rules])}}for(var E in q){if(q[E]&&q[E].parentNode===f){f.removeChild(q[E])}}for(var E in D){var M=w.createElement("style"),F=D[E].join("\n");M.type="text/css";M.media=E;f.insertBefore(M,G.nextSibling);if(M.styleSheet){M.styleSheet.cssText=F}else{M.appendChild(w.createTextNode(F))}q.push(M)}},n=function(x,z){var y=c();if(!y){return}y.open("GET",x,true);y.onreadystatechange=function(){if(y.readyState!=4||y.status!=200&&y.status!=304){return}z(y.responseText)};if(y.readyState==4){return}y.send(null)},c=(function(){var x=false;try{x=new XMLHttpRequest()}catch(y){x=new ActiveXObject("Microsoft.XMLHTTP")}return function(){return x}})();a();respond.update=a;function t(){j(true)}if(e.addEventListener){e.addEventListener("resize",t,false)}else{if(e.attachEvent){e.attachEvent("onresize",t)}}})(this); -------------------------------------------------------------------------------- /UnicornStore/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/favicon.ico -------------------------------------------------------------------------------- /UnicornStore/wwwroot/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /UnicornStore/wwwroot/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /UnicornStore/wwwroot/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/UnicornStore/wwwroot/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /content/secrets/_index.md: -------------------------------------------------------------------------------- 1 | # Securing Your .NET Container Secrets 2 | 3 | As customers move .NET workloads to the cloud, many start to consider containerizing their applications because of the agility and cost savings that containers provide. Combine those compelling drivers with the multi-OS capabilities that come with .NET Core, and customers have an exciting reason to migrate their applications. A primary question is how they can safely store secrets and sensitive configuration values in containerized workloads. In this workshop, learn how to safely containerize the Unicorn Store. 4 | 5 | You will learn how to run the Unicorn Store which is an ASP.NET Core application in a Docker container while connecting to a SQL backend (Database=UnicornStore) in [Amazon RDS](https://aws.amazon.com/rds/). The RDS credentials to the database in are stored in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) along with other sensitive information needed for the application to run. This allows the Unicorn Store application to safely connect to the database from the container without storing the secrets in a file on the container or in source control. 6 | 7 | Take the time to read [mutiple environments in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-3.0) and [safe storage of app secrets in development in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-3.0&tabs=windows) before starting so you understand the various different configuration options. 8 | 9 | ## Architecture Overview 10 | 11 | ![Unicorn Store Architecture with AWS Secrets Manager and Amazon RDS](/static/images/secrets/secrets-manager-architecture.png) 12 | 13 | ## Getting Started 14 | 15 | Click [here](/content/secrets/prerequisites/_index.md) to start the workshop. -------------------------------------------------------------------------------- /content/secrets/accessing-secrets.md: -------------------------------------------------------------------------------- 1 | # Accessing Secrets 2 | 3 | Now that you've set up the secret for local development, you may be wondering how can you access a secret in your .NET Core code. The [ASP.NET Core Configuration API](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/index?view=aspnetcore-3.0) provides access to Secret Manager secrets. 4 | 5 | In ASP.NET 2.0 or later, the user secrets configuration source is automatically added in development mode when the project calls [CreateDefaultBuilder](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) to initialize a new instance of the host with preconfigured defaults. Below is a snippet from our [Program.cs](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/Program.cs) file. 6 | 7 | ![program-createdefaultbuilder](/static/images/secrets/program-createdefaultbuilder.png) 8 | 9 | Anytime we want to retrieve a user secret during local development, we can do so via the Configuration API. Below is a snippet from our [Startup.cs](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/Startup.cs) file. You'll notice that IConfiguration is injected into the Startup constructor to access configuration values. Once that's done, accessing a key/value for something like the password value is as simple as calling ***Configuration["UNICORNSTORE_DBSECRET:password"]***. 10 | 11 | ![startup-configuration](/static/images/secrets/startup-configuration.png) 12 | 13 | You may have noticed in the above snippet of code that we are constructing our database connection string using [SqlConnectionStringBuilder](https://docs.microsoft.com/dotnet/api/system.data.sqlclient.sqlconnectionstringbuilder). This is best practice because now we aren't storing sensitive information like a password in plain text which is insecure. Look at the [appsettings.Development.json](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/appsettings.Development.json) file in our project. You'll notice that none of the sensitive information is in the connection string. Only a portion of what's needed is set there and the rest of the connection string can be driven by configuration based on environment. 14 | 15 | ![connectionstring](/static/images/secrets/connectionstring.png) 16 | 17 | The Configuration API is a very powerful feature of .NET Core and can handle multiple configuration sources. When an ASP.NET Core application starts, it loads your configuration providers in the order they are configured. If a configuration source is loaded and the key already exists, it overwrites the previous value meaning the last key loaded wins. 18 | 19 | In ***Program.cs*** there is a method called ***CreateDefaultBuilder*** which is behind the configuration provider setup. Looking at the [CreateDefaultBuilder](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder?view=dotnet-plat-ext-3.0) method, we can see that the order of providers are configured as follows: 20 | 21 | 1. Files (appsettings.json, appsettings.{Environment}.json, where {Environment} is the app's current hosting environment) 22 | 2. User secrets (Secret Manager) (in the Development environment only) 23 | 3. Environment variables 24 | 4. Command-line arguments 25 | 26 | It's a common practice to position the Command-line Configuration Provider last in a series of providers to allow command-line arguments to override configuration set by the other providers. 27 | 28 | This sequence of providers is put into place when you initialize a new WebHostBuilder with CreateDefaultBuilder. 29 | 30 | Click [**here**](/content/secrets/create-secrets.md) to move to the next section where we will create some more local secrets for the Unicorn Store and run the application in your development environment. -------------------------------------------------------------------------------- /content/secrets/cfn/player-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: This template will launch the Player Account Environment for .Net Modernization Workshop. 4 | Parameters: 5 | BucketName: 6 | Type: String 7 | Description: "S3 Bucket Name where nested templates are Stored in" 8 | BucketPrefix: 9 | Type: String 10 | Description: S3 Prefix for nested template artifacts 11 | UnicornStoreDBUsername: 12 | Type: String 13 | Description: UnicornStore Database Username for RDS 14 | Default: "awssa" 15 | UnicornStoreDBPassword: 16 | AllowedPattern: "^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)" 17 | ConstraintDescription: Must contain only alphanumeric characters with at least one capital letter and one number 18 | Description: UnicornStore Database Username for RDS 19 | MaxLength: '41' 20 | MinLength: '8' 21 | Type: String 22 | Default: BBTh123ca 23 | Resources: 24 | BasicVPC: 25 | Type: "AWS::CloudFormation::Stack" 26 | Properties: 27 | TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/${BucketPrefix}/templates/player-vpc-template.yaml" 28 | TimeoutInMinutes: 10 29 | WorkshopIDE: 30 | Type: "AWS::Cloud9::EnvironmentEC2" 31 | Properties: 32 | Description: "Cloud9 Browser Based IDE for executing the modernization AWS Workshop" 33 | AutomaticStopTimeMinutes: 60 34 | InstanceType: t3.small 35 | SubnetId: !GetAtt BasicVPC.Outputs.PublicSubnet1 36 | UnicornStoreRDSSecurityGroup: 37 | Type: "AWS::EC2::SecurityGroup" 38 | Properties: 39 | GroupDescription: UnicornStoreRDSSecurityGroup 40 | SecurityGroupIngress: 41 | - IpProtocol: tcp 42 | FromPort: 1433 43 | ToPort: 1433 44 | CidrIp: !GetAtt BasicVPC.Outputs.VPCCIDR 45 | VpcId: !GetAtt BasicVPC.Outputs.VPCId 46 | UnicornStoreRDS: 47 | Type: "AWS::RDS::DBInstance" 48 | Properties: 49 | AllocatedStorage: 20 50 | DBInstanceClass: db.t2.medium 51 | Port: 1433 52 | PubliclyAccessible: 'true' 53 | StorageType: gp2 54 | MasterUsername: !Ref UnicornStoreDBUsername 55 | MasterUserPassword: !Ref UnicornStoreDBPassword 56 | Engine: sqlserver-web 57 | EngineVersion: 14.00.3223.3.v1 58 | LicenseModel: license-included 59 | MultiAZ: false 60 | DBSubnetGroupName: !Ref UnicornStoreSubnetGroup 61 | VPCSecurityGroups: 62 | - Fn::GetAtt: 63 | - UnicornStoreRDSSecurityGroup 64 | - GroupId 65 | Tags: 66 | - 67 | Key: "Name" 68 | Value: "UnicornStoreDB" 69 | UnicornStoreSubnetGroup: 70 | Type: "AWS::RDS::DBSubnetGroup" 71 | Properties: 72 | DBSubnetGroupDescription: UnicornStore-SubnetGroup 73 | SubnetIds: 74 | - !GetAtt BasicVPC.Outputs.PublicSubnet1 75 | - !GetAtt BasicVPC.Outputs.PublicSubnet2 76 | UnicornStoreECR: 77 | Type: "AWS::ECR::Repository" 78 | Properties: 79 | RepositoryName : modernization-unicorn-store 80 | UnicornStoreCloudwatchLogGroup: 81 | Type: "AWS::Logs::LogGroup" 82 | Properties: 83 | LogGroupName: UnicornStore 84 | RetentionInDays: 30 85 | ECSExecutionRole: 86 | Type: AWS::IAM::Role 87 | Properties: 88 | AssumeRolePolicyDocument: 89 | Version: "2012-10-17" 90 | Statement: 91 | - 92 | Effect: "Allow" 93 | Principal: 94 | Service: 95 | - "ecs-tasks.amazonaws.com" 96 | Action: 97 | - "sts:AssumeRole" 98 | ManagedPolicyArns: 99 | - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" 100 | - "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" 101 | Policies: 102 | - 103 | PolicyName: RetrieveUnicornSecret 104 | PolicyDocument: 105 | Version: "2012-10-17" 106 | Statement: 107 | - Effect: Allow 108 | Action: 109 | - secretsmanager:GetSecretValue 110 | Resource: 111 | - !Ref UnicornStoreDBSecret 112 | - !Ref DefaultAdminPasswordSecret 113 | - !Ref DefaultAdminUsernameSecret 114 | RoleName: "UnicornStoreExecutionRole" 115 | UnicornStoreLbSecurityGroup: 116 | Type: "AWS::EC2::SecurityGroup" 117 | Properties: 118 | GroupName: UnicornStoreLbSecurityGroup 119 | GroupDescription: Security group the the Unicornstore Application Load Balancer 120 | SecurityGroupIngress: 121 | - IpProtocol: tcp 122 | FromPort: 80 123 | ToPort: 80 124 | CidrIp: 0.0.0.0/0 125 | VpcId: !GetAtt BasicVPC.Outputs.VPCId 126 | UnicornStoreTaskSecurityGroup: 127 | Type: "AWS::EC2::SecurityGroup" 128 | Properties: 129 | GroupName: UnicornStoreTaskSecurityGroup 130 | GroupDescription: Security group the the Unicornstore Fargate Task 131 | VpcId: !GetAtt BasicVPC.Outputs.VPCId 132 | UnicornStoreTaskSecurityGroupIngress: 133 | Type: "AWS::EC2::SecurityGroupIngress" 134 | Properties: 135 | GroupId: !Ref UnicornStoreTaskSecurityGroup 136 | IpProtocol: tcp 137 | FromPort: 80 138 | ToPort: 80 139 | SourceSecurityGroupId: !Ref UnicornStoreLbSecurityGroup 140 | UnicornStoreLoadBalancer: 141 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 142 | Properties: 143 | Name: UnicornStore-LB 144 | Scheme: internet-facing 145 | SecurityGroups: 146 | - !Ref UnicornStoreLbSecurityGroup 147 | Subnets: 148 | - !GetAtt BasicVPC.Outputs.PublicSubnet1 149 | - !GetAtt BasicVPC.Outputs.PublicSubnet2 150 | Tags: 151 | - Key: Name 152 | Value: UnicornStore-LB 153 | Type: application 154 | IpAddressType: ipv4 155 | DependsOn: UnicornStoreLbSecurityGroup 156 | ECSCluster: 157 | Type: "AWS::ECS::Cluster" 158 | Properties: 159 | ClusterName: UnicornStoreCluster 160 | UnicornStoreDBSecret: 161 | Type: "AWS::SecretsManager::Secret" 162 | Properties: 163 | Name: UNICORNSTORE_DBSECRET 164 | Description: UnicornStoreDB RDS Secret 165 | SecretString: 166 | !Join 167 | - '' 168 | - - '{"username":' 169 | - !Sub '"${UnicornStoreDBUsername}",' 170 | - '"password":' 171 | - !Sub '"${UnicornStoreDBPassword}",' 172 | - '"engine":' 173 | - '"sqlserver",' 174 | - '"host":' 175 | - !Sub '"${UnicornStoreRDS.Endpoint.Address}",' 176 | - '"port":' 177 | - !Sub "${UnicornStoreRDS.Endpoint.Port}," 178 | - '"dbInstanceIdentifier":' 179 | - !Sub '"${UnicornStoreRDS}"' 180 | - '}' 181 | DefaultAdminPasswordSecret: 182 | Type: "AWS::SecretsManager::Secret" 183 | Properties: 184 | Name: DefaultAdminPassword 185 | Description: UnicornStore DefaultAdminPassword 186 | SecretString: Secret1* 187 | DefaultAdminUsernameSecret: 188 | Type: "AWS::SecretsManager::Secret" 189 | Properties: 190 | Name: DefaultAdminUsername 191 | Description: UnicornStore DefaultAdminUsername 192 | SecretString: Administrator@test.com 193 | Outputs: 194 | Cloud9IDE: 195 | Description: "The IDE Login URL" 196 | Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/cloud9/ide/${WorkshopIDE}" 197 | 198 | -------------------------------------------------------------------------------- /content/secrets/cfn/player-vpc-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: 'This template will launch the Player Account VPC Environment for .Net Modernization Workshop. 4 | You will be billed for the AWS resources used if you create a stack from this template.' 5 | Mappings: 6 | SubnetConfig: 7 | VPC: 8 | CIDR: 10.0.0.0/16 9 | Public: 10 | CIDR: 10.0.0.0/24 11 | Public2: 12 | CIDR: 10.0.1.0/24 13 | Resources: 14 | VPC: 15 | Type: AWS::EC2::VPC 16 | Properties: 17 | EnableDnsSupport: 'true' 18 | EnableDnsHostnames: 'true' 19 | CidrBlock: 20 | Fn::FindInMap: 21 | - SubnetConfig 22 | - VPC 23 | - CIDR 24 | Tags: 25 | - Key: Name 26 | Value: "Demo VPC" 27 | - Key: Application 28 | Value: 29 | Ref: AWS::StackName 30 | - Key: Network 31 | Value: Public 32 | PublicSubnet1: 33 | Type: AWS::EC2::Subnet 34 | Properties: 35 | VpcId: 36 | Ref: VPC 37 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 38 | CidrBlock: 39 | Fn::FindInMap: 40 | - SubnetConfig 41 | - Public 42 | - CIDR 43 | Tags: 44 | - Key: Application 45 | Value: 46 | Ref: AWS::StackName 47 | - Key: Network 48 | Value: Public 49 | PublicSubnet2: 50 | Type: AWS::EC2::Subnet 51 | Properties: 52 | VpcId: 53 | Ref: VPC 54 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 55 | CidrBlock: 56 | Fn::FindInMap: 57 | - SubnetConfig 58 | - Public2 59 | - CIDR 60 | Tags: 61 | - Key: Application 62 | Value: 63 | Ref: AWS::StackName 64 | - Key: Network 65 | Value: Public 66 | InternetGateway: 67 | Type: AWS::EC2::InternetGateway 68 | Properties: 69 | Tags: 70 | - Key: Application 71 | Value: 72 | Ref: AWS::StackName 73 | - Key: Network 74 | Value: Public 75 | GatewayToInternet: 76 | Type: AWS::EC2::VPCGatewayAttachment 77 | Properties: 78 | VpcId: 79 | Ref: VPC 80 | InternetGatewayId: 81 | Ref: InternetGateway 82 | PublicRouteTable: 83 | Type: AWS::EC2::RouteTable 84 | Properties: 85 | VpcId: 86 | Ref: VPC 87 | Tags: 88 | - Key: Application 89 | Value: 90 | Ref: AWS::StackName 91 | - Key: Network 92 | Value: Public 93 | PublicRoute: 94 | Type: AWS::EC2::Route 95 | DependsOn: GatewayToInternet 96 | Properties: 97 | RouteTableId: 98 | Ref: PublicRouteTable 99 | DestinationCidrBlock: 0.0.0.0/0 100 | GatewayId: 101 | Ref: InternetGateway 102 | PublicSubnet1RouteTableAssociation: 103 | Type: AWS::EC2::SubnetRouteTableAssociation 104 | Properties: 105 | SubnetId: 106 | Ref: PublicSubnet1 107 | RouteTableId: 108 | Ref: PublicRouteTable 109 | PublicSubnet2RouteTableAssociation: 110 | Type: AWS::EC2::SubnetRouteTableAssociation 111 | Properties: 112 | SubnetId: 113 | Ref: PublicSubnet2 114 | RouteTableId: 115 | Ref: PublicRouteTable 116 | PublicNetworkAcl: 117 | Type: AWS::EC2::NetworkAcl 118 | Properties: 119 | VpcId: 120 | Ref: VPC 121 | Tags: 122 | - Key: Application 123 | Value: 124 | Ref: AWS::StackName 125 | - Key: Network 126 | Value: Public 127 | InboundPublicNetworkAclEntry: 128 | Type: AWS::EC2::NetworkAclEntry 129 | Properties: 130 | NetworkAclId: 131 | Ref: PublicNetworkAcl 132 | RuleNumber: '100' 133 | Protocol: -1 134 | RuleAction: allow 135 | Egress: 'false' 136 | CidrBlock: 0.0.0.0/0 137 | OutboundPublicNetworkAclEntry: 138 | Type: AWS::EC2::NetworkAclEntry 139 | Properties: 140 | NetworkAclId: 141 | Ref: PublicNetworkAcl 142 | RuleNumber: '100' 143 | Protocol: -1 144 | RuleAction: allow 145 | Egress: 'true' 146 | CidrBlock: 0.0.0.0/0 147 | PublicSubnet1NetworkAclAssociation: 148 | Type: AWS::EC2::SubnetNetworkAclAssociation 149 | Properties: 150 | SubnetId: 151 | Ref: PublicSubnet1 152 | NetworkAclId: 153 | Ref: PublicNetworkAcl 154 | PublicSubnet2NetworkAclAssociation: 155 | Type: AWS::EC2::SubnetNetworkAclAssociation 156 | Properties: 157 | SubnetId: 158 | Ref: PublicSubnet2 159 | NetworkAclId: 160 | Ref: PublicNetworkAcl 161 | Outputs: 162 | VPCId: 163 | Description: VPCId of the newly created VPC 164 | Value: 165 | Ref: VPC 166 | PublicSubnet1: 167 | Description: SubnetId of the public subnet 1 168 | Value: 169 | Ref: PublicSubnet1 170 | PublicSubnet2: 171 | Description: SubnetId of the public subnet 2 172 | Value: 173 | Ref: PublicSubnet2 174 | VPCCIDR: 175 | Description: VPC CIDR CidrBlock 176 | Value: 177 | Fn::FindInMap: 178 | - SubnetConfig 179 | - VPC 180 | - CIDR -------------------------------------------------------------------------------- /content/secrets/container-registry.md: -------------------------------------------------------------------------------- 1 | # Container Registry 2 | 3 | Now that we've successfully containerized the Unicorn Store application, it's time to push the image to [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) before we can deploy our application to an orchestrator like AWS Fargate. 4 | 5 | Amazon Elastic Container Registry (ECR) is a fully-managed Docker container registry that makes it easy for developers to store, manage, and deploy Docker container images. Amazon Elastic Container Registry integrates with Amazon ECS and the Docker CLI, allowing you to simplify your development and production workflows. You can easily push your container images to Amazon ECR using the Docker CLI from your development machine, and Amazon ECS can pull them directly for production deployments. 6 | 7 | To get started, log into your Amazon ECR registry using the helper provided by the AWS CLI: 8 | 9 | ``` 10 | eval $(aws ecr get-login --no-include-email) 11 | ``` 12 | 13 | Use the AWS CLI to get information about the Amazon ECR repository for the Unicorn Store that was created for you ahead of time: 14 | 15 | ``` 16 | aws ecr describe-repositories --repository-name modernization-unicorn-store 17 | ``` 18 | 19 | ![unicornstore-ecr](/static/images/secrets/unicornstore-ecr.png) 20 | 21 | Verify that the modernization-unicorn-store_unicornstore:latest image exists by running the following command: 22 | 23 | ``` 24 | docker images 25 | ``` 26 | 27 | ![unicornstore-docker-image](/static/images/secrets/unicornstore-docker-images.png) 28 | 29 | 30 | The next step is to tag your image so you can push the image to the repository by running the following command: 31 | 32 | ``` 33 | docker tag modernization-unicorn-store_unicornstore:latest $(aws ecr describe-repositories --repository-name modernization-unicorn-store --query=repositories[0].repositoryUri --output=text):latest 34 | ``` 35 | 36 | Run the following command to push your image to the repository: 37 | 38 | ``` 39 | docker push $(aws ecr describe-repositories --repository-name modernization-unicorn-store --query=repositories[0].repositoryUri --output=text):latest 40 | ``` 41 | 42 | With the Unicorn Store image now pushed to Amazon ECR, we are ready to deploy it to AWS Fargate. 43 | 44 | Click [**here**](/content/secrets/taskdefinitions.md) to move to the next section where we will create the Task Definition for the Unicorn Store to run in AWS Fargate. 45 | -------------------------------------------------------------------------------- /content/secrets/containerize-unicornstore.md: -------------------------------------------------------------------------------- 1 | # Containerize the Unicorn Store 2 | 3 | Now that we've successfully launched the Unicorn Store in our local development environment, it's time to containerize the application to get it ready to deploy to AWS Fargate. We will be using AWS Secrets Manager to inject the secrets into the container when it is initially started. The secrets will be injected as environment variables containing the sensitive data to present to the container. 4 | 5 | If you remember from earlier in the lab, environment variables can be accessed via the the Configuration Provider in .NET Core. This means we can containerize the application without any sensitive information stored inside the container. When the container is launched, we can inject the senstive information in the format that the application is expecting. 6 | 7 | To build our container, we will use [Docker Compose](https://docs.docker.com/compose/) which is a tool for defining and running multi-container Docker applications. In the root of the modernization-unicorn-store repository you will see a file called [docker-compose.yml](https://github.com/aws-samples/modernization-unicorn-store/blob/master/docker-compose.yml). Feel free to review the contents. You will notice it uses [Dockerfile](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/Dockerfile) in the UnicornStore project to build the application into a container. Issue the following command to build the container image: 8 | 9 | ``` 10 | cd ~/environment/modernization-unicorn-store/ 11 | 12 | docker-compose build 13 | ``` 14 | 15 | Once the image has been successfully built you should be able to see the modernization-unicorn-store_unicornstore:latest image by issuing the following command: 16 | 17 | ``` 18 | docker images 19 | ``` 20 | 21 | ![unicornstore-docker-image](/static/images/secrets/unicornstore-docker-images.png) 22 | 23 | At this point, you can launch the container locally with docker-compose. You'll notice that in the root of the modernization-unicorn-store repository there are two files named [docker-compose.development.yml](https://github.com/aws-samples/modernization-unicorn-store/blob/master/docker-compose.development.yml) and [docker-compose.production.yml](https://github.com/aws-samples/modernization-unicorn-store/blob/master/docker-compose.production.yml). A common use case for multiple compose files is changing a development Compose app for a production-like environment. Keep in mind, you should ***NEVER*** store sensitive information in plain text in those files so they aren't accidentally committed to a source code repository. To showcase how multiple Compose files can be used together try uncommenting the lines in ***docker-compose.development.yml*** so they look like the below example and save the file. 24 | 25 | You will need to replace the db specific information in the UNICORNSTORE_DBSECRET key with your specific information that you noted down. If you forgot to write them down, simply issue the following command: 26 | 27 | ``` 28 | dotnet user-secrets list -p ~/environment/modernization-unicorn-store/UnicornStore/ 29 | 30 | ``` 31 | 32 | To edit the docker-compose-development.yml file with your values simply double click it on the left navigation tree in Cloud9 which opens up a text editor. Make sure you replace the following values in the UNICORNSTORE_DBSECRET json string in the docker-compose-development.yml. 33 | 34 | * username 35 | * password 36 | * host 37 | * dbInstanceIdentifier 38 | 39 | The file should look like the below once edited and saved with your values. 40 | 41 | ![docker-compose-development](/static/images/secrets/docker-compose-development.png) 42 | 43 | Now run the following command: 44 | 45 | ``` 46 | docker-compose -f docker-compose.yml -f docker-compose.development.yml up 47 | ``` 48 | 49 | You should be able to navigate to the Unicorn Store by clicking the ***Preview, Preview Running Application*** button on the menu bar in the AWS Cloud9 IDE. However, now the application is running in a local container. The necessary configuration has now been inserted as environment variables when the container was started just like when we go to launch it in Fargate. 50 | 51 | Go ahead and stop the running .NET application by issuing a ***Ctrl+C*** in the Cloud9 Terminal. 52 | 53 | Click [**here**](/content/secrets/container-registry.md) to move to the next section where we will push the Unicorn Store Docker image to Amazon Elastic Container Registry. -------------------------------------------------------------------------------- /content/secrets/create-secrets.md: -------------------------------------------------------------------------------- 1 | # Create Secrets 2 | 3 | Now that you understand the basics of the .NET Core Secrets Manager tool, let's create two more secrets that are needed for the Unicorn Store. If you look at the [appsettings.Development.json](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/appsettings.Development.json) or [appsettings.Production.json](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/appsettings.Production.json) files, you'll notice two keys with the name ***DefaultAdminUsername*** and ***DefaultAdminPassword*** but neither have values. While you could set the values here directly, it would be insecure because then the files would be checked into source control with sensitive information inside. 4 | 5 | ![secret-appsettings](/static/images/secrets/secret-appsettings.png) 6 | 7 | When the lab was provisioned, the CloudFormation template created and stored both of those names in AWS Secrets Manager with a secret-id of ***DefaultAdminUsername*** and ***DefaultAdminUsername*** with a pre-set SecretString for each. We will use the values for SecretString locally in our testing via the .NET Core Secrets Manager tool. We will use AWS Secrets Manager later on in this lab when we deploy the Unicorn Store to AWS Fargate. 8 | 9 | Let's set up the two secrets locally by issuing the following commands: 10 | 11 | .NET Core Secrets Manager 12 | 13 | ``` 14 | cd ~/environment/modernization-unicorn-store/UnicornStore 15 | 16 | AdminUser=$(aws secretsmanager get-secret-value --secret-id DefaultAdminUsername | jq -r '.SecretString') 17 | 18 | dotnet user-secrets set "DefaultAdminUsername" $AdminUser 19 | 20 | AdminPass=$(aws secretsmanager get-secret-value --secret-id DefaultAdminPassword | jq -r '.SecretString') 21 | 22 | dotnet user-secrets set "DefaultAdminPassword" $AdminPass 23 | ``` 24 | 25 | Now that we have all of our secrets set up for local testing, let's look at the local secrets to ensure everything is ready. Your secrets should be the same as below but have your unique secret values: 26 | 27 | ``` 28 | dotnet user-secrets list 29 | ``` 30 | 31 | ![secret-final](/static/images/secrets/secret-final.png) 32 | 33 | Take note of all of the values by taking a screenshot or writing them down. You will need them later in this lab. 34 | 35 | To test out the Unicorn Store in our local development environment using .NET Core Secrets Manager you can simply run the below command or run the command in an IDE like Visual Studio: 36 | 37 | ``` 38 | dotnet run 39 | ``` 40 | 41 | After a couple of seconds, you will see a message similar to the below. 42 | 43 | 44 | However, in order to preview the application in Cloud9, click the ***Preview, Preview Running Application*** button on the menu bar in the AWS Cloud9 IDE. 45 | 46 | ![cloud9-dotnet](/static/images/secrets/cloud9-dotnet.png) 47 | 48 | This opens an application preview tab within the environment, and then displays the application's output on the tab. Click the ***Pop Out Into New Window*** button on the preview tab so the page loads into a larger window. 49 | 50 | ![unicornstore](/static/images/unicornstore.png) 51 | 52 | Try clicking the admin link at the bottom of the page and logging in with the secret values you set for ***DefaultAdminUsername*** and ***DefaultAdminPassword***. 53 | 54 | We've now successfully configured our development environment to authenticate to our Unicorn Store RDS instance. You may be wondering who created the database and how the application connected to it at this point. In the UnicornStore project, there is a class named [DBInitializer.cs](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/Data/DbInitializer.cs) in the Data folder that gets called via [Program.cs](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/Program.cs) when the application starts. If the database doesn't exist, the application creates it and populates it with sample data. The application connects to the database with the credentials you specified in .NET Secrets Manager because the application is running with an environment variable of ***"ASPNETCORE_ENVIRONMENT": "Development"*** which is set when you ran the application. 55 | 56 | Go ahead and stop the running .NET application by issuing a ***Ctrl+C*** in the Cloud9 Terminal. 57 | 58 | Click [**here**](/content/secrets/containerize-unicornstore.md) to move to the next section where we will containerize the Unicorn Store and run it in Cloud9 using environment variables from Docker Compose. -------------------------------------------------------------------------------- /content/secrets/fargate.md: -------------------------------------------------------------------------------- 1 | # Fargate 2 | 3 | Now that we've successfully registered our Task Definition, it's time to deploy the Unicorn Store to [AWS Fargate](https://aws.amazon.com/fargate/). 4 | 5 | To start, run the below commands to set some variables in order to successfully complete this chapter and run subsequent commands in the AWS CLI: 6 | 7 | ``` 8 | UnicornVPCID=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values="Demo VPC" --query="Vpcs[0].VpcId" --output=text) 9 | 10 | TaskSecurityGroup=$(aws ec2 describe-security-groups --filters Name=vpc-id,Values=$UnicornVPCID Name=group-name,Values=UnicornStoreTaskSecurityGroup --query "SecurityGroups[0].GroupId" --output=text) 11 | 12 | UnicornSubnet1=$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$UnicornVPCID --query "Subnets[0].SubnetId" --output=text) 13 | 14 | UnicornSubnet2=$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$UnicornVPCID --query "Subnets[1].SubnetId" --output=text) 15 | 16 | ``` 17 | 18 | Now let's create a Target Group for the Application Load Balancer. When the lab was provisioned, the CloudFormation template created an Application Load Balancer with a name of ***UnicornStore-LB*** in your account. Notice the value of ***--target-type ip*** in the below command. This is because we are using the Fargate launch type and the task will use the awsvpc network mode. This is mandatory because tasks that use awsvpc network mode are associated with an elastic network interface, not an Amazon EC2 instance. 19 | 20 | ``` 21 | aws elbv2 create-target-group --name ecs-Unicor-UnicornStore-Service --protocol HTTP --port 80 --vpc-id $UnicornVPCID --health-check-path "/health" --target-type ip 22 | ``` 23 | 24 | Let's create a listener for the Application Load Balancer. A listener is a process that checks for connection requests, using the protocol and port that you configure. The rules that you define for a listener determine how the load balancer routes requests to the targets in one or more target groups. 25 | 26 | ``` 27 | LBARN=$(aws elbv2 describe-load-balancers --names="UnicornStore-LB" --query="LoadBalancers[0].LoadBalancerArn" --output=text) 28 | 29 | TGARN=$(aws elbv2 describe-target-groups --names ecs-Unicor-UnicornStore-Service --query="TargetGroups[0].TargetGroupArn" --output=text) 30 | 31 | aws elbv2 create-listener --load-balancer-arn $LBARN --protocol HTTP --port 80 --default-actions Type=forward,TargetGroupArn=$TGARN 32 | 33 | ``` 34 | 35 | Now that the load balancer is ready, let's create the ECS Fargate Service for the Unicorn Store. 36 | 37 | ``` 38 | aws ecs create-service --cluster UnicornStoreCluster --service-name UnicornStore-Service --task-definition modernization-unicorn --desired-count 1 --launch-type "FARGATE" --network-configuration "awsvpcConfiguration={subnets=[$UnicornSubnet1, $UnicornSubnet2],securityGroups=[$TaskSecurityGroup],assignPublicIp=ENABLED}" --load-balancers targetGroupArn=$TGARN,containerName=modernization-unicorn-store_unicornstore,containerPort=80 39 | 40 | ``` 41 | 42 | Run the below commands to get the URL for the Unicorn Store behind the ALB and paste it into your browser. It may take up to a minute or so for the initial registration and the URL to the Unicorn Store to be healthy and ready to serve traffic: 43 | 44 | ``` 45 | LBDNS=$(printf "http://%s\n" $(aws elbv2 describe-load-balancers --names="UnicornStore-LB" --query="LoadBalancers[0].DNSName" --output=text)) 46 | 47 | until [[ `aws elbv2 describe-target-health --target-group-arn $TGARN --query "TargetHealthDescriptions[0].[TargetHealth]" --output text` == "healthy" ]]; do echo "The Unicorn Store is NOT registered with the Target Group at `date`"; sleep 10; done && echo "The Unicorn Store is ready at `date` - Please proceed to $LBDNS" 48 | ``` 49 | 50 | ***CONGRATULATIONS!!!*** You've now successfully deployed the ASP.NET Core Unicorn Store to AWS Fargate with the Production configuration coming from AWS Secrets Manager! 51 | 52 | ![unicornstore-prod](/static/images/secrets/unicornstore-prod.png) 53 | 54 | 55 | -------------------------------------------------------------------------------- /content/secrets/introduction.md: -------------------------------------------------------------------------------- 1 | # Secrets Introduction 2 | 3 | When designing an application, one of the primary considerations is how sensitive data will be stored. A very common use-case is ensuring that something like the password and information needed to connect to your database aren’t written to a file or checked into a source control repository. This information should be considered sensitive and should only be accessed by the users and applications in a least privileged model. 4 | 5 | In this section, we will understand how to how to leverage [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) which helps you protect secrets that are needed for your applications and makes it easy to manage and things like database credentials to name a few. 6 | 7 | We will also cover the concept of using a .NET Core tool called [Secret Manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-3.0&tabs=windows) which allows you to store sensitive data during the development of your application. AWS Secrets Manager and .NET Core Secret Manager should never be confused but we will show you how to use both when designing your .NET Core application. By leveraging the .NET Core Secret Manager tool, a developer can easily create key/value objects in a JSON file on their local machine outside of the actual project to ensure the actual secrets aren’t checked into a source control repository. 8 | 9 | Click [**here**](/content/secrets/secrets.md) to move to the next section where we will explain the basics of the ASP.NET Core Secret Manager tool and create the UNICORNSTORE_DBSECRET. 10 | -------------------------------------------------------------------------------- /content/secrets/prerequisites/_index.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | To start the workshop, follow one of the below links depending on whether you are... 4 | 5 | * ...[running the workshop on your own (in your own account)](self_paced/_index.md) 6 | * ...[attending an AWS hosted event (using AWS provided hashes)](aws_event/_index.md) 7 | 8 | Once you have completed with either setup, continue with [**Create a Workspace**](getting-started.md) -------------------------------------------------------------------------------- /content/secrets/prerequisites/aws_event/_index.md: -------------------------------------------------------------------------------- 1 | # Running the workshop at an AWS Event 2 | 3 | Only complete this section if you are at an AWS hosted event (such as re:Invent, re:Inforce, Immersion Day or any other event hosted by an AWS employee). The instructions are geared towards using the AWS Workshop Portal which pre-provisions the resources needed to complete the workshop. Please make sure that the AWS employee stated this is how the workshop will be run. If not, then go to [start the workshop on your own](/content/secrets/prerequisites/self_paced/_index.md). Otherwise, please proceed to the below. 4 | 5 | * ...[Instructions on how to login to the AWS Workshop Portal](portal.md) 6 | -------------------------------------------------------------------------------- /content/secrets/prerequisites/aws_event/portal.md: -------------------------------------------------------------------------------- 1 | # Login to AWS Workshop Portal 2 | 3 | This workshop creates an AWS account and a Cloud9 environment. You will need the **Participant Hash** provided upon entry, and your email address to track your unique session. 4 | 5 | Connect to the portal by clicking the button or browsing to [https://portal.awsworkshop.io/](https://portal.awsworkshop.io/). 6 | 7 | 10 | Connect to Portal 11 | 12 | 13 | 14 | ![Portal Login](/static/images/secrets/prerequisites/portal_login.png) 15 | 16 | Enter your **Participant Hash** and your email address, and click **Log In**. 17 | 18 | Once you have been logged in, please first log into the AWS console by clicking on the button. Once you have successfully logged into the AWS Console, you can open the Cloud9 IDE by clicking on the button. 19 | 20 | ![Portal Buttons](/static/images/secrets/prerequisites/portal_buttons.png) 21 | 22 | The workshop added an IAM role for performing all the steps of the workshop in the Cloud9 Environment. You do not need to add a role to the instance powering the Cloud9 Environment. 23 | 24 | 25 | Once you have completed the step above, you can head straight to [**Getting Started**](/content/secrets/prerequisites/getting-started.md) -------------------------------------------------------------------------------- /content/secrets/prerequisites/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with this Workshop 2 | 3 | In order for you to succeed in this workshop, we need you to run through a few steps to finalize the configuration of your Cloud9 environment. You could do this workshop in your own environment using an IDE like Visual Studio, but for a consistent experience for all users, we will walk through setting up a Cloud9 environment instead. If you launched the CloudFormation resources in this module manually or are running this at an AWS Event, a Cloud9 environment has been provisioned for you. 4 | 5 | #### Update and install some tools 6 | 7 | The first step is to update the `AWS CLI`, `pip` and a range of pre-installed packages. 8 | 9 | ``` 10 | sudo yum update -y && pip install --upgrade --user awscli pip 11 | exec $SHELL 12 | ``` 13 | 14 | #### Configure the AWS Environment 15 | 16 | After you have the installed the latest awscli and pip we need to configure our environment to use us-west-2 17 | 18 | ``` 19 | aws configure set region us-west-2 20 | 21 | ``` 22 | 23 | #### Install the .NET Core SDK 24 | 25 | 1. In this step, you install the .NET Core 3 SDK into your environment, which is required to run this sample. Run the below command to install a ***libunwind*** package that the .NET Core 3 SDK needs. 26 | 27 | ``` 28 | sudo yum -y install libunwind 29 | ``` 30 | 31 | 2. Download the .NET Core 3 SDK installer script into your environment by running the following command. 32 | 33 | ``` 34 | curl -O -L https://dot.net/v1/dotnet-install.sh 35 | ``` 36 | 37 | 3. Make the installer script executable by the current user by running the following command. 38 | 39 | ``` 40 | sudo chmod u=rx dotnet-install.sh 41 | ``` 42 | 43 | 4. Run the installer script, which downloads and installs the .NET Core 3 SDK, by running the following command. 44 | 45 | ``` 46 | ./dotnet-install.sh -c Current 47 | ``` 48 | 49 | 5. Add the .NET Core 3 SDK to your PATH. To do this, in the shell profile for the environment (for example, the .bashrc file), add the $HOME/.dotnet subdirectory to the PATH variable for the environment, as follows. 50 | 51 | * Open the .bashrc file for editing by using the vi command. 52 | 53 | ``` 54 | vi ~/.bashrc 55 | ``` 56 | 57 | * Using the down arrow or j key, move to the line that starts with ***export PATH***. 58 | 59 | * Using the right arrow or $ key, move to the end of that line. 60 | 61 | * Switch to insert mode by pressing the i key. (-- INSERT --- will appear at the end of the display.) 62 | 63 | * Add the $HOME/.dotnet subdirectory to the PATH variable by typing :$HOME/.dotnet. Be sure to include the colon character (:). The line should now look similar to the following. 64 | 65 | ![bashrc](/static/images/secrets/prerequisites/bashrc.png) 66 | 67 | * Save the file. To do this, press the Esc key (-- INSERT --- will disappear from the end of the display), type :wq (to write to and then quit the file), and then press Enter. 68 | 69 | 6. Load the .NET Core 3 SDK by sourcing the .bashrc file. 70 | 71 | ``` 72 | . ~/.bashrc 73 | ``` 74 | 75 | 7. Confirm the .NET Core 3 SDK is loaded by running .NET Core CLI with the --help option. 76 | 77 | ``` 78 | dotnet --help 79 | ``` 80 | 81 | 8. If successful, the .NET Core 3 SDK version number is displayed, with additional usage information. 82 | 83 | #### Clone the source repository for this workshop 84 | 85 | Now we want to clone the repository that contains all of the content and files you need to complete this workshop. 86 | 87 | ``` 88 | cd ~/environment 89 | 90 | git clone https://github.com/aws-samples/modernization-unicorn-store.git 91 | ``` 92 | 93 | #### Installing Docker Compose 94 | 95 | For this workshop we use the tool [Docker Compose](https://docs.docker.com/compose/) which is a tool for defining and running multi-container Docker applications. 96 | 97 | 1. Run this command to download the current stable release of Docker Compose: 98 | 99 | ``` 100 | sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 101 | ``` 102 | 103 | 2. Apply executable permissions to the binary: 104 | 105 | ``` 106 | sudo chmod +x /usr/local/bin/docker-compose 107 | ``` 108 | 109 | 3. Test the installation: 110 | ``` 111 | docker-compose --version 112 | ``` 113 | 114 | #### Install JQ 115 | 116 | We will also be using a tool called [jq](https://stedolan.github.io/jq/) which is a lightweight and flexible command-line JSON processor. 117 | ``` 118 | sudo yum -y install jq 119 | ``` 120 | 121 | #### Clean up space on Cloud9 IDE 122 | 123 | We are using one of the smaller Cloud9 instances and space is limited so we can clean up some space by removing these docker images we won't be using by running the following command: 124 | 125 | ``` 126 | docker rmi $(docker images -q) 127 | ``` 128 | 129 | #### Congratulations 130 | 131 | You now have a fully working Cloud9 IDE that is ready to use! Click [**here**](/content/secrets/introduction.md) to start learning about secrets. 132 | -------------------------------------------------------------------------------- /content/secrets/prerequisites/self_paced/_index.md: -------------------------------------------------------------------------------- 1 | # Running the workshop on your own 2 | 3 | Only complete this section if you are running the workshop on your own. 4 | 5 | * ...[Create an AWS account](account.md) -------------------------------------------------------------------------------- /content/secrets/prerequisites/self_paced/account.md: -------------------------------------------------------------------------------- 1 | # Create an AWS account 2 | 3 | #### Your account must have the ability to create new IAM roles and scope other IAM permissions. 4 | 5 | 1. If you don't already have an AWS account with Administrator access: [create one now by clicking here](https://aws.amazon.com/getting-started/) 6 | 7 | 2. Once you have an AWS account, ensure you are following the remaining workshop steps as an IAM user with administrator access to the AWS account: 8 | [Create a new IAM user to use for the workshop](https://console.aws.amazon.com/iam/home?#/users$new) 9 | 10 | 3. Enter the user details: 11 | ![Create User](/static/images/secrets/prerequisites/iam-1-create-user.png) 12 | 13 | 4. Attach the AdministratorAccess IAM Policy: 14 | ![Attach Policy](/static/images/secrets/prerequisites/iam-2-attach-policy.png) 15 | 16 | 5. Click to create the new user: 17 | ![Confirm User](/static/images/secrets/prerequisites/iam-3-create-user.png) 18 | 19 | 6. Take note of the login URL and save: 20 | ![Login URL](/static/images/secrets/prerequisites/iam-4-save-url.png) 21 | 22 | 23 | You are now ready to launch the CloudFormation resources needed for this workshop. Click [**here**](cloudformation.md) to move to the next section where we will launch the CloudFormation resources. 24 | -------------------------------------------------------------------------------- /content/secrets/prerequisites/self_paced/cloudformation.md: -------------------------------------------------------------------------------- 1 | # Launch the CloudFormation resources 2 | 3 | To get started, you will need a S3 bucket to host the CloudFormation nested stack template for the VPC. To do this you can either create the bucket through the console and upload the template yourself or follow the below instructions using the AWS CLI. 4 | 5 | 1. Create the bucket. This workshop runs out of the us-west-2 so the `--region` flag is important so don't change it. Also, make sure you give a unique name for the `--bucket` value and write it down for later use. 6 | 7 | ``` 8 | aws s3api create-bucket --bucket modernization-unicorn-store --region us-west-2 --create-bucket-configuration LocationConstraint=us-west-2 9 | ``` 10 | 11 | 2. If you haven't already done so, download the two CloudFormation templates in the `content/secrets/cfn` directory in this repository to your local machine or just clone the repository. 12 | 13 | 3. Change to the `content/secrets/cfn` directory in this repository and upload the `player-vpc-template` to the s3 bucket you just created. Only change the `--bucket` value to your bucket. Keep the rest of the command as is. 14 | 15 | ``` 16 | aws s3api put-object --bucket modernization-unicorn-store --key modules/modernization/unicorn-store/templates/player-vpc-template.yaml --body player-vpc-template.yaml 17 | ``` 18 | 19 | 4. Deploy the CloudFormation template by running the below comannd. Only change the value for the `ParameterValue` key to your bucket. 20 | 21 | ``` 22 | aws cloudformation create-stack \ 23 | --stack-name modernization-unicorn-store \ 24 | --parameters ParameterKey=BucketName,ParameterValue=modernization-unicorn-store ParameterKey=BucketPrefix,ParameterValue=modules/modernization/unicorn-store \ 25 | --template-body file://player-template.yaml \ 26 | --capabilities CAPABILITY_NAMED_IAM \ 27 | --region us-west-2 28 | ``` 29 | 30 | 5. The CloudFormation stack takes a while to run due to it provisioning a RDS instance. Run this command and wait for the CloudFormation template to finish deploying. 31 | 32 | ``` 33 | until [[ `aws cloudformation describe-stacks --stack-name "modernization-unicorn-store" --region us-west-2 --query "Stacks[0].[StackStatus]" --output text` == "CREATE_COMPLETE" ]]; do echo "The stack is NOT in a state of CREATE_COMPLETE at `date`"; sleep 30; done && echo "The Stack is built at `date` - Please proceed" 34 | ``` 35 | 36 | Once you have completed with either setup, continue with [**Create a Workspace**](/content/secrets/prerequisites/getting-started.md) -------------------------------------------------------------------------------- /content/secrets/secrets.md: -------------------------------------------------------------------------------- 1 | # Secrets 2 | 3 | To get started, you will need certain secrets for the Unicorn Store to function. Since our end goal is to deploy the Unicorn Store into a container that retrieves the connection string to RDS from AWS Secrets Manager and other sensitive information, let’s first understand the format of the secret. The good news is both AWS Secrets Manager and .NET Core Secret Manager store the secret in JSON. When the lab was provisioned, the CloudFormation template created and stored a secret in AWS Secrets Manager with a secret-id of **UNICORNSTORE_DBSECRET** that contains the credentials to the UnicornStoreRDS AWS::RDS::DBInstance. Below is an example of a secret for the credentials to our Unicorn Store RDS Database from AWS Secrets Manager. 4 | 5 | ![examplesecret](/static/images/secrets/examplesecret.png) 6 | 7 | For .NET Core secrets in the Secret Manager tool, the values are stored in a JSON configuration file in a system-protected user profile folder on the local machine: 8 | 9 | | Linux/macOS File system path: | Windows File system path: | 10 | |---------------------------------------------------------|----------------------------------------------------------------| 11 | | ~/.microsoft/usersecrets//secrets.json | %APPDATA%\Microsoft\UserSecrets\\secrets.json | 12 | 13 | The value is the value that is defined in the [UnicornStore.csproj](https://github.com/aws-samples/modernization-unicorn-store/blob/master/UnicornStore/UnicornStore.csproj) file. This element has to be set in order to use user secrets in .NET Core. We have already set this value for you as defined below: 14 | 15 | ![csproj-usersecretsid](/static/images/secrets/csproj-usersecretsid.png) 16 | 17 | Let's populate our local secrets.json development file for the .NET Core Secret Manager tool. We are going to query AWS Secrets Manager to retrieve the UNICORNSTORE_DBSECRET and populate our local development environment with the values so we can connect to our test database in RDS. Go ahead and run the below commands: 18 | 19 | ``` 20 | AWSSECRET=$(aws secretsmanager get-secret-value --secret-id UNICORNSTORE_DBSECRET | jq -r '.SecretString') 21 | 22 | SECRET=$(printf '{"UNICORNSTORE_DBSECRET": %s}\n' $AWSSECRET) 23 | 24 | mkdir -p ~/.microsoft/usersecrets/45b651b1-da6a-44fb-af93-525b292efddb/ 25 | 26 | echo $SECRET | jq . > ~/.microsoft/usersecrets/45b651b1-da6a-44fb-af93-525b292efddb/secrets.json 27 | ``` 28 | 29 | Check to make sure the file was created and the contents contain the secret. The format should be the same as above but have your unique secret values. 30 | 31 | ``` 32 | cat ~/.microsoft/usersecrets/45b651b1-da6a-44fb-af93-525b292efddb/secrets.json 33 | ``` 34 | 35 | As you can see, the secrets.json file is outside of your project meaning they will not be committed to any source control systems! 36 | 37 | Click [**here**](/content/secrets/accessing-secrets.md) to move to the next section where we will explain how to access secrets in ASP.NET Core Secret Manager via the Configuration API. -------------------------------------------------------------------------------- /content/secrets/taskdefinitions.files/modernization-unicorn-store-task-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "modernization-unicorn", 3 | "networkMode": "awsvpc", 4 | "containerDefinitions": [ 5 | { 6 | "name": "modernization-unicorn-store_unicornstore", 7 | "image": ".dkr.ecr.us-west-2.amazonaws.com/modernization-unicorn-store:latest", 8 | "cpu": 512, 9 | "memoryReservation": 1024, 10 | "portMappings": [{ 11 | "containerPort": 80 12 | }], 13 | "environment": [ 14 | { 15 | "name": "ASPNETCORE_ENVIRONMENT", 16 | "value": "Production" 17 | } 18 | ], 19 | "secrets": [ 20 | { 21 | "name": "UNICORNSTORE_DBSECRET", 22 | "valueFrom": "" 23 | }, 24 | { 25 | "name": "DefaultAdminUsername", 26 | "valueFrom": "" 27 | 28 | }, 29 | { 30 | "name": "DefaultAdminPassword", 31 | "valueFrom" : "" 32 | } 33 | ], 34 | "logConfiguration": { 35 | "logDriver": "awslogs", 36 | "options": { 37 | "awslogs-group": "UnicornStore", 38 | "awslogs-region": "us-west-2", 39 | "awslogs-stream-prefix": "web" 40 | } 41 | } 42 | } 43 | ], 44 | "executionRoleArn": "arn:aws:iam:::role/UnicornStoreExecutionRole", 45 | "requiresCompatibilities": [ 46 | "FARGATE" 47 | ], 48 | "cpu": "1 vcpu", 49 | "memory": "2 gb" 50 | } 51 | -------------------------------------------------------------------------------- /content/secrets/taskdefinitions.md: -------------------------------------------------------------------------------- 1 | # Task Definitions 2 | 3 | ## Introduction 4 | 5 | [Amazon ECS Task Definitions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) are required to run Docker containers in Amazon ECS. 6 | 7 | ## Create the Unicorn Store Task Definition 8 | 9 | Copy/Paste the following commands into your terminal. 10 | 11 | ``` 12 | 13 | cd ~/environment/modernization-unicorn-store/content/secrets/taskdefinitions.files/ 14 | 15 | ``` 16 | 17 | Check the configuration of modernization-unicorn-store-task-definition.json by issuing the following command: 18 | 19 | ``` 20 | cat modernization-unicorn-store-task-definition.json | jq 21 | ``` 22 | 23 | You can see a couple of interesting key/values in this json file that defines our application. One of the most important sections is the ***secrets*** section. You will see here this is how are accessing AWS Secrets Manager. To inject sensitive data into your containers as environment variables, use the secrets container definition parameter. 24 | 25 | ![modernization-unicorn-store-task-definition](/static/images/secrets/modernization-unicorn-store-task-definition.png) 26 | 27 | We need to replace the placeholders for your account id and the individual arn's of each secret in the task definition template file so that it works in your account. The below commands do it automatically for you: 28 | 29 | Insert the AccountID for the image and the executionRoleArn: 30 | 31 | ``` 32 | ACCOUNT_ID=$(aws ecr describe-repositories --repository-name modernization-unicorn-store | jq -r '.repositories[] | {registryId}' | jq --raw-output '.registryId') 33 | 34 | echo $ACCOUNT_ID 35 | 36 | sed -i "s//${ACCOUNT_ID}/" modernization-unicorn-store-task-definition.json 37 | ``` 38 | 39 | Insert the UNICORNSTORE_DBSECRET Secrets Manager Arn: 40 | 41 | ``` 42 | DBSECRET_ARN=$(aws secretsmanager describe-secret --secret-id UNICORNSTORE_DBSECRET | jq -r '.ARN') 43 | 44 | echo $DBSECRET_ARN 45 | 46 | sed -i "s//${DBSECRET_ARN}/" modernization-unicorn-store-task-definition.json 47 | ``` 48 | 49 | Insert the DefaultAdminUsername Secrets Manager Arn: 50 | 51 | ``` 52 | AdminSecret_ARN=$(aws secretsmanager describe-secret --secret-id DefaultAdminUsername | jq -r '.ARN') 53 | 54 | echo $AdminSecret_ARN 55 | 56 | sed -i "s//${AdminSecret_ARN}/" modernization-unicorn-store-task-definition.json 57 | ``` 58 | 59 | Insert the DefaultAdminPassword Secrets Manager Arn: 60 | 61 | ``` 62 | PasswordSecret_ARN=$(aws secretsmanager describe-secret --secret-id DefaultAdminPassword | jq -r '.ARN') 63 | 64 | echo $PasswordSecret_ARN 65 | 66 | sed -i "s//${PasswordSecret_ARN}/" modernization-unicorn-store-task-definition.json 67 | ``` 68 | 69 | Go ahead and inspect the modernization-unicorn-store-task-definition.json file again to see that the configuration has been updated by issuing the following command: 70 | 71 | ``` 72 | cat modernization-unicorn-store-task-definition.json | jq 73 | ``` 74 | 75 | Now we can register the task definition with ECS from the JSON file by running the following command: 76 | 77 | ``` 78 | aws ecs register-task-definition --cli-input-json file://modernization-unicorn-store-task-definition.json 79 | ``` 80 | 81 | ## Required IAM Permissions for Amazon ECS Secrets 82 | 83 | You may be wondering how the task definition is going to access the secret at this point. It's as simple as adding a policy to the UnicornStoreExecutionRole IAM role that is specified in the task definition. When the lab was provisioned, the CloudFormation template created the IAM role for you with the appropriate permissions to provide access to the Secrets Manager resources. 84 | 85 | Feel free to explore the IAM Role for UnicornStoreExecutionRole in the AWS console or via the cli and look at the ***RetrieveUnicornSecret*** Inline policy. It should have something similar to below. 86 | 87 | ![retrieveunicornsecret-policy](/static/images/secrets/retrieveunicornsecret-policy.png) 88 | 89 | Click [**here**](/content/secrets/fargate.md) to move to the next section where we will create the service in AWS Fargate to run the Unicorn Store. 90 | -------------------------------------------------------------------------------- /docker-compose.development.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | unicornstore: 6 | environment: 7 | - ASPNETCORE_ENVIRONMENT=Development 8 | # The below environment variables are an example. 9 | # Never store passwords or other sensitive data in configuration provider code or in plain text configuration files. 10 | # Don't use production secrets in development or test environments. 11 | # Specify secrets outside of the project so that they can't be accidentally committed to a source code repository. 12 | # You need to UNCOMMENT the below and insert your db specific information. 13 | #- UNICORNSTORE_DBSECRET={"username":"unicorndbuser","password":"unicorndbpassword","engine":"sqlserver","host":"unicorn-db-us-west-2.rds.amazonaws.com","port":1433,"dbInstanceIdentifier":"unicorn-db"} 14 | #- DefaultAdminUsername=Administrator@test.com 15 | #- DefaultAdminPassword=Secret1* 16 | healthcheck: 17 | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/health"] 18 | interval: 30s 19 | timeout: 5s 20 | retries: 5 21 | start_period: 30s 22 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | unicornstore: 6 | environment: 7 | - ASPNETCORE_ENVIRONMENT=Production 8 | # The below environment variables are an example. 9 | # Never store passwords or other sensitive data in configuration provider code or in plain text configuration files. 10 | # Don't use production secrets in development or test environments. 11 | # Specify secrets outside of the project so that they can't be accidentally committed to a source code repository. 12 | # You need to UNCOMMENT the below and insert your db specific information. 13 | #- UNICORNSTORE_DBSECRET={"username":"unicorndbuser","password":"unicorndbpassword","engine":"sqlserver","host":"unicorn-db-us-west-2.rds.amazonaws.com","port":1433,"dbInstanceIdentifier":"unicorn-db"} 14 | #- DefaultAdminUsername=Administrator@test.com 15 | #- DefaultAdminPassword=Secret1* 16 | healthcheck: 17 | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/health"] 18 | interval: 30s 19 | timeout: 5s 20 | retries: 5 21 | start_period: 30s 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | unicornstore: 6 | build: 7 | context: ./ 8 | dockerfile: UnicornStore/Dockerfile 9 | ports: 10 | - 8080:80 11 | -------------------------------------------------------------------------------- /static/640px-Amazon_Web_Services_Logo.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/640px-Amazon_Web_Services_Logo.svg.png -------------------------------------------------------------------------------- /static/AWS-Logo.svg: -------------------------------------------------------------------------------- 1 | AWS-Logo_White-Color -------------------------------------------------------------------------------- /static/Amazon_Web_Services_Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /static/images/secrets/cloud9-dotnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/cloud9-dotnet.png -------------------------------------------------------------------------------- /static/images/secrets/connectionstring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/connectionstring.png -------------------------------------------------------------------------------- /static/images/secrets/csproj-usersecretsid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/csproj-usersecretsid.png -------------------------------------------------------------------------------- /static/images/secrets/docker-compose-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/docker-compose-development.png -------------------------------------------------------------------------------- /static/images/secrets/examplesecret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/examplesecret.png -------------------------------------------------------------------------------- /static/images/secrets/modernization-unicorn-store-task-definition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/modernization-unicorn-store-task-definition.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/bashrc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/bashrc.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/iam-1-create-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/iam-1-create-user.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/iam-2-attach-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/iam-2-attach-policy.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/iam-3-create-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/iam-3-create-user.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/iam-4-save-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/iam-4-save-url.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/portal_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/portal_buttons.png -------------------------------------------------------------------------------- /static/images/secrets/prerequisites/portal_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/prerequisites/portal_login.png -------------------------------------------------------------------------------- /static/images/secrets/program-createdefaultbuilder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/program-createdefaultbuilder.png -------------------------------------------------------------------------------- /static/images/secrets/retrieveunicornsecret-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/retrieveunicornsecret-policy.png -------------------------------------------------------------------------------- /static/images/secrets/secret-appsettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/secret-appsettings.png -------------------------------------------------------------------------------- /static/images/secrets/secret-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/secret-final.png -------------------------------------------------------------------------------- /static/images/secrets/secrets-manager-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/secrets-manager-architecture.png -------------------------------------------------------------------------------- /static/images/secrets/startup-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/startup-configuration.png -------------------------------------------------------------------------------- /static/images/secrets/unicornstore-docker-images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/unicornstore-docker-images.png -------------------------------------------------------------------------------- /static/images/secrets/unicornstore-ecr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/unicornstore-ecr.png -------------------------------------------------------------------------------- /static/images/secrets/unicornstore-prod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/secrets/unicornstore-prod.png -------------------------------------------------------------------------------- /static/images/unicornstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/modernization-unicorn-store/ee4470ddee7503eda40ed7af82c34d0a61ac1d9d/static/images/unicornstore.png --------------------------------------------------------------------------------