├── .gitignore ├── LICENSE ├── MyCoolWebServer.sln ├── MyCoolWebServer ├── Application │ ├── Controllers │ │ └── HomeController.cs │ ├── MainApplication.cs │ └── Views │ │ └── Home │ │ ├── IndexView.cs │ │ └── SessionTestView.cs ├── ByTheCakeApplication │ ├── ByTheCakeApp.cs │ ├── Controllers │ │ ├── AccountController.cs │ │ ├── BaseController.cs │ │ ├── HomeController.cs │ │ ├── ProductsController.cs │ │ └── ShoppingController.cs │ ├── Data │ │ ├── ByTheCakeDbContext.cs │ │ ├── Migrations │ │ │ ├── 20171010110642_InitialMigration.Designer.cs │ │ │ ├── 20171010110642_InitialMigration.cs │ │ │ └── ByTheCakeDbContextModelSnapshot.cs │ │ └── Models │ │ │ ├── Order.cs │ │ │ ├── OrderProduct.cs │ │ │ ├── Product.cs │ │ │ └── User.cs │ ├── Resources │ │ ├── Account │ │ │ ├── login.html │ │ │ ├── profile.html │ │ │ └── register.html │ │ ├── Home │ │ │ ├── about.html │ │ │ └── index.html │ │ ├── Products │ │ │ ├── add.html │ │ │ ├── details.html │ │ │ └── search.html │ │ ├── Shopping │ │ │ ├── cart.html │ │ │ └── finish-order.html │ │ └── layout.html │ ├── Services │ │ ├── IProductService.cs │ │ ├── IShoppingService.cs │ │ ├── IUserService.cs │ │ ├── ProductService.cs │ │ ├── ShoppingService.cs │ │ └── UserService.cs │ └── ViewModels │ │ ├── Account │ │ ├── LoginViewModel.cs │ │ ├── ProfileViewModel.cs │ │ └── RegisterUserViewModel.cs │ │ ├── Products │ │ ├── AddProductViewModel.cs │ │ ├── ProductDetailsViewModel.cs │ │ ├── ProductInCartViewModel.cs │ │ └── ProductListingViewModel.cs │ │ └── ShoppingCart.cs ├── GameStoreApplication │ ├── Common │ │ ├── Authentication.cs │ │ └── ValidationConstants.cs │ ├── Controllers │ │ ├── AccountController.cs │ │ ├── AdminController.cs │ │ ├── BaseController.cs │ │ └── HomeController.cs │ ├── Data │ │ ├── GameStoreDbContext.cs │ │ ├── Migrations │ │ │ ├── 20171013110432_GameStoreInitialDatabase.Designer.cs │ │ │ ├── 20171013110432_GameStoreInitialDatabase.cs │ │ │ └── GameStoreDbContextModelSnapshot.cs │ │ └── Models │ │ │ ├── Game.cs │ │ │ ├── User.cs │ │ │ └── UserGame.cs │ ├── GameStoreApp.cs │ ├── Resources │ │ ├── Account │ │ │ ├── login.html │ │ │ └── register.html │ │ ├── Admin │ │ │ ├── add-game.html │ │ │ └── list-games.html │ │ ├── Home │ │ │ └── index.html │ │ └── layout.html │ ├── Services │ │ ├── Contracts │ │ │ ├── IGameService.cs │ │ │ └── IUserService.cs │ │ ├── GameService.cs │ │ └── UserService.cs │ ├── Utilities │ │ └── PasswordAttribute.cs │ └── ViewModels │ │ ├── Account │ │ ├── LoginViewModel.cs │ │ └── RegisterViewModel.cs │ │ └── Admin │ │ ├── AdminAddGameViewModel.cs │ │ └── AdminListGameViewModel.cs ├── Infrastructure │ ├── Controller.cs │ └── FileView.cs ├── Launcher.cs ├── MyCoolWebServer.csproj └── Server │ ├── Common │ ├── CoreValidator.cs │ ├── InternalServerErrorView.cs │ └── NotFoundView.cs │ ├── ConnectionHandler.cs │ ├── Contracts │ ├── IApplication.cs │ ├── IRunnable.cs │ └── IView.cs │ ├── Enums │ ├── HttpRequestMethod.cs │ └── HttpStatusCode.cs │ ├── Exceptions │ ├── BadRequestException.cs │ └── InvalidResponseException.cs │ ├── Handlers │ ├── Contracts │ │ └── IRequestHandler.cs │ ├── HttpHandler.cs │ └── RequestHandler.cs │ ├── Http │ ├── Contracts │ │ ├── IHttpContext.cs │ │ ├── IHttpCookieCollection.cs │ │ ├── IHttpHeaderCollection.cs │ │ ├── IHttpRequest.cs │ │ ├── IHttpResponse.cs │ │ └── IHttpSession.cs │ ├── HttpContext.cs │ ├── HttpCookie.cs │ ├── HttpCookieCollection.cs │ ├── HttpHeader.cs │ ├── HttpHeaderCollection.cs │ ├── HttpRequest.cs │ ├── HttpSession.cs │ ├── Response │ │ ├── BadRequestResponse.cs │ │ ├── HttpResponse.cs │ │ ├── InternalServerErrorResponse.cs │ │ ├── NotFoundResponse.cs │ │ ├── RedirectResponse.cs │ │ └── ViewResponse.cs │ └── SessionStore.cs │ ├── Routing │ ├── AppRouteConfig.cs │ ├── Contracts │ │ ├── IAppRouteConfig.cs │ │ ├── IRoutingContext.cs │ │ └── IServerRouteConfig.cs │ ├── RoutingContext.cs │ └── ServerRouteConfig.cs │ └── WebServer.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ivaylo Kenov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MyCoolWebServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.16 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyCoolWebServer", "MyCoolWebServer\MyCoolWebServer.csproj", "{D4B182EA-BA0D-45A6-A312-3B0A2662F1AD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D4B182EA-BA0D-45A6-A312-3B0A2662F1AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {D4B182EA-BA0D-45A6-A312-3B0A2662F1AD}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {D4B182EA-BA0D-45A6-A312-3B0A2662F1AD}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {D4B182EA-BA0D-45A6-A312-3B0A2662F1AD}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3ABA898C-743A-4BCB-94FE-871C34896B4B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /MyCoolWebServer/Application/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Application.Controllers 2 | { 3 | using Application.Views.Home; 4 | using Server.Enums; 5 | using Server.Http; 6 | using Server.Http.Response; 7 | using Server.Http.Contracts; 8 | using System; 9 | 10 | public class HomeController 11 | { 12 | public IHttpResponse Index() 13 | { 14 | var response = new ViewResponse(HttpStatusCode.Ok, new IndexView()); 15 | 16 | response.Cookies.Add(new HttpCookie("lang", "en")); 17 | 18 | return response; 19 | } 20 | 21 | public IHttpResponse SessionTest(IHttpRequest req) 22 | { 23 | var session = req.Session; 24 | 25 | const string sessionDateKey = "saved_date"; 26 | 27 | if (session.Get(sessionDateKey) == null) 28 | { 29 | session.Add(sessionDateKey, DateTime.UtcNow); 30 | } 31 | 32 | return new ViewResponse( 33 | HttpStatusCode.Ok, 34 | new SessionTestView(session.Get(sessionDateKey))); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MyCoolWebServer/Application/MainApplication.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Application 2 | { 3 | using Application.Controllers; 4 | using Server.Contracts; 5 | using Server.Routing.Contracts; 6 | 7 | public class MainApplication : IApplication 8 | { 9 | public void Configure(IAppRouteConfig appRouteConfig) 10 | { 11 | appRouteConfig.Get( 12 | "/", 13 | req => new HomeController().Index()); 14 | 15 | appRouteConfig.Get( 16 | "/testsession", 17 | req => new HomeController().SessionTest(req)); 18 | 19 | appRouteConfig.Get( 20 | "/users/{(?[a-z]+)}", 21 | req => new HomeController().Index()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MyCoolWebServer/Application/Views/Home/IndexView.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Application.Views.Home 2 | { 3 | using Server.Contracts; 4 | 5 | public class IndexView : IView 6 | { 7 | public string View() => "

Welcome!

"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/Application/Views/Home/SessionTestView.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Application.Views.Home 2 | { 3 | using Server.Contracts; 4 | using System; 5 | 6 | public class SessionTestView : IView 7 | { 8 | private readonly DateTime dateTime; 9 | 10 | public SessionTestView(DateTime dateTime) 11 | { 12 | this.dateTime = dateTime; 13 | } 14 | 15 | public string View() 16 | { 17 | return $"

Saved date: {dateTime}

"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ByTheCakeApp.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication 2 | { 3 | using Controllers; 4 | using Data; 5 | using Microsoft.EntityFrameworkCore; 6 | using Server.Contracts; 7 | using Server.Routing.Contracts; 8 | using ViewModels.Account; 9 | using ViewModels.Products; 10 | 11 | public class ByTheCakeApp : IApplication 12 | { 13 | public void InitializeDatabase() 14 | { 15 | using (var db = new ByTheCakeDbContext()) 16 | { 17 | db.Database.Migrate(); 18 | } 19 | } 20 | 21 | public void Configure(IAppRouteConfig appRouteConfig) 22 | { 23 | appRouteConfig.AnonymousPaths.Add("/register"); 24 | appRouteConfig.AnonymousPaths.Add("/login"); 25 | 26 | appRouteConfig 27 | .Get("/", req => new HomeController().Index()); 28 | 29 | appRouteConfig 30 | .Get("/about", req => new HomeController().About()); 31 | 32 | appRouteConfig 33 | .Get("/add", req => new ProductsController().Add()); 34 | 35 | appRouteConfig 36 | .Post( 37 | "/add", 38 | req => new ProductsController().Add( 39 | new AddProductViewModel 40 | { 41 | Name = req.FormData["name"], 42 | Price = decimal.Parse(req.FormData["price"]), 43 | ImageUrl = req.FormData["imageUrl"] 44 | })); 45 | 46 | appRouteConfig 47 | .Get( 48 | "/search", 49 | req => new ProductsController().Search(req)); 50 | 51 | appRouteConfig 52 | .Get( 53 | "/cakes/{(?[0-9]+)}", 54 | req => new ProductsController() 55 | .Details(int.Parse(req.UrlParameters["id"]))); 56 | 57 | appRouteConfig 58 | .Get( 59 | "/register", 60 | req => new AccountController().Register()); 61 | 62 | appRouteConfig 63 | .Post( 64 | "/register", 65 | req => new AccountController().Register( 66 | req, 67 | new RegisterUserViewModel 68 | { 69 | Username = req.FormData["username"], 70 | Password = req.FormData["password"], 71 | ConfirmPassword = req.FormData["confirm-password"] 72 | })); 73 | 74 | appRouteConfig 75 | .Get( 76 | "/login", 77 | req => new AccountController().Login()); 78 | 79 | appRouteConfig 80 | .Post( 81 | "/login", 82 | req => new AccountController().Login( 83 | req, 84 | new LoginViewModel 85 | { 86 | Username = req.FormData["username"], 87 | Password = req.FormData["password"] 88 | })); 89 | 90 | appRouteConfig 91 | .Get( 92 | "/profile", 93 | req => new AccountController().Profile(req)); 94 | 95 | appRouteConfig 96 | .Post( 97 | "/logout", 98 | req => new AccountController().Logout(req)); 99 | 100 | appRouteConfig 101 | .Get( 102 | "/shopping/add/{(?[0-9]+)}", 103 | req => new ShoppingController().AddToCart(req)); 104 | 105 | appRouteConfig 106 | .Get( 107 | "/cart", 108 | req => new ShoppingController().ShowCart(req)); 109 | 110 | appRouteConfig 111 | .Post( 112 | "/shopping/finish-order", 113 | req => new ShoppingController().FinishOrder(req)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Controllers 2 | { 3 | using Server.Http; 4 | using Server.Http.Contracts; 5 | using Server.Http.Response; 6 | using Services; 7 | using System; 8 | using ViewModels; 9 | using ViewModels.Account; 10 | 11 | public class AccountController : BaseController 12 | { 13 | private const string RegisterView = @"account\register"; 14 | private const string LoginView = @"account\login"; 15 | 16 | private readonly IUserService users; 17 | 18 | public AccountController() 19 | { 20 | this.users = new UserService(); 21 | } 22 | 23 | public IHttpResponse Register() 24 | { 25 | this.SetDefaultViewData(); 26 | return this.FileViewResponse(RegisterView); 27 | } 28 | 29 | public IHttpResponse Register( 30 | IHttpRequest req, 31 | RegisterUserViewModel model) 32 | { 33 | this.SetDefaultViewData(); 34 | 35 | if (model.Username.Length < 3 36 | || model.Password.Length < 3 37 | || model.ConfirmPassword != model.Password) 38 | { 39 | this.ShowError("Invalid user details"); 40 | 41 | return this.FileViewResponse(RegisterView); 42 | } 43 | 44 | var success = this.users.Create(model.Username, model.Password); 45 | 46 | if (success) 47 | { 48 | this.LoginUser(req, model.Username); 49 | 50 | return new RedirectResponse("/"); 51 | } 52 | else 53 | { 54 | this.ShowError("This username is taken"); 55 | 56 | return this.FileViewResponse(RegisterView); 57 | } 58 | } 59 | 60 | public IHttpResponse Login() 61 | { 62 | this.SetDefaultViewData(); 63 | return this.FileViewResponse(LoginView); 64 | } 65 | 66 | public IHttpResponse Login( 67 | IHttpRequest req, 68 | LoginViewModel model) 69 | { 70 | if (string.IsNullOrWhiteSpace(model.Username) 71 | || string.IsNullOrWhiteSpace(model.Password)) 72 | { 73 | this.ShowError("You have empty fields"); 74 | 75 | return this.FileViewResponse(LoginView); 76 | } 77 | 78 | var success = this.users.Find(model.Username, model.Password); 79 | 80 | if (success) 81 | { 82 | this.LoginUser(req, model.Username); 83 | 84 | return new RedirectResponse("/"); 85 | } 86 | else 87 | { 88 | this.ShowError("Invalid user details"); 89 | 90 | return this.FileViewResponse(LoginView); 91 | } 92 | } 93 | 94 | public IHttpResponse Profile(IHttpRequest req) 95 | { 96 | if (!req.Session.Contains(SessionStore.CurrentUserKey)) 97 | { 98 | throw new InvalidOperationException("There is no logged in user."); 99 | } 100 | 101 | var username = req.Session.Get(SessionStore.CurrentUserKey); 102 | 103 | var profile = this.users.Profile(username); 104 | 105 | if (profile == null) 106 | { 107 | throw new InvalidOperationException($"The user {username} could not be found in the database."); 108 | } 109 | 110 | this.ViewData["username"] = profile.Username; 111 | this.ViewData["registrationDate"] = profile.RegistrationDate.ToShortDateString(); 112 | this.ViewData["totalOrders"] = profile.TotalOrders.ToString(); 113 | 114 | return this.FileViewResponse(@"account\profile"); 115 | } 116 | 117 | public IHttpResponse Logout(IHttpRequest req) 118 | { 119 | req.Session.Clear(); 120 | 121 | return new RedirectResponse("/login"); 122 | } 123 | 124 | private void SetDefaultViewData() => this.ViewData["authDisplay"] = "none"; 125 | 126 | private void LoginUser(IHttpRequest req, string username) 127 | { 128 | req.Session.Add(SessionStore.CurrentUserKey, username); 129 | req.Session.Add(ShoppingCart.SessionKey, new ShoppingCart()); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Controllers 2 | { 3 | using Infrastructure; 4 | 5 | public abstract class BaseController : Controller 6 | { 7 | protected override string ApplicationDirectory => "ByTheCakeApplication"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Controllers 2 | { 3 | using Infrastructure; 4 | using Server.Http.Contracts; 5 | 6 | public class HomeController : BaseController 7 | { 8 | public IHttpResponse Index() => this.FileViewResponse(@"home\index"); 9 | 10 | public IHttpResponse About() => this.FileViewResponse(@"home\about"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Controllers/ProductsController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Controllers 2 | { 3 | using Infrastructure; 4 | using Server.Http.Contracts; 5 | using Server.Http.Response; 6 | using Services; 7 | using System; 8 | using System.Linq; 9 | using ViewModels; 10 | using ViewModels.Products; 11 | 12 | public class ProductsController : BaseController 13 | { 14 | private const string AddView = @"products\add"; 15 | 16 | private readonly IProductService products; 17 | 18 | public ProductsController() 19 | { 20 | this.products = new ProductService(); 21 | } 22 | 23 | public IHttpResponse Add() 24 | { 25 | this.ViewData["showResult"] = "none"; 26 | 27 | return this.FileViewResponse(AddView); 28 | } 29 | 30 | public IHttpResponse Add(AddProductViewModel model) 31 | { 32 | if (model.Name.Length < 3 33 | || model.Name.Length > 30 34 | || model.ImageUrl.Length < 3 35 | || model.ImageUrl.Length > 2000) 36 | { 37 | this.ShowError("Product information is not valid"); 38 | 39 | return this.FileViewResponse(AddView); 40 | } 41 | 42 | this.products.Create(model.Name, model.Price, model.ImageUrl); 43 | 44 | this.ViewData["name"] = model.Name; 45 | this.ViewData["price"] = model.Price.ToString(); 46 | this.ViewData["imageUrl"] = model.ImageUrl; 47 | this.ViewData["showResult"] = "block"; 48 | 49 | return this.FileViewResponse(AddView); 50 | } 51 | 52 | public IHttpResponse Search(IHttpRequest req) 53 | { 54 | const string searchTermKey = "searchTerm"; 55 | 56 | var urlParameters = req.UrlParameters; 57 | 58 | this.ViewData["results"] = string.Empty; 59 | 60 | var searchTerm = urlParameters.ContainsKey(searchTermKey) 61 | ? urlParameters[searchTermKey] 62 | : null; 63 | 64 | this.ViewData["searchTerm"] = searchTerm; 65 | 66 | var result = this.products.All(searchTerm); 67 | 68 | if (!result.Any()) 69 | { 70 | this.ViewData["results"] = "No cakes found"; 71 | } 72 | else 73 | { 74 | var allProducts = result 75 | .Select(c => $@"
{c.Name} - ${c.Price:F2} Order
"); 76 | 77 | var allProductsAsString = string.Join(Environment.NewLine, allProducts); 78 | 79 | this.ViewData["results"] = allProductsAsString; 80 | } 81 | 82 | this.ViewData["showCart"] = "none"; 83 | 84 | var shoppingCart = req.Session.Get(ShoppingCart.SessionKey); 85 | 86 | if (shoppingCart.ProductIds.Any()) 87 | { 88 | var totalProducts = shoppingCart.ProductIds.Count; 89 | var totalProductsText = totalProducts != 1 ? "products" : "product"; 90 | 91 | this.ViewData["showCart"] = "block"; 92 | this.ViewData["products"] = $"{totalProducts} {totalProductsText}"; 93 | } 94 | 95 | return this.FileViewResponse(@"products\search"); 96 | } 97 | 98 | public IHttpResponse Details(int id) 99 | { 100 | var product = this.products.Find(id); 101 | 102 | if (product == null) 103 | { 104 | return new NotFoundResponse(); 105 | } 106 | 107 | this.ViewData["name"] = product.Name; 108 | this.ViewData["price"] = product.Price.ToString("F2"); 109 | this.ViewData["imageUrl"] = product.ImageUrl; 110 | 111 | return this.FileViewResponse(@"products\details"); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Controllers/ShoppingController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Controllers 2 | { 3 | using Infrastructure; 4 | using Server.Http; 5 | using Server.Http.Contracts; 6 | using Server.Http.Response; 7 | using Services; 8 | using System; 9 | using System.Linq; 10 | using ViewModels; 11 | 12 | public class ShoppingController : BaseController 13 | { 14 | private readonly IUserService users; 15 | private readonly IProductService products; 16 | private readonly IShoppingService shopping; 17 | 18 | public ShoppingController() 19 | { 20 | this.users = new UserService(); 21 | this.products = new ProductService(); 22 | this.shopping = new ShoppingService(); 23 | } 24 | 25 | public IHttpResponse AddToCart(IHttpRequest req) 26 | { 27 | var id = int.Parse(req.UrlParameters["id"]); 28 | 29 | var productExists = this.products.Exists(id); 30 | 31 | if (!productExists) 32 | { 33 | return new NotFoundResponse(); 34 | } 35 | 36 | var shoppingCart = req.Session.Get(ShoppingCart.SessionKey); 37 | shoppingCart.ProductIds.Add(id); 38 | 39 | var redirectUrl = "/search"; 40 | 41 | const string searchTermKey = "searchTerm"; 42 | 43 | if (req.UrlParameters.ContainsKey(searchTermKey)) 44 | { 45 | redirectUrl = $"{redirectUrl}?{searchTermKey}={req.UrlParameters[searchTermKey]}"; 46 | } 47 | 48 | return new RedirectResponse(redirectUrl); 49 | } 50 | 51 | public IHttpResponse ShowCart(IHttpRequest req) 52 | { 53 | var shoppingCart = req.Session.Get(ShoppingCart.SessionKey); 54 | 55 | if (!shoppingCart.ProductIds.Any()) 56 | { 57 | this.ViewData["cartItems"] = "No items in your cart"; 58 | this.ViewData["totalCost"] = "0.00"; 59 | } 60 | else 61 | { 62 | var productsInCart = this.products 63 | .FindProductsInCart(shoppingCart.ProductIds); 64 | 65 | var items = productsInCart 66 | .Select(pr => $"
{pr.Name} - ${pr.Price:F2}

"); 67 | 68 | var totalPrice = productsInCart 69 | .Sum(pr => pr.Price); 70 | 71 | this.ViewData["cartItems"] = string.Join(string.Empty, items); 72 | this.ViewData["totalCost"] = $"{totalPrice:F2}"; 73 | } 74 | 75 | return this.FileViewResponse(@"shopping\cart"); 76 | } 77 | 78 | public IHttpResponse FinishOrder(IHttpRequest req) 79 | { 80 | var username = req.Session.Get(SessionStore.CurrentUserKey); 81 | var shoppingCart = req.Session.Get(ShoppingCart.SessionKey); 82 | 83 | var userId = this.users.GetUserId(username); 84 | if (userId == null) 85 | { 86 | throw new InvalidOperationException($"User {username} does not exist"); 87 | } 88 | 89 | var productIds = shoppingCart.ProductIds; 90 | if (!productIds.Any()) 91 | { 92 | return new RedirectResponse("/"); 93 | } 94 | 95 | this.shopping.CreateOrder(userId.Value, productIds); 96 | 97 | shoppingCart.ProductIds.Clear(); 98 | 99 | return this.FileViewResponse(@"shopping\finish-order"); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/ByTheCakeDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Data 2 | { 3 | using Microsoft.EntityFrameworkCore; 4 | using Models; 5 | 6 | public class ByTheCakeDbContext : DbContext 7 | { 8 | public DbSet Users { get; set; } 9 | 10 | public DbSet Products { get; set; } 11 | 12 | public DbSet Orders { get; set; } 13 | 14 | protected override void OnConfiguring(DbContextOptionsBuilder builder) 15 | { 16 | builder 17 | .UseSqlServer("Server=.;Database=ByTheCakeDb;Integrated Security=True;"); 18 | } 19 | 20 | protected override void OnModelCreating(ModelBuilder builder) 21 | { 22 | builder 23 | .Entity() 24 | .HasKey(op => new { op.OrderId, op.ProductId }); 25 | 26 | builder 27 | .Entity() 28 | .HasMany(u => u.Orders) 29 | .WithOne(o => o.User) 30 | .HasForeignKey(o => o.UserId); 31 | 32 | builder 33 | .Entity() 34 | .HasMany(pr => pr.Orders) 35 | .WithOne(op => op.Product) 36 | .HasForeignKey(op => op.ProductId); 37 | 38 | builder 39 | .Entity() 40 | .HasMany(o => o.Products) 41 | .WithOne(op => op.Order) 42 | .HasForeignKey(op => op.OrderId); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Migrations/20171010110642_InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using System; 7 | 8 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Migrations 9 | { 10 | [DbContext(typeof(ByTheCakeDbContext))] 11 | [Migration("20171010110642_InitialMigration")] 12 | partial class InitialMigration 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 19 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 20 | 21 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.Order", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd(); 25 | 26 | b.Property("CreationDate"); 27 | 28 | b.Property("UserId"); 29 | 30 | b.HasKey("Id"); 31 | 32 | b.HasIndex("UserId"); 33 | 34 | b.ToTable("Orders"); 35 | }); 36 | 37 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.OrderProduct", b => 38 | { 39 | b.Property("OrderId"); 40 | 41 | b.Property("ProductId"); 42 | 43 | b.HasKey("OrderId", "ProductId"); 44 | 45 | b.HasIndex("ProductId"); 46 | 47 | b.ToTable("OrderProduct"); 48 | }); 49 | 50 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.Product", b => 51 | { 52 | b.Property("Id") 53 | .ValueGeneratedOnAdd(); 54 | 55 | b.Property("ImageUrl") 56 | .IsRequired() 57 | .HasMaxLength(2000); 58 | 59 | b.Property("Name") 60 | .IsRequired() 61 | .HasMaxLength(30); 62 | 63 | b.Property("Price"); 64 | 65 | b.HasKey("Id"); 66 | 67 | b.ToTable("Products"); 68 | }); 69 | 70 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.User", b => 71 | { 72 | b.Property("Id") 73 | .ValueGeneratedOnAdd(); 74 | 75 | b.Property("Name"); 76 | 77 | b.Property("Password") 78 | .IsRequired() 79 | .HasMaxLength(100); 80 | 81 | b.Property("RegistrationDate"); 82 | 83 | b.Property("Username") 84 | .IsRequired() 85 | .HasMaxLength(20); 86 | 87 | b.HasKey("Id"); 88 | 89 | b.ToTable("Users"); 90 | }); 91 | 92 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.Order", b => 93 | { 94 | b.HasOne("MyCoolWebServer.ByTheCakeApplication.Data.Models.User", "User") 95 | .WithMany("Orders") 96 | .HasForeignKey("UserId") 97 | .OnDelete(DeleteBehavior.Cascade); 98 | }); 99 | 100 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.OrderProduct", b => 101 | { 102 | b.HasOne("MyCoolWebServer.ByTheCakeApplication.Data.Models.Order", "Order") 103 | .WithMany("Products") 104 | .HasForeignKey("OrderId") 105 | .OnDelete(DeleteBehavior.Cascade); 106 | 107 | b.HasOne("MyCoolWebServer.ByTheCakeApplication.Data.Models.Product", "Product") 108 | .WithMany("Orders") 109 | .HasForeignKey("ProductId") 110 | .OnDelete(DeleteBehavior.Cascade); 111 | }); 112 | #pragma warning restore 612, 618 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Migrations/20171010110642_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using System; 4 | 5 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Migrations 6 | { 7 | public partial class InitialMigration : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Products", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "int", nullable: false) 16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 17 | ImageUrl = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: false), 18 | Name = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), 19 | Price = table.Column(type: "decimal(18, 2)", nullable: false) 20 | }, 21 | constraints: table => 22 | { 23 | table.PrimaryKey("PK_Products", x => x.Id); 24 | }); 25 | 26 | migrationBuilder.CreateTable( 27 | name: "Users", 28 | columns: table => new 29 | { 30 | Id = table.Column(type: "int", nullable: false) 31 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 32 | Name = table.Column(type: "nvarchar(max)", nullable: true), 33 | Password = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 34 | RegistrationDate = table.Column(type: "datetime2", nullable: false), 35 | Username = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false) 36 | }, 37 | constraints: table => 38 | { 39 | table.PrimaryKey("PK_Users", x => x.Id); 40 | }); 41 | 42 | migrationBuilder.CreateTable( 43 | name: "Orders", 44 | columns: table => new 45 | { 46 | Id = table.Column(type: "int", nullable: false) 47 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 48 | CreationDate = table.Column(type: "datetime2", nullable: false), 49 | UserId = table.Column(type: "int", nullable: false) 50 | }, 51 | constraints: table => 52 | { 53 | table.PrimaryKey("PK_Orders", x => x.Id); 54 | table.ForeignKey( 55 | name: "FK_Orders_Users_UserId", 56 | column: x => x.UserId, 57 | principalTable: "Users", 58 | principalColumn: "Id", 59 | onDelete: ReferentialAction.Cascade); 60 | }); 61 | 62 | migrationBuilder.CreateTable( 63 | name: "OrderProduct", 64 | columns: table => new 65 | { 66 | OrderId = table.Column(type: "int", nullable: false), 67 | ProductId = table.Column(type: "int", nullable: false) 68 | }, 69 | constraints: table => 70 | { 71 | table.PrimaryKey("PK_OrderProduct", x => new { x.OrderId, x.ProductId }); 72 | table.ForeignKey( 73 | name: "FK_OrderProduct_Orders_OrderId", 74 | column: x => x.OrderId, 75 | principalTable: "Orders", 76 | principalColumn: "Id", 77 | onDelete: ReferentialAction.Cascade); 78 | table.ForeignKey( 79 | name: "FK_OrderProduct_Products_ProductId", 80 | column: x => x.ProductId, 81 | principalTable: "Products", 82 | principalColumn: "Id", 83 | onDelete: ReferentialAction.Cascade); 84 | }); 85 | 86 | migrationBuilder.CreateIndex( 87 | name: "IX_OrderProduct_ProductId", 88 | table: "OrderProduct", 89 | column: "ProductId"); 90 | 91 | migrationBuilder.CreateIndex( 92 | name: "IX_Orders_UserId", 93 | table: "Orders", 94 | column: "UserId"); 95 | } 96 | 97 | protected override void Down(MigrationBuilder migrationBuilder) 98 | { 99 | migrationBuilder.DropTable( 100 | name: "OrderProduct"); 101 | 102 | migrationBuilder.DropTable( 103 | name: "Orders"); 104 | 105 | migrationBuilder.DropTable( 106 | name: "Products"); 107 | 108 | migrationBuilder.DropTable( 109 | name: "Users"); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Migrations/ByTheCakeDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using System; 6 | 7 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Migrations 8 | { 9 | [DbContext(typeof(ByTheCakeDbContext))] 10 | partial class ByTheCakeDbContextModelSnapshot : ModelSnapshot 11 | { 12 | protected override void BuildModel(ModelBuilder modelBuilder) 13 | { 14 | #pragma warning disable 612, 618 15 | modelBuilder 16 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 17 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 18 | 19 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.Order", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd(); 23 | 24 | b.Property("CreationDate"); 25 | 26 | b.Property("UserId"); 27 | 28 | b.HasKey("Id"); 29 | 30 | b.HasIndex("UserId"); 31 | 32 | b.ToTable("Orders"); 33 | }); 34 | 35 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.OrderProduct", b => 36 | { 37 | b.Property("OrderId"); 38 | 39 | b.Property("ProductId"); 40 | 41 | b.HasKey("OrderId", "ProductId"); 42 | 43 | b.HasIndex("ProductId"); 44 | 45 | b.ToTable("OrderProduct"); 46 | }); 47 | 48 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.Product", b => 49 | { 50 | b.Property("Id") 51 | .ValueGeneratedOnAdd(); 52 | 53 | b.Property("ImageUrl") 54 | .IsRequired() 55 | .HasMaxLength(2000); 56 | 57 | b.Property("Name") 58 | .IsRequired() 59 | .HasMaxLength(30); 60 | 61 | b.Property("Price"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("Products"); 66 | }); 67 | 68 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.User", b => 69 | { 70 | b.Property("Id") 71 | .ValueGeneratedOnAdd(); 72 | 73 | b.Property("Name"); 74 | 75 | b.Property("Password") 76 | .IsRequired() 77 | .HasMaxLength(100); 78 | 79 | b.Property("RegistrationDate"); 80 | 81 | b.Property("Username") 82 | .IsRequired() 83 | .HasMaxLength(20); 84 | 85 | b.HasKey("Id"); 86 | 87 | b.ToTable("Users"); 88 | }); 89 | 90 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.Order", b => 91 | { 92 | b.HasOne("MyCoolWebServer.ByTheCakeApplication.Data.Models.User", "User") 93 | .WithMany("Orders") 94 | .HasForeignKey("UserId") 95 | .OnDelete(DeleteBehavior.Cascade); 96 | }); 97 | 98 | modelBuilder.Entity("MyCoolWebServer.ByTheCakeApplication.Data.Models.OrderProduct", b => 99 | { 100 | b.HasOne("MyCoolWebServer.ByTheCakeApplication.Data.Models.Order", "Order") 101 | .WithMany("Products") 102 | .HasForeignKey("OrderId") 103 | .OnDelete(DeleteBehavior.Cascade); 104 | 105 | b.HasOne("MyCoolWebServer.ByTheCakeApplication.Data.Models.Product", "Product") 106 | .WithMany("Orders") 107 | .HasForeignKey("ProductId") 108 | .OnDelete(DeleteBehavior.Cascade); 109 | }); 110 | #pragma warning restore 612, 618 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Models/Order.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Models 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | public class Order 7 | { 8 | public int Id { get; set; } 9 | 10 | public DateTime CreationDate { get; set; } 11 | 12 | public int UserId { get; set; } 13 | 14 | public User User { get; set; } 15 | 16 | public List Products { get; set; } = new List(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Models/OrderProduct.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Models 2 | { 3 | public class OrderProduct 4 | { 5 | public int OrderId { get; set; } 6 | 7 | public Order Order { get; set; } 8 | 9 | public int ProductId { get; set; } 10 | 11 | public Product Product { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Models/Product.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Models 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Collections.Generic; 5 | 6 | public class Product 7 | { 8 | public int Id { get; set; } 9 | 10 | [Required] 11 | [MaxLength(30)] 12 | public string Name { get; set; } 13 | 14 | public decimal Price { get; set; } 15 | 16 | [Required] 17 | [MaxLength(2000)] 18 | public string ImageUrl { get; set; } 19 | 20 | public List Orders { get; set; } = new List(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Data/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Data.Models 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | public class User 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | 13 | [Required] 14 | [MaxLength(20)] 15 | public string Username { get; set; } 16 | 17 | [Required] 18 | [MaxLength(100)] 19 | public string Password { get; set; } 20 | 21 | public DateTime RegistrationDate { get; set; } 22 | 23 | public List Orders { get; set; } = new List(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Account/login.html: -------------------------------------------------------------------------------- 1 | 

Login

2 | 3 |
4 | Username: 5 | 6 |
7 | Password: 8 | 9 |
10 | 11 |
12 | 13 |
14 | {{{error}}} 15 |
16 | 17 |
18 | 19 | Don't have an account? Create one here! -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Account/profile.html: -------------------------------------------------------------------------------- 1 | Back to home 2 | 3 |
4 | 5 |

My Profile

6 | 7 |

Username: {{{username}}}

8 |

Registered on: {{{registrationDate}}}

9 |

Orders count: {{{totalOrders}}}

10 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Account/register.html: -------------------------------------------------------------------------------- 1 | 

Register

2 | 3 |
4 | Username: 5 | 6 |
7 | Password: 8 | 9 |
10 | Confirm Password: 11 | 12 |
13 | 14 |
15 | 16 |
17 | {{{error}}} 18 |
19 | 20 |
21 | 22 | Already have an account? Login here! -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Home/about.html: -------------------------------------------------------------------------------- 1 | 

About us

2 |
3 |
By the Cake Ltd.
4 |
Company Name
5 |
{Your Name}
6 |
Owner
7 |
-------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Home/index.html: -------------------------------------------------------------------------------- 1 | 

By the Cake

2 |

Enjoy our awesome cakes

3 |
4 | 29 |

30 | Home 31 |

32 |
33 |

Our cakes

34 |

Cake is a form of sweet dessert that is typically baked. In its oldest forms, cakes were modifications of breads, but cakes now cover a wide range of preparations that can be simple or elaborate, and that share features with other desserts such as pastries, meringues, custards, and pies.

35 | 36 |
37 |
38 |

Our Stores

39 |

Our stores are located in 21 cities all over the world. Come and see what we have for you.

40 | 41 |
42 |
43 | City: Hong Kong             City: Salzburg
44 | Address: ChoCoLad 18        Address: SchokoLeiden 73  
45 | Phone: +78952804429	    Phone: +49241432990
46 |     
-------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Products/add.html: -------------------------------------------------------------------------------- 1 | Back to home 2 | 3 |
4 | Name: 5 | 6 |
7 | Price: 8 | 9 |
10 | Image URL: 11 | 12 |
13 | 14 |
15 | 16 |
17 | {{{error}}} 18 |
19 | 20 |
21 |

Name: {{{name}}}

22 |

Price: {{{price}}}

23 | {{{name}}} 24 |
-------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Products/details.html: -------------------------------------------------------------------------------- 1 | Back to home 2 | 3 |

{{{name}}}

4 | 5 |

6 | Price: ${{{price}}} 7 |

8 | 9 |

10 | {{{name}}} 11 |

-------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Products/search.html: -------------------------------------------------------------------------------- 1 | Back to home 2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 | Your Cart: {{{products}}} 10 |
11 | 12 |
13 | 14 | {{{results}}} -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Shopping/cart.html: -------------------------------------------------------------------------------- 1 | 

Your Cart

2 | 3 | 7 |
8 | 9 | {{{cartItems}}} 10 | 11 |
12 | Total Cost: ${{{totalCost}}} 13 |
14 | 15 |
16 | 17 |
-------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/Shopping/finish-order.html: -------------------------------------------------------------------------------- 1 | Back to home 2 | 3 |

Thank you for your order!

-------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Resources/layout.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | By the Cake 10 | 11 | 12 |
13 | 14 |
15 |
16 | {{{content}}} 17 |
18 |
19 | ©All Rights Reserved 20 |
21 | 22 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Services/IProductService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Services 2 | { 3 | using System.Collections.Generic; 4 | using ViewModels.Products; 5 | 6 | public interface IProductService 7 | { 8 | void Create(string name, decimal price, string imageUrl); 9 | 10 | IEnumerable All(string searchTerm = null); 11 | 12 | ProductDetailsViewModel Find(int id); 13 | 14 | bool Exists(int id); 15 | 16 | IEnumerable FindProductsInCart(IEnumerable ids); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Services/IShoppingService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Services 2 | { 3 | using System.Collections.Generic; 4 | 5 | public interface IShoppingService 6 | { 7 | void CreateOrder(int userId, IEnumerable productIds); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Services/IUserService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Services 2 | { 3 | using ViewModels.Account; 4 | 5 | public interface IUserService 6 | { 7 | bool Create(string username, string password); 8 | 9 | bool Find(string username, string password); 10 | 11 | ProfileViewModel Profile(string username); 12 | 13 | int? GetUserId(string username); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Services/ProductService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Services 2 | { 3 | using Data; 4 | using Data.Models; 5 | using ViewModels.Products; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | public class ProductService : IProductService 10 | { 11 | public void Create(string name, decimal price, string imageUrl) 12 | { 13 | using (var db = new ByTheCakeDbContext()) 14 | { 15 | var product = new Product 16 | { 17 | Name = name, 18 | Price = price, 19 | ImageUrl = imageUrl 20 | }; 21 | 22 | db.Add(product); 23 | db.SaveChanges(); 24 | } 25 | } 26 | 27 | public IEnumerable All(string searchTerm = null) 28 | { 29 | using (var db = new ByTheCakeDbContext()) 30 | { 31 | var resultsQuery = db.Products.AsQueryable(); 32 | 33 | if (!string.IsNullOrEmpty(searchTerm)) 34 | { 35 | resultsQuery = resultsQuery 36 | .Where(pr => pr.Name.ToLower().Contains(searchTerm.ToLower())); 37 | } 38 | 39 | return resultsQuery 40 | .Select(pr => new ProductListingViewModel 41 | { 42 | Id = pr.Id, 43 | Name = pr.Name, 44 | Price = pr.Price 45 | }) 46 | .ToList(); 47 | } 48 | } 49 | 50 | public ProductDetailsViewModel Find(int id) 51 | { 52 | using (var db = new ByTheCakeDbContext()) 53 | { 54 | return db.Products 55 | .Where(pr => pr.Id == id) 56 | .Select(pr => new ProductDetailsViewModel 57 | { 58 | Name = pr.Name, 59 | Price = pr.Price, 60 | ImageUrl = pr.ImageUrl 61 | }) 62 | .FirstOrDefault(); 63 | } 64 | } 65 | 66 | public bool Exists(int id) 67 | { 68 | using (var db = new ByTheCakeDbContext()) 69 | { 70 | return db.Products.Any(pr => pr.Id == id); 71 | } 72 | } 73 | 74 | public IEnumerable FindProductsInCart(IEnumerable ids) 75 | { 76 | using (var db = new ByTheCakeDbContext()) 77 | { 78 | return db.Products 79 | .Where(pr => ids.Contains(pr.Id)) 80 | .Select(pr => new ProductInCartViewModel 81 | { 82 | Name = pr.Name, 83 | Price = pr.Price 84 | }) 85 | .ToList(); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Services/ShoppingService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Services 2 | { 3 | using Data; 4 | using Data.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | public class ShoppingService : IShoppingService 10 | { 11 | public void CreateOrder(int userId, IEnumerable productIds) 12 | { 13 | using (var db = new ByTheCakeDbContext()) 14 | { 15 | var order = new Order 16 | { 17 | UserId = userId, 18 | CreationDate = DateTime.UtcNow, 19 | Products = productIds 20 | .Select(id => new OrderProduct 21 | { 22 | ProductId = id 23 | }) 24 | .ToList() 25 | }; 26 | 27 | db.Add(order); 28 | db.SaveChanges(); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.Services 2 | { 3 | using Data; 4 | using Data.Models; 5 | using System; 6 | using System.Linq; 7 | using ViewModels.Account; 8 | 9 | public class UserService : IUserService 10 | { 11 | public bool Create(string username, string password) 12 | { 13 | using (var db = new ByTheCakeDbContext()) 14 | { 15 | if (db.Users.Any(u => u.Username == username)) 16 | { 17 | return false; 18 | } 19 | 20 | var user = new User 21 | { 22 | Username = username, 23 | Password = password, 24 | RegistrationDate = DateTime.UtcNow 25 | }; 26 | 27 | db.Add(user); 28 | db.SaveChanges(); 29 | 30 | return true; 31 | } 32 | } 33 | 34 | public bool Find(string username, string password) 35 | { 36 | using (var db = new ByTheCakeDbContext()) 37 | { 38 | return db 39 | .Users 40 | .Any(u => u.Username == username && u.Password == password); 41 | } 42 | } 43 | 44 | public ProfileViewModel Profile(string username) 45 | { 46 | using (var db = new ByTheCakeDbContext()) 47 | { 48 | return db 49 | .Users 50 | .Where(u => u.Username == username) 51 | .Select(u => new ProfileViewModel 52 | { 53 | Username = u.Username, 54 | RegistrationDate = u.RegistrationDate, 55 | TotalOrders = u.Orders.Count() 56 | }) 57 | .FirstOrDefault(); 58 | } 59 | } 60 | 61 | public int? GetUserId(string username) 62 | { 63 | using (var db = new ByTheCakeDbContext()) 64 | { 65 | var id = db 66 | .Users 67 | .Where(u => u.Username == username) 68 | .Select(u => u.Id) 69 | .FirstOrDefault(); 70 | 71 | return id != 0 ? (int?)id : null; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Account 2 | { 3 | public class LoginViewModel 4 | { 5 | public string Username { get; set; } 6 | 7 | public string Password { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Account/ProfileViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Account 2 | { 3 | using System; 4 | 5 | public class ProfileViewModel 6 | { 7 | public string Username { get; set; } 8 | 9 | public DateTime RegistrationDate { get; set; } 10 | 11 | public int TotalOrders { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Account/RegisterUserViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Account 2 | { 3 | public class RegisterUserViewModel 4 | { 5 | public string Username { get; set; } 6 | 7 | public string Password { get; set; } 8 | 9 | public string ConfirmPassword { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Products/AddProductViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Products 2 | { 3 | public class AddProductViewModel 4 | { 5 | public string Name { get; set; } 6 | 7 | public decimal Price { get; set; } 8 | 9 | public string ImageUrl { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Products/ProductDetailsViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Products 2 | { 3 | public class ProductDetailsViewModel 4 | { 5 | public string Name { get; set; } 6 | 7 | public decimal Price { get; set; } 8 | 9 | public string ImageUrl { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Products/ProductInCartViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Products 2 | { 3 | public class ProductInCartViewModel 4 | { 5 | public string Name { get; set; } 6 | 7 | public decimal Price { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/Products/ProductListingViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels.Products 2 | { 3 | public class ProductListingViewModel 4 | { 5 | public int Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public decimal Price { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MyCoolWebServer/ByTheCakeApplication/ViewModels/ShoppingCart.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.ByTheCakeApplication.ViewModels 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class ShoppingCart 6 | { 7 | public const string SessionKey = "%^Current_Shopping_Cart^%"; 8 | 9 | public List ProductIds { get; private set; } = new List(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Common/Authentication.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Common 2 | { 3 | public class Authentication 4 | { 5 | public Authentication(bool isAuthenticated, bool isAdmin) 6 | { 7 | this.IsAuthenticated = isAuthenticated; 8 | this.IsAdmin = isAdmin; 9 | } 10 | 11 | public bool IsAuthenticated { get; private set; } 12 | 13 | public bool IsAdmin { get; private set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Common/ValidationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Common 2 | { 3 | public class ValidationConstants 4 | { 5 | public const string InvalidMinLengthErrorMessage = "{0} must be at least {1} symbols."; 6 | public const string InvalidMaxLengthErrorMessage = "{0} cannot be more than {1} symbols."; 7 | public const string ExactLengthErrorMessage = "{0} must be exactly {1} symbols."; 8 | 9 | public class Account 10 | { 11 | public const int EmailMaxLength = 30; 12 | public const int NameMinLength = 2; 13 | public const int NameMaxLength = 30; 14 | public const int PasswordMinLength = 6; 15 | public const int PasswordMaxLength = 50; 16 | } 17 | 18 | public class Game 19 | { 20 | public const int TitleMinLength = 3; 21 | public const int TitleMaxLength = 100; 22 | public const int VideoIdLength = 11; 23 | public const int DescriptionMinLength = 20; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Controllers 2 | { 3 | using Server.Http; 4 | using Server.Http.Contracts; 5 | using Services; 6 | using Services.Contracts; 7 | using ViewModels.Account; 8 | 9 | public class AccountController : BaseController 10 | { 11 | private const string RegisterView = @"account\register"; 12 | private const string LoginView = @"account\login"; 13 | 14 | private readonly IUserService users; 15 | 16 | public AccountController(IHttpRequest request) 17 | : base(request) 18 | { 19 | this.users = new UserService(); 20 | } 21 | 22 | public IHttpResponse Register() 23 | => this.FileViewResponse(RegisterView); 24 | 25 | public IHttpResponse Register(RegisterViewModel model) 26 | { 27 | if (!this.ValidateModel(model)) 28 | { 29 | return this.Register(); 30 | } 31 | 32 | var success = this.users 33 | .Create(model.Email, model.FullName, model.Password); 34 | 35 | if (!success) 36 | { 37 | this.ShowError("E-mail is taken."); 38 | return this.Register(); 39 | } 40 | else 41 | { 42 | this.LoginUser(model.Email); 43 | return this.RedirectResponse(HomePath); 44 | } 45 | } 46 | 47 | public IHttpResponse Login() 48 | => this.FileViewResponse(LoginView); 49 | 50 | public IHttpResponse Login(LoginViewModel model) 51 | { 52 | if (!this.ValidateModel(model)) 53 | { 54 | return this.Login(); 55 | } 56 | 57 | var success = this.users.Find(model.Email, model.Password); 58 | 59 | if (!success) 60 | { 61 | this.ShowError("Invalid user details."); 62 | 63 | return this.Login(); 64 | } 65 | else 66 | { 67 | this.LoginUser(model.Email); 68 | 69 | return this.RedirectResponse(HomePath); 70 | } 71 | } 72 | 73 | public IHttpResponse Logout() 74 | { 75 | this.Request.Session.Clear(); 76 | 77 | return this.RedirectResponse(HomePath); 78 | } 79 | 80 | private void LoginUser(string email) 81 | { 82 | this.Request.Session.Add(SessionStore.CurrentUserKey, email); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Controllers/AdminController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Controllers 2 | { 3 | using Server.Http.Contracts; 4 | using Services; 5 | using Services.Contracts; 6 | using System; 7 | using System.Linq; 8 | using ViewModels.Admin; 9 | 10 | public class AdminController : BaseController 11 | { 12 | private const string AddGameView = @"admin\add-game"; 13 | private const string ListGamesView = @"admin\list-games"; 14 | 15 | private readonly IGameService games; 16 | 17 | public AdminController(IHttpRequest request) 18 | : base(request) 19 | { 20 | this.games = new GameService(); 21 | } 22 | 23 | public IHttpResponse Add() 24 | { 25 | if (this.Authentication.IsAdmin) 26 | { 27 | return this.FileViewResponse(AddGameView); 28 | } 29 | else 30 | { 31 | return this.RedirectResponse(HomePath); 32 | } 33 | } 34 | 35 | public IHttpResponse Add(AdminAddGameViewModel model) 36 | { 37 | if (!this.Authentication.IsAdmin) 38 | { 39 | return this.RedirectResponse(HomePath); 40 | } 41 | 42 | if (!this.ValidateModel(model)) 43 | { 44 | return this.Add(); 45 | } 46 | 47 | this.games.Create( 48 | model.Title, 49 | model.Description, 50 | model.Image, 51 | model.Price, 52 | model.Size, 53 | model.VideoId, 54 | model.ReleaseDate.Value); 55 | 56 | return this.RedirectResponse("/admin/games/list"); 57 | } 58 | 59 | public IHttpResponse List() 60 | { 61 | if (!this.Authentication.IsAdmin) 62 | { 63 | return this.RedirectResponse(HomePath); 64 | } 65 | 66 | var result = this.games 67 | .All() 68 | .Select(g => $@" 69 | {g.Id} 70 | {g.Name} 71 | {g.Size:F2} GB 72 | {g.Price:F2} € 73 | 74 | Edit 75 | Delete 76 | 77 | "); 78 | 79 | var gamesAsHtml = string.Join(Environment.NewLine, result); 80 | 81 | this.ViewData["games"] = gamesAsHtml; 82 | 83 | return this.FileViewResponse(ListGamesView); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Controllers 2 | { 3 | using Common; 4 | using Infrastructure; 5 | using Server.Http; 6 | using Server.Http.Contracts; 7 | using Services; 8 | using Services.Contracts; 9 | 10 | public abstract class BaseController : Controller 11 | { 12 | protected const string HomePath = "/"; 13 | 14 | private readonly IUserService users; 15 | 16 | protected BaseController(IHttpRequest request) 17 | { 18 | this.Request = request; 19 | this.Authentication = new Authentication(false, false); 20 | 21 | this.users = new UserService(); 22 | 23 | this.ApplyAuthentication(); 24 | } 25 | 26 | protected IHttpRequest Request { get; private set; } 27 | 28 | protected Authentication Authentication { get; private set; } 29 | 30 | protected override string ApplicationDirectory => "GameStoreApplication"; 31 | 32 | private void ApplyAuthentication() 33 | { 34 | var anonymousDisplay = "flex"; 35 | var authDisplay = "none"; 36 | var adminDisplay = "none"; 37 | 38 | var authenticatedUserEmail = this.Request 39 | .Session 40 | .Get(SessionStore.CurrentUserKey); 41 | 42 | if (authenticatedUserEmail != null) 43 | { 44 | anonymousDisplay = "none"; 45 | authDisplay = "flex"; 46 | 47 | var isAdmin = this.users.IsAdmin(authenticatedUserEmail); 48 | 49 | if (isAdmin) 50 | { 51 | adminDisplay = "flex"; 52 | } 53 | 54 | this.Authentication = new Authentication(true, isAdmin); 55 | } 56 | 57 | this.ViewData["anonymousDisplay"] = anonymousDisplay; 58 | this.ViewData["authDisplay"] = authDisplay; 59 | this.ViewData["adminDisplay"] = adminDisplay; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Controllers 2 | { 3 | using Server.Http.Contracts; 4 | 5 | public class HomeController : BaseController 6 | { 7 | public HomeController(IHttpRequest request) 8 | : base(request) 9 | { 10 | } 11 | 12 | public IHttpResponse Index() 13 | { 14 | return this.FileViewResponse(@"home\index"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/GameStoreDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Data 2 | { 3 | using Microsoft.EntityFrameworkCore; 4 | using Models; 5 | 6 | public class GameStoreDbContext : DbContext 7 | { 8 | public DbSet Users { get; set; } 9 | 10 | public DbSet Games { get; set; } 11 | 12 | protected override void OnConfiguring(DbContextOptionsBuilder builder) 13 | { 14 | builder 15 | .UseSqlServer("Server=.;Database=GameStoreDb;Integrated Security=True;"); 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder builder) 19 | { 20 | builder 21 | .Entity() 22 | .HasKey(ug => new { ug.GameId, ug.UserId }); 23 | 24 | builder 25 | .Entity() 26 | .HasIndex(u => u.Email) 27 | .IsUnique(); 28 | 29 | builder 30 | .Entity() 31 | .HasMany(u => u.Games) 32 | .WithOne(ug => ug.User) 33 | .HasForeignKey(ug => ug.UserId); 34 | 35 | builder 36 | .Entity() 37 | .HasMany(g => g.Users) 38 | .WithOne(ug => ug.Game) 39 | .HasForeignKey(ug => ug.GameId); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/Migrations/20171013110432_GameStoreInitialDatabase.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using MyCoolWebServer.GameStoreApplication.Data; 7 | using System; 8 | 9 | namespace GameStoreApplication.Data.Migrations 10 | { 11 | [DbContext(typeof(GameStoreDbContext))] 12 | [Migration("20171013110432_GameStoreInitialDatabase")] 13 | partial class GameStoreInitialDatabase 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.Game", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("Description") 28 | .IsRequired(); 29 | 30 | b.Property("Image") 31 | .IsRequired(); 32 | 33 | b.Property("Price"); 34 | 35 | b.Property("ReleaseDate"); 36 | 37 | b.Property("Size"); 38 | 39 | b.Property("Title") 40 | .IsRequired() 41 | .HasMaxLength(100); 42 | 43 | b.Property("VideoId") 44 | .IsRequired() 45 | .HasMaxLength(11); 46 | 47 | b.HasKey("Id"); 48 | 49 | b.ToTable("Games"); 50 | }); 51 | 52 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.User", b => 53 | { 54 | b.Property("Id") 55 | .ValueGeneratedOnAdd(); 56 | 57 | b.Property("Email") 58 | .IsRequired() 59 | .HasMaxLength(30); 60 | 61 | b.Property("IsAdmin"); 62 | 63 | b.Property("Name") 64 | .HasMaxLength(30); 65 | 66 | b.Property("Password") 67 | .IsRequired() 68 | .HasMaxLength(50); 69 | 70 | b.HasKey("Id"); 71 | 72 | b.HasIndex("Email") 73 | .IsUnique(); 74 | 75 | b.ToTable("Users"); 76 | }); 77 | 78 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.UserGame", b => 79 | { 80 | b.Property("GameId"); 81 | 82 | b.Property("UserId"); 83 | 84 | b.HasKey("GameId", "UserId"); 85 | 86 | b.HasIndex("UserId"); 87 | 88 | b.ToTable("UserGame"); 89 | }); 90 | 91 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.UserGame", b => 92 | { 93 | b.HasOne("MyCoolWebServer.GameStoreApplication.Data.Models.Game", "Game") 94 | .WithMany("Users") 95 | .HasForeignKey("GameId") 96 | .OnDelete(DeleteBehavior.Cascade); 97 | 98 | b.HasOne("MyCoolWebServer.GameStoreApplication.Data.Models.User", "User") 99 | .WithMany("Games") 100 | .HasForeignKey("UserId") 101 | .OnDelete(DeleteBehavior.Cascade); 102 | }); 103 | #pragma warning restore 612, 618 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/Migrations/20171013110432_GameStoreInitialDatabase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using System; 4 | 5 | namespace GameStoreApplication.Data.Migrations 6 | { 7 | public partial class GameStoreInitialDatabase : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Games", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "int", nullable: false) 16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 17 | Description = table.Column(type: "nvarchar(max)", nullable: false), 18 | Image = table.Column(type: "nvarchar(max)", nullable: false), 19 | Price = table.Column(type: "decimal(18, 2)", nullable: false), 20 | ReleaseDate = table.Column(type: "datetime2", nullable: false), 21 | Size = table.Column(type: "float", nullable: false), 22 | Title = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 23 | VideoId = table.Column(type: "nvarchar(11)", maxLength: 11, nullable: false) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_Games", x => x.Id); 28 | }); 29 | 30 | migrationBuilder.CreateTable( 31 | name: "Users", 32 | columns: table => new 33 | { 34 | Id = table.Column(type: "int", nullable: false) 35 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 36 | Email = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), 37 | IsAdmin = table.Column(type: "bit", nullable: false), 38 | Name = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: true), 39 | Password = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) 40 | }, 41 | constraints: table => 42 | { 43 | table.PrimaryKey("PK_Users", x => x.Id); 44 | }); 45 | 46 | migrationBuilder.CreateTable( 47 | name: "UserGame", 48 | columns: table => new 49 | { 50 | GameId = table.Column(type: "int", nullable: false), 51 | UserId = table.Column(type: "int", nullable: false) 52 | }, 53 | constraints: table => 54 | { 55 | table.PrimaryKey("PK_UserGame", x => new { x.GameId, x.UserId }); 56 | table.ForeignKey( 57 | name: "FK_UserGame_Games_GameId", 58 | column: x => x.GameId, 59 | principalTable: "Games", 60 | principalColumn: "Id", 61 | onDelete: ReferentialAction.Cascade); 62 | table.ForeignKey( 63 | name: "FK_UserGame_Users_UserId", 64 | column: x => x.UserId, 65 | principalTable: "Users", 66 | principalColumn: "Id", 67 | onDelete: ReferentialAction.Cascade); 68 | }); 69 | 70 | migrationBuilder.CreateIndex( 71 | name: "IX_UserGame_UserId", 72 | table: "UserGame", 73 | column: "UserId"); 74 | 75 | migrationBuilder.CreateIndex( 76 | name: "IX_Users_Email", 77 | table: "Users", 78 | column: "Email", 79 | unique: true); 80 | } 81 | 82 | protected override void Down(MigrationBuilder migrationBuilder) 83 | { 84 | migrationBuilder.DropTable( 85 | name: "UserGame"); 86 | 87 | migrationBuilder.DropTable( 88 | name: "Games"); 89 | 90 | migrationBuilder.DropTable( 91 | name: "Users"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/Migrations/GameStoreDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using MyCoolWebServer.GameStoreApplication.Data; 6 | using System; 7 | 8 | namespace GameStoreApplication.Data.Migrations 9 | { 10 | [DbContext(typeof(GameStoreDbContext))] 11 | partial class GameStoreDbContextModelSnapshot : ModelSnapshot 12 | { 13 | protected override void BuildModel(ModelBuilder modelBuilder) 14 | { 15 | #pragma warning disable 612, 618 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 18 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 19 | 20 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.Game", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd(); 24 | 25 | b.Property("Description") 26 | .IsRequired(); 27 | 28 | b.Property("Image") 29 | .IsRequired(); 30 | 31 | b.Property("Price"); 32 | 33 | b.Property("ReleaseDate"); 34 | 35 | b.Property("Size"); 36 | 37 | b.Property("Title") 38 | .IsRequired() 39 | .HasMaxLength(100); 40 | 41 | b.Property("VideoId") 42 | .IsRequired() 43 | .HasMaxLength(11); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.ToTable("Games"); 48 | }); 49 | 50 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.User", b => 51 | { 52 | b.Property("Id") 53 | .ValueGeneratedOnAdd(); 54 | 55 | b.Property("Email") 56 | .IsRequired() 57 | .HasMaxLength(30); 58 | 59 | b.Property("IsAdmin"); 60 | 61 | b.Property("Name") 62 | .HasMaxLength(30); 63 | 64 | b.Property("Password") 65 | .IsRequired() 66 | .HasMaxLength(50); 67 | 68 | b.HasKey("Id"); 69 | 70 | b.HasIndex("Email") 71 | .IsUnique(); 72 | 73 | b.ToTable("Users"); 74 | }); 75 | 76 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.UserGame", b => 77 | { 78 | b.Property("GameId"); 79 | 80 | b.Property("UserId"); 81 | 82 | b.HasKey("GameId", "UserId"); 83 | 84 | b.HasIndex("UserId"); 85 | 86 | b.ToTable("UserGame"); 87 | }); 88 | 89 | modelBuilder.Entity("MyCoolWebServer.GameStoreApplication.Data.Models.UserGame", b => 90 | { 91 | b.HasOne("MyCoolWebServer.GameStoreApplication.Data.Models.Game", "Game") 92 | .WithMany("Users") 93 | .HasForeignKey("GameId") 94 | .OnDelete(DeleteBehavior.Cascade); 95 | 96 | b.HasOne("MyCoolWebServer.GameStoreApplication.Data.Models.User", "User") 97 | .WithMany("Games") 98 | .HasForeignKey("UserId") 99 | .OnDelete(DeleteBehavior.Cascade); 100 | }); 101 | #pragma warning restore 612, 618 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/Models/Game.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Data.Models 2 | { 3 | using Common; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel.DataAnnotations; 7 | 8 | public class Game 9 | { 10 | public int Id { get; set; } 11 | 12 | [Required] 13 | [MinLength(ValidationConstants.Game.TitleMinLength)] 14 | [MaxLength(ValidationConstants.Game.TitleMaxLength)] 15 | public string Title { get; set; } 16 | 17 | [Required] 18 | [MinLength(ValidationConstants.Game.VideoIdLength)] 19 | [MaxLength(ValidationConstants.Game.VideoIdLength)] 20 | public string VideoId { get; set; } 21 | 22 | [Required] 23 | public string Image { get; set; } 24 | 25 | // In GB 26 | public double Size { get; set; } 27 | 28 | public decimal Price { get; set; } 29 | 30 | [Required] 31 | [MinLength(ValidationConstants.Game.DescriptionMinLength)] 32 | public string Description { get; set; } 33 | 34 | public DateTime ReleaseDate { get; set; } 35 | 36 | public List Users { get; set; } = new List(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Data.Models 2 | { 3 | using Common; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | public class User 8 | { 9 | public int Id { get; set; } 10 | 11 | [MinLength(ValidationConstants.Account.NameMinLength)] 12 | [MaxLength(ValidationConstants.Account.NameMaxLength)] 13 | public string Name { get; set; } 14 | 15 | [Required] 16 | [MaxLength(ValidationConstants.Account.EmailMaxLength)] 17 | public string Email { get; set; } 18 | 19 | [Required] 20 | [MinLength(ValidationConstants.Account.PasswordMinLength)] 21 | [MaxLength(ValidationConstants.Account.PasswordMaxLength)] 22 | public string Password { get; set; } 23 | 24 | public bool IsAdmin { get; set; } 25 | 26 | public List Games { get; set; } = new List(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Data/Models/UserGame.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Data.Models 2 | { 3 | public class UserGame 4 | { 5 | public int UserId { get; set; } 6 | 7 | public User User { get; set; } 8 | 9 | public int GameId { get; set; } 10 | 11 | public Game Game { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/GameStoreApp.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication 2 | { 3 | using Controllers; 4 | using Data; 5 | using Microsoft.EntityFrameworkCore; 6 | using Server.Contracts; 7 | using Server.Routing.Contracts; 8 | using System; 9 | using System.Globalization; 10 | using ViewModels.Account; 11 | using ViewModels.Admin; 12 | 13 | public class GameStoreApp : IApplication 14 | { 15 | public void InitializeDatabase() 16 | { 17 | using (var db = new GameStoreDbContext()) 18 | { 19 | db.Database.Migrate(); 20 | } 21 | } 22 | 23 | public void Configure(IAppRouteConfig appRouteConfig) 24 | { 25 | appRouteConfig.AnonymousPaths.Add("/"); 26 | appRouteConfig.AnonymousPaths.Add("/account/register"); 27 | appRouteConfig.AnonymousPaths.Add("/account/login"); 28 | 29 | appRouteConfig 30 | .Get("/", req => new HomeController(req).Index()); 31 | 32 | appRouteConfig 33 | .Get( 34 | "/account/register", 35 | req => new AccountController(req).Register()); 36 | 37 | appRouteConfig 38 | .Post( 39 | "/account/register", 40 | req => new AccountController(req).Register( 41 | new RegisterViewModel 42 | { 43 | Email = req.FormData["email"], 44 | FullName = req.FormData["full-name"], 45 | Password = req.FormData["password"], 46 | ConfirmPassword = req.FormData["confirm-password"] 47 | })); 48 | 49 | appRouteConfig 50 | .Get( 51 | "/account/login", 52 | req => new AccountController(req).Login()); 53 | 54 | appRouteConfig 55 | .Post( 56 | "/account/login", 57 | req => new AccountController(req).Login( 58 | new LoginViewModel 59 | { 60 | Email = req.FormData["email"], 61 | Password = req.FormData["password"] 62 | })); 63 | 64 | appRouteConfig 65 | .Get( 66 | "/account/logout", 67 | req => new AccountController(req).Logout()); 68 | 69 | appRouteConfig 70 | .Get( 71 | "/admin/games/add", 72 | req => new AdminController(req).Add()); 73 | 74 | appRouteConfig 75 | .Post( 76 | "/admin/games/add", 77 | req => new AdminController(req).Add( 78 | new AdminAddGameViewModel 79 | { 80 | Title = req.FormData["title"], 81 | Description = req.FormData["description"], 82 | Image = req.FormData["thumbnail"], 83 | Price = decimal.Parse(req.FormData["price"]), 84 | Size = double.Parse(req.FormData["size"]), 85 | VideoId = req.FormData["video-id"], 86 | ReleaseDate = DateTime.ParseExact( 87 | req.FormData["release-date"], 88 | "yyyy-MM-dd", 89 | CultureInfo.InvariantCulture) 90 | })); 91 | 92 | appRouteConfig 93 | .Get( 94 | "/admin/games/list", 95 | req => new AdminController(req).List()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Resources/Account/login.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |

Login

6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 | 19 |
20 |
21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Resources/Account/register.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |

Register

6 |
7 | 8 |
9 | 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 |
-------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Resources/Admin/add-game.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |

Add Game

9 |
10 |
11 |
12 | 13 | 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 | GB 42 |
43 |
44 | 45 |
46 | 47 |
48 | https://www.youtube.com/watch?v= 49 | 50 |
51 |
52 | 53 |
54 | 55 | 56 |
57 | 58 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
-------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Resources/Admin/list-games.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |

All Games – 

5 | + Add 6 | Game 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{{games}}} 20 | 21 |
#NameSizePriceActions
22 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Resources/Home/index.html: -------------------------------------------------------------------------------- 1 | Welcome -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Resources/layout.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Game Store 7 | 8 | 9 | 10 |
11 | 53 |
54 | 55 |
56 |
57 | × 58 | {{{error}}} 59 |
60 |
61 | 62 | {{{content}}} 63 | 64 |
65 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Services/Contracts/IGameService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Services.Contracts 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using ViewModels.Admin; 6 | 7 | public interface IGameService 8 | { 9 | void Create( 10 | string title, 11 | string description, 12 | string image, 13 | decimal price, 14 | double size, 15 | string videoId, 16 | DateTime releaseDate); 17 | 18 | IEnumerable All(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Services/Contracts/IUserService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Services.Contracts 2 | { 3 | public interface IUserService 4 | { 5 | bool Create(string email, string name, string password); 6 | 7 | bool Find(string email, string password); 8 | 9 | bool IsAdmin(string email); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Services/GameService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Services 2 | { 3 | using Contracts; 4 | using Data; 5 | using Data.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using ViewModels.Admin; 10 | 11 | public class GameService : IGameService 12 | { 13 | public void Create( 14 | string title, 15 | string description, 16 | string image, 17 | decimal price, 18 | double size, 19 | string videoId, 20 | DateTime releaseDate) 21 | { 22 | using (var db = new GameStoreDbContext()) 23 | { 24 | var game = new Game 25 | { 26 | Title = title, 27 | Description = description, 28 | Image = image, 29 | Price = price, 30 | Size = size, 31 | VideoId = videoId, 32 | ReleaseDate = releaseDate 33 | }; 34 | 35 | db.Add(game); 36 | db.SaveChanges(); 37 | } 38 | } 39 | 40 | public IEnumerable All() 41 | { 42 | using (var db = new GameStoreDbContext()) 43 | { 44 | return db 45 | .Games 46 | .Select(g => new AdminListGameViewModel 47 | { 48 | Id = g.Id, 49 | Name = g.Title, 50 | Price = g.Price, 51 | Size = g.Size 52 | }) 53 | .ToList(); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Services 2 | { 3 | using Contracts; 4 | using Data; 5 | using Data.Models; 6 | using System.Linq; 7 | 8 | public class UserService : IUserService 9 | { 10 | public bool Create(string email, string name, string password) 11 | { 12 | using (var db = new GameStoreDbContext()) 13 | { 14 | if (db.Users.Any(u => u.Email == email)) 15 | { 16 | return false; 17 | } 18 | 19 | var isAdmin = !db.Users.Any(); 20 | 21 | var user = new User 22 | { 23 | Email = email, 24 | Name = name, 25 | Password = password, 26 | IsAdmin = isAdmin 27 | }; 28 | 29 | db.Add(user); 30 | db.SaveChanges(); 31 | } 32 | 33 | return true; 34 | } 35 | 36 | public bool Find(string email, string password) 37 | { 38 | using (var db = new GameStoreDbContext()) 39 | { 40 | return db.Users.Any(u => u.Email == email && u.Password == password); 41 | } 42 | } 43 | 44 | public bool IsAdmin(string email) 45 | { 46 | using (var db = new GameStoreDbContext()) 47 | { 48 | return db.Users.Any(u => u.Email == email && u.IsAdmin); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/Utilities/PasswordAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.Utilities 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | 6 | public class PasswordAttribute : ValidationAttribute 7 | { 8 | public PasswordAttribute() 9 | { 10 | this.ErrorMessage = "Password should be at least 6 symbols long, should contain at least 1 uppercase letter, 1 lowercase letter and 1 digit."; 11 | } 12 | 13 | public override bool IsValid(object value) 14 | { 15 | var password = value as string; 16 | if (password == null) 17 | { 18 | return true; 19 | } 20 | 21 | return password.Any(s => char.IsUpper(s)) 22 | && password.Any(s => char.IsLower(s)) 23 | && password.Any(s => char.IsDigit(s)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/ViewModels/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.ViewModels.Account 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | public class LoginViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | 11 | [Required] 12 | public string Password { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/ViewModels/Account/RegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.ViewModels.Account 2 | { 3 | using Common; 4 | using System.ComponentModel.DataAnnotations; 5 | using Utilities; 6 | 7 | public class RegisterViewModel 8 | { 9 | [Display(Name = "E-mail")] 10 | [Required] 11 | [MaxLength( 12 | ValidationConstants.Account.EmailMaxLength, 13 | ErrorMessage = ValidationConstants.InvalidMaxLengthErrorMessage)] 14 | [EmailAddress] 15 | public string Email { get; set; } 16 | 17 | [Display(Name = "Full Name")] 18 | [MinLength( 19 | ValidationConstants.Account.NameMinLength, 20 | ErrorMessage = ValidationConstants.InvalidMinLengthErrorMessage)] 21 | [MaxLength( 22 | ValidationConstants.Account.NameMaxLength, 23 | ErrorMessage = ValidationConstants.InvalidMaxLengthErrorMessage)] 24 | public string FullName { get; set; } 25 | 26 | [Required] 27 | [MinLength( 28 | ValidationConstants.Account.PasswordMinLength, 29 | ErrorMessage = ValidationConstants.InvalidMinLengthErrorMessage)] 30 | [MaxLength( 31 | ValidationConstants.Account.PasswordMaxLength, 32 | ErrorMessage = ValidationConstants.InvalidMaxLengthErrorMessage)] 33 | [Password] 34 | public string Password { get; set; } 35 | 36 | [Display(Name = "Confirm Password")] 37 | [Compare(nameof(Password))] 38 | public string ConfirmPassword { get; set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/ViewModels/Admin/AdminAddGameViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.ViewModels.Admin 2 | { 3 | using Common; 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | public class AdminAddGameViewModel 8 | { 9 | [Required] 10 | [MinLength( 11 | ValidationConstants.Game.TitleMinLength, 12 | ErrorMessage = ValidationConstants.InvalidMinLengthErrorMessage)] 13 | [MaxLength( 14 | ValidationConstants.Game.TitleMaxLength, 15 | ErrorMessage = ValidationConstants.InvalidMaxLengthErrorMessage)] 16 | public string Title { get; set; } 17 | 18 | [Display(Name = "YouTube Video URL")] 19 | [Required] 20 | [MinLength( 21 | ValidationConstants.Game.VideoIdLength, 22 | ErrorMessage = ValidationConstants.ExactLengthErrorMessage)] 23 | [MaxLength( 24 | ValidationConstants.Game.VideoIdLength, 25 | ErrorMessage = ValidationConstants.ExactLengthErrorMessage)] 26 | public string VideoId { get; set; } 27 | 28 | [Required] 29 | public string Image { get; set; } 30 | 31 | // In GB 32 | public double Size { get; set; } 33 | 34 | public decimal Price { get; set; } 35 | 36 | [Required] 37 | [MinLength( 38 | ValidationConstants.Game.DescriptionMinLength, 39 | ErrorMessage = ValidationConstants.InvalidMinLengthErrorMessage)] 40 | public string Description { get; set; } 41 | 42 | [Display(Name = "Release Date")] 43 | [Required] 44 | public DateTime? ReleaseDate { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MyCoolWebServer/GameStoreApplication/ViewModels/Admin/AdminListGameViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.GameStoreApplication.ViewModels.Admin 2 | { 3 | public class AdminListGameViewModel 4 | { 5 | public int Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public decimal Price { get; set; } 10 | 11 | public double Size { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/Infrastructure/Controller.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Infrastructure 2 | { 3 | using Server.Enums; 4 | using Server.Http.Contracts; 5 | using Server.Http.Response; 6 | using System.Collections.Generic; 7 | using System.ComponentModel.DataAnnotations; 8 | using System.IO; 9 | using System.Linq; 10 | 11 | public abstract class Controller 12 | { 13 | public const string DefaultPath = @"{0}\Resources\{1}.html"; 14 | public const string ContentPlaceholder = "{{{content}}}"; 15 | 16 | protected Controller() 17 | { 18 | this.ViewData = new Dictionary 19 | { 20 | ["anonymousDisplay"] = "none", 21 | ["authDisplay"] = "flex", 22 | ["showError"] = "none" 23 | }; 24 | } 25 | 26 | protected abstract string ApplicationDirectory { get; } 27 | 28 | protected IDictionary ViewData { get; private set; } 29 | 30 | protected IHttpResponse FileViewResponse(string fileName) 31 | { 32 | var result = this.ProcessFileHtml(fileName); 33 | 34 | if (this.ViewData.Any()) 35 | { 36 | foreach (var value in this.ViewData) 37 | { 38 | result = result.Replace($"{{{{{{{value.Key}}}}}}}", value.Value); 39 | } 40 | } 41 | 42 | return new ViewResponse(HttpStatusCode.Ok, new FileView(result)); 43 | } 44 | 45 | protected IHttpResponse RedirectResponse(string route) 46 | => new RedirectResponse(route); 47 | 48 | protected void ShowError(string errorMessage) 49 | { 50 | this.ViewData["showError"] = "block"; 51 | this.ViewData["error"] = errorMessage; 52 | } 53 | 54 | protected bool ValidateModel(object model) 55 | { 56 | var context = new ValidationContext(model); 57 | var results = new List(); 58 | 59 | if (Validator.TryValidateObject(model, context, results, true) == false) 60 | { 61 | foreach (var result in results) 62 | { 63 | if (result != ValidationResult.Success) 64 | { 65 | this.ShowError(result.ErrorMessage); 66 | return false; 67 | } 68 | } 69 | } 70 | 71 | return true; 72 | } 73 | 74 | private string ProcessFileHtml(string fileName) 75 | { 76 | var layoutHtml = File.ReadAllText(string.Format(DefaultPath, this.ApplicationDirectory, "layout")); 77 | 78 | var fileHtml = File 79 | .ReadAllText(string.Format(DefaultPath, this.ApplicationDirectory, fileName)); 80 | 81 | var result = layoutHtml.Replace(ContentPlaceholder, fileHtml); 82 | 83 | return result; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MyCoolWebServer/Infrastructure/FileView.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Infrastructure 2 | { 3 | using Server.Contracts; 4 | 5 | public class FileView : IView 6 | { 7 | private readonly string htmlFile; 8 | 9 | public FileView(string htmlFile) 10 | { 11 | this.htmlFile = htmlFile; 12 | } 13 | 14 | public string View() => this.htmlFile; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MyCoolWebServer/Launcher.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer 2 | { 3 | using GameStoreApplication; 4 | using Server; 5 | using Server.Contracts; 6 | using Server.Routing; 7 | 8 | public class Launcher : IRunnable 9 | { 10 | public static void Main() 11 | { 12 | new Launcher().Run(); 13 | } 14 | 15 | public void Run() 16 | { 17 | var mainApplication = new GameStoreApp(); 18 | mainApplication.InitializeDatabase(); 19 | 20 | var appRouteConfig = new AppRouteConfig(); 21 | mainApplication.Configure(appRouteConfig); 22 | 23 | var webServer = new WebServer(1337, appRouteConfig); 24 | 25 | webServer.Run(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MyCoolWebServer/MyCoolWebServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Common/CoreValidator.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Common 2 | { 3 | using System; 4 | 5 | public static class CoreValidator 6 | { 7 | public static void ThrowIfNull(object obj, string name) 8 | { 9 | if (obj == null) 10 | { 11 | throw new ArgumentNullException(name); 12 | } 13 | } 14 | 15 | public static void ThrowIfNullOrEmpty(string text, string name) 16 | { 17 | if (string.IsNullOrEmpty(text)) 18 | { 19 | throw new ArgumentException($"{name} cannot be null or empty.", name); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Common/InternalServerErrorView.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Common 2 | { 3 | using Contracts; 4 | using System; 5 | 6 | public class InternalServerErrorView : IView 7 | { 8 | private readonly Exception exception; 9 | 10 | public InternalServerErrorView(Exception exception) 11 | { 12 | this.exception = exception; 13 | } 14 | 15 | public string View() 16 | { 17 | return $"

{this.exception.Message}

{this.exception.StackTrace}

"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Common/NotFoundView.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Common 2 | { 3 | using Contracts; 4 | 5 | public class NotFoundView : IView 6 | { 7 | public string View() 8 | { 9 | return "

404 This page does not exist :/

"; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/ConnectionHandler.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server 2 | { 3 | using Common; 4 | using Handlers; 5 | using Http; 6 | using Http.Contracts; 7 | using Routing.Contracts; 8 | using System; 9 | using System.Net.Sockets; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | public class ConnectionHandler 14 | { 15 | private readonly Socket client; 16 | 17 | private readonly IServerRouteConfig serverRouteConfig; 18 | 19 | public ConnectionHandler(Socket client, IServerRouteConfig serverRouteConfig) 20 | { 21 | CoreValidator.ThrowIfNull(client, nameof(client)); 22 | CoreValidator.ThrowIfNull(serverRouteConfig, nameof(serverRouteConfig)); 23 | 24 | this.client = client; 25 | this.serverRouteConfig = serverRouteConfig; 26 | } 27 | 28 | public async Task ProcessRequestAsync() 29 | { 30 | var httpRequest = await this.ReadRequest(); 31 | 32 | if (httpRequest != null) 33 | { 34 | var httpContext = new HttpContext(httpRequest); 35 | 36 | var httpResponse = new HttpHandler(this.serverRouteConfig).Handle(httpContext); 37 | 38 | var responseBytes = Encoding.UTF8.GetBytes(httpResponse.ToString()); 39 | 40 | var byteSegments = new ArraySegment(responseBytes); 41 | 42 | await this.client.SendAsync(byteSegments, SocketFlags.None); 43 | 44 | Console.WriteLine($"-----REQUEST-----"); 45 | Console.WriteLine(httpRequest); 46 | Console.WriteLine($"-----RESPONSE-----"); 47 | Console.WriteLine(httpResponse); 48 | Console.WriteLine(); 49 | } 50 | 51 | this.client.Shutdown(SocketShutdown.Both); 52 | } 53 | 54 | private async Task ReadRequest() 55 | { 56 | var result = new StringBuilder(); 57 | 58 | var data = new ArraySegment(new byte[1024]); 59 | 60 | while (true) 61 | { 62 | int numberOfBytesRead = await this.client.ReceiveAsync(data.Array, SocketFlags.None); 63 | 64 | if (numberOfBytesRead == 0) 65 | { 66 | break; 67 | } 68 | 69 | var bytesAsString = Encoding.UTF8.GetString(data.Array, 0, numberOfBytesRead); 70 | 71 | result.Append(bytesAsString); 72 | 73 | if (numberOfBytesRead < 1023) 74 | { 75 | break; 76 | } 77 | } 78 | 79 | if (result.Length == 0) 80 | { 81 | return null; 82 | } 83 | 84 | return new HttpRequest(result.ToString()); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Contracts/IApplication.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Contracts 2 | { 3 | using Routing.Contracts; 4 | 5 | public interface IApplication 6 | { 7 | void Configure(IAppRouteConfig appRouteConfig); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Contracts/IRunnable.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Contracts 2 | { 3 | public interface IRunnable 4 | { 5 | void Run(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Contracts/IView.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Contracts 2 | { 3 | public interface IView 4 | { 5 | string View(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Enums/HttpRequestMethod.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Enums 2 | { 3 | public enum HttpRequestMethod 4 | { 5 | Get, 6 | Post 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Enums/HttpStatusCode.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Enums 2 | { 3 | public enum HttpStatusCode 4 | { 5 | Ok = 200, 6 | MovedPermanently = 301, 7 | Found = 302, 8 | MovedTemporary = 303, 9 | BadRequest = 400, 10 | NotAuthorized = 401, 11 | NotFound = 404, 12 | InternalServerError = 500 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Exceptions/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Exceptions 2 | { 3 | using System; 4 | 5 | public class BadRequestException : Exception 6 | { 7 | private const string InvalidRequestMessage = "Request is not valid."; 8 | 9 | public static object ThrowFromInvalidRequest() 10 | => throw new BadRequestException(InvalidRequestMessage); 11 | 12 | public BadRequestException(string message) 13 | : base(message) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Exceptions/InvalidResponseException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Exceptions 2 | { 3 | using System; 4 | 5 | public class InvalidResponseException : Exception 6 | { 7 | public InvalidResponseException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Handlers/Contracts/IRequestHandler.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Handlers.Contracts 2 | { 3 | using Http.Contracts; 4 | 5 | public interface IRequestHandler 6 | { 7 | IHttpResponse Handle(IHttpContext context); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Handlers/HttpHandler.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Handlers 2 | { 3 | using Common; 4 | using Contracts; 5 | using Http.Contracts; 6 | using Http.Response; 7 | using Routing.Contracts; 8 | using Server.Http; 9 | using System; 10 | using System.Linq; 11 | using System.Text.RegularExpressions; 12 | 13 | public class HttpHandler : IRequestHandler 14 | { 15 | private readonly IServerRouteConfig serverRouteConfig; 16 | 17 | public HttpHandler(IServerRouteConfig routeConfig) 18 | { 19 | CoreValidator.ThrowIfNull(routeConfig, nameof(routeConfig)); 20 | 21 | this.serverRouteConfig = routeConfig; 22 | } 23 | 24 | public IHttpResponse Handle(IHttpContext context) 25 | { 26 | try 27 | { 28 | // Check if user is authenticated 29 | var anonymousPaths = this.serverRouteConfig.AnonymousPaths; 30 | 31 | if (!anonymousPaths.Contains(context.Request.Path) && 32 | (context.Request.Session == null || !context.Request.Session.Contains(SessionStore.CurrentUserKey))) 33 | { 34 | return new RedirectResponse(anonymousPaths.First()); 35 | } 36 | 37 | var requestMethod = context.Request.Method; 38 | var requestPath = context.Request.Path; 39 | var registeredRoutes = this.serverRouteConfig.Routes[requestMethod]; 40 | 41 | foreach (var registeredRoute in registeredRoutes) 42 | { 43 | var routePattern = registeredRoute.Key; 44 | var routingContext = registeredRoute.Value; 45 | 46 | var routeRegex = new Regex(routePattern); 47 | var match = routeRegex.Match(requestPath); 48 | 49 | if (!match.Success) 50 | { 51 | continue; 52 | } 53 | 54 | var parameters = routingContext.Parameters; 55 | 56 | foreach (var parameter in parameters) 57 | { 58 | var parameterValue = match.Groups[parameter].Value; 59 | context.Request.AddUrlParameter(parameter, parameterValue); 60 | } 61 | 62 | return routingContext.Handler.Handle(context); 63 | } 64 | } 65 | catch (Exception ex) 66 | { 67 | return new InternalServerErrorResponse(ex); 68 | } 69 | 70 | return new NotFoundResponse(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Handlers/RequestHandler.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Handlers 2 | { 3 | using Common; 4 | using Contracts; 5 | using Http; 6 | using Http.Contracts; 7 | using System; 8 | 9 | public class RequestHandler : IRequestHandler 10 | { 11 | private readonly Func handlingFunc; 12 | 13 | public RequestHandler(Func handlingFunc) 14 | { 15 | CoreValidator.ThrowIfNull(handlingFunc, nameof(handlingFunc)); 16 | 17 | this.handlingFunc = handlingFunc; 18 | } 19 | 20 | public IHttpResponse Handle(IHttpContext context) 21 | { 22 | string sessionIdToSend = null; 23 | 24 | if (!context.Request.Cookies.ContainsKey(SessionStore.SessionCookieKey)) 25 | { 26 | var sessionId = Guid.NewGuid().ToString(); 27 | 28 | context.Request.Session = SessionStore.Get(sessionId); 29 | 30 | sessionIdToSend = sessionId; 31 | } 32 | 33 | var response = this.handlingFunc(context.Request); 34 | 35 | if (sessionIdToSend != null) 36 | { 37 | response.Headers.Add( 38 | HttpHeader.SetCookie, 39 | $"{SessionStore.SessionCookieKey}={sessionIdToSend}; HttpOnly; path=/"); 40 | } 41 | 42 | if (!response.Headers.ContainsKey(HttpHeader.ContentType)) 43 | { 44 | response.Headers.Add(HttpHeader.ContentType, "text/plain"); 45 | } 46 | 47 | foreach (var cookie in response.Cookies) 48 | { 49 | response.Headers.Add(HttpHeader.SetCookie, cookie.ToString()); 50 | } 51 | 52 | return response; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Contracts/IHttpContext.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Contracts 2 | { 3 | public interface IHttpContext 4 | { 5 | IHttpRequest Request { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Contracts/IHttpCookieCollection.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Contracts 2 | { 3 | using System.Collections.Generic; 4 | 5 | public interface IHttpCookieCollection : IEnumerable 6 | { 7 | void Add(HttpCookie cookie); 8 | 9 | void Add(string key, string value); 10 | 11 | bool ContainsKey(string key); 12 | 13 | HttpCookie Get(string key); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Contracts/IHttpHeaderCollection.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Contracts 2 | { 3 | using System.Collections.Generic; 4 | 5 | public interface IHttpHeaderCollection : IEnumerable> 6 | { 7 | void Add(HttpHeader header); 8 | 9 | void Add(string key, string value); 10 | 11 | bool ContainsKey(string key); 12 | 13 | ICollection Get(string key); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Contracts/IHttpRequest.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Contracts 2 | { 3 | using Enums; 4 | using System.Collections.Generic; 5 | 6 | public interface IHttpRequest 7 | { 8 | IDictionary FormData { get; } 9 | 10 | IHttpHeaderCollection Headers { get; } 11 | 12 | IHttpCookieCollection Cookies { get; } 13 | 14 | string Path { get; } 15 | 16 | HttpRequestMethod Method { get; } 17 | 18 | string Url { get; } 19 | 20 | IDictionary UrlParameters { get; } 21 | 22 | IHttpSession Session { get; set; } 23 | 24 | void AddUrlParameter(string key, string value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Contracts/IHttpResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Contracts 2 | { 3 | using Enums; 4 | 5 | public interface IHttpResponse 6 | { 7 | HttpStatusCode StatusCode { get; } 8 | 9 | IHttpHeaderCollection Headers { get; } 10 | 11 | IHttpCookieCollection Cookies { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Contracts/IHttpSession.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Contracts 2 | { 3 | public interface IHttpSession 4 | { 5 | string Id { get; } 6 | 7 | object Get(string key); 8 | 9 | T Get(string key); 10 | 11 | bool Contains(string key); 12 | 13 | void Add(string key, object value); 14 | 15 | void Clear(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpContext.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using Common; 4 | using Contracts; 5 | 6 | public class HttpContext : IHttpContext 7 | { 8 | private readonly IHttpRequest request; 9 | 10 | public HttpContext(IHttpRequest request) 11 | { 12 | CoreValidator.ThrowIfNull(request, nameof(request)); 13 | 14 | this.request = request; 15 | } 16 | 17 | public IHttpRequest Request => this.request; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpCookie.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using Server.Common; 4 | using System; 5 | 6 | public class HttpCookie 7 | { 8 | // expires is in days 9 | public HttpCookie(string key, string value, int expires = 3) 10 | { 11 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 12 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value)); 13 | 14 | this.Key = key; 15 | this.Value = value; 16 | 17 | this.Expires = DateTime.UtcNow.AddDays(expires); 18 | } 19 | 20 | public HttpCookie(string key, string value, bool isNew, int expires = 3) 21 | : this(key, value, expires) 22 | { 23 | this.IsNew = isNew; 24 | } 25 | 26 | public string Key { get; private set; } 27 | 28 | public string Value { get; private set; } 29 | 30 | public DateTime Expires { get; private set; } 31 | 32 | public bool IsNew { get; private set; } = true; 33 | 34 | public override string ToString() 35 | => $"{this.Key}={this.Value}; Expires={this.Expires.ToLongTimeString()}"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpCookieCollection.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using Common; 4 | using Contracts; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections; 8 | 9 | public class HttpCookieCollection : IHttpCookieCollection 10 | { 11 | private readonly IDictionary cookies; 12 | 13 | public HttpCookieCollection() 14 | { 15 | this.cookies = new Dictionary(); 16 | } 17 | 18 | public void Add(HttpCookie cookie) 19 | { 20 | CoreValidator.ThrowIfNull(cookie, nameof(cookie)); 21 | 22 | this.cookies[cookie.Key] = cookie; 23 | } 24 | 25 | public void Add(string key, string value) 26 | { 27 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 28 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value)); 29 | 30 | this.Add(new HttpCookie(key, value)); 31 | } 32 | 33 | public bool ContainsKey(string key) 34 | { 35 | CoreValidator.ThrowIfNull(key, nameof(key)); 36 | 37 | return this.cookies.ContainsKey(key); 38 | } 39 | 40 | public IEnumerator GetEnumerator() 41 | => this.cookies.Values.GetEnumerator(); 42 | 43 | IEnumerator IEnumerable.GetEnumerator() 44 | => this.cookies.Values.GetEnumerator(); 45 | 46 | public HttpCookie Get(string key) 47 | { 48 | CoreValidator.ThrowIfNull(key, nameof(key)); 49 | 50 | if (!this.cookies.ContainsKey(key)) 51 | { 52 | throw new InvalidOperationException($"The given key {key} is not present in the cookies collection."); 53 | } 54 | 55 | return this.cookies[key]; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpHeader.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using MyCoolWebServer.Server.Common; 4 | 5 | public class HttpHeader 6 | { 7 | public const string ContentType = "Content-Type"; 8 | public const string Host = "Host"; 9 | public const string Location = "Location"; 10 | public const string Cookie = "Cookie"; 11 | public const string SetCookie = "Set-Cookie"; 12 | 13 | public HttpHeader(string key, string value) 14 | { 15 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 16 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value)); 17 | 18 | this.Key = key; 19 | this.Value = value; 20 | } 21 | 22 | public string Key { get; private set; } 23 | 24 | public string Value { get; private set; } 25 | 26 | public override string ToString() => $"{this.Key}: {this.Value}"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpHeaderCollection.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using Common; 4 | using Contracts; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Collections; 9 | 10 | public class HttpHeaderCollection : IHttpHeaderCollection 11 | { 12 | private readonly IDictionary> headers; 13 | 14 | public HttpHeaderCollection() 15 | { 16 | this.headers = new Dictionary>(); 17 | } 18 | 19 | public void Add(HttpHeader header) 20 | { 21 | CoreValidator.ThrowIfNull(header, nameof(header)); 22 | 23 | var headerKey = header.Key; 24 | 25 | if (!this.headers.ContainsKey(headerKey)) 26 | { 27 | this.headers[headerKey] = new List(); 28 | } 29 | 30 | this.headers[headerKey].Add(header); 31 | } 32 | 33 | public void Add(string key, string value) 34 | { 35 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 36 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value)); 37 | 38 | this.Add(new HttpHeader(key, value)); 39 | } 40 | 41 | public bool ContainsKey(string key) 42 | { 43 | CoreValidator.ThrowIfNull(key, nameof(key)); 44 | 45 | return this.headers.ContainsKey(key); 46 | } 47 | 48 | public ICollection Get(string key) 49 | { 50 | CoreValidator.ThrowIfNull(key, nameof(key)); 51 | 52 | if (!this.headers.ContainsKey(key)) 53 | { 54 | throw new InvalidOperationException($"The given key {key} is not present in the headers collection."); 55 | } 56 | 57 | return this.headers[key]; 58 | } 59 | 60 | public IEnumerator> GetEnumerator() 61 | => this.headers.Values.GetEnumerator(); 62 | 63 | IEnumerator IEnumerable.GetEnumerator() 64 | => this.headers.Values.GetEnumerator(); 65 | 66 | public override string ToString() 67 | { 68 | var result = new StringBuilder(); 69 | 70 | foreach (var header in this.headers) 71 | { 72 | var headerKey = header.Key; 73 | 74 | foreach (var headerValue in header.Value) 75 | { 76 | result.AppendLine($"{headerKey}: {headerValue.Value}"); 77 | } 78 | } 79 | 80 | return result.ToString(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpRequest.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using Common; 4 | using Contracts; 5 | using Enums; 6 | using Exceptions; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net; 11 | 12 | public class HttpRequest : IHttpRequest 13 | { 14 | private readonly string requestText; 15 | 16 | public HttpRequest(string requestText) 17 | { 18 | CoreValidator.ThrowIfNullOrEmpty(requestText, nameof(requestText)); 19 | 20 | this.requestText = requestText; 21 | 22 | this.FormData = new Dictionary(); 23 | this.UrlParameters = new Dictionary(); 24 | this.Headers = new HttpHeaderCollection(); 25 | this.Cookies = new HttpCookieCollection(); 26 | 27 | this.ParseRequest(requestText); 28 | } 29 | 30 | public IDictionary FormData { get; private set; } 31 | 32 | public IHttpHeaderCollection Headers { get; private set; } 33 | 34 | public IHttpCookieCollection Cookies { get; private set; } 35 | 36 | public string Path { get; private set; } 37 | 38 | public HttpRequestMethod Method { get; private set; } 39 | 40 | public string Url { get; private set; } 41 | 42 | public IDictionary UrlParameters { get; private set; } 43 | 44 | public IHttpSession Session { get; set; } 45 | 46 | public void AddUrlParameter(string key, string value) 47 | { 48 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 49 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value)); 50 | 51 | this.UrlParameters[key] = value; 52 | } 53 | 54 | private void ParseRequest(string requestText) 55 | { 56 | var requestLines = requestText.Split(Environment.NewLine); 57 | 58 | if (!requestLines.Any()) 59 | { 60 | BadRequestException.ThrowFromInvalidRequest(); 61 | } 62 | 63 | var requestLine = requestLines.First().Split( 64 | new[] { ' ' }, 65 | StringSplitOptions.RemoveEmptyEntries); 66 | 67 | if (requestLine.Length != 3 || requestLine[2].ToLower() != "http/1.1") 68 | { 69 | BadRequestException.ThrowFromInvalidRequest(); 70 | } 71 | 72 | this.Method = this.ParseMethod(requestLine.First()); 73 | this.Url = requestLine[1]; 74 | this.Path = this.ParsePath(this.Url); 75 | 76 | this.ParseHeaders(requestLines); 77 | this.ParseCookies(); 78 | this.ParseParameters(); 79 | this.ParseFormData(requestLines.Last()); 80 | 81 | this.SetSession(); 82 | } 83 | 84 | private HttpRequestMethod ParseMethod(string method) 85 | { 86 | HttpRequestMethod parsedMethod; 87 | if (!Enum.TryParse(method, true, out parsedMethod)) 88 | { 89 | BadRequestException.ThrowFromInvalidRequest(); 90 | } 91 | 92 | return parsedMethod; 93 | } 94 | 95 | private string ParsePath(string url) 96 | => url.Split(new[] { '?', '#' }, StringSplitOptions.RemoveEmptyEntries)[0]; 97 | 98 | private void ParseHeaders(string[] requestLines) 99 | { 100 | var emptyLineAfterHeadersIndex = Array.IndexOf(requestLines, string.Empty); 101 | 102 | for (int i = 1; i < emptyLineAfterHeadersIndex; i++) 103 | { 104 | var currentLine = requestLines[i]; 105 | var headerParts = currentLine.Split(new[] { ": " }, StringSplitOptions.RemoveEmptyEntries); 106 | 107 | if (headerParts.Length != 2) 108 | { 109 | BadRequestException.ThrowFromInvalidRequest(); 110 | } 111 | 112 | var headerKey = headerParts[0]; 113 | var headerValue = headerParts[1].Trim(); 114 | 115 | var header = new HttpHeader(headerKey, headerValue); 116 | 117 | this.Headers.Add(header); 118 | } 119 | 120 | if (!this.Headers.ContainsKey(HttpHeader.Host)) 121 | { 122 | BadRequestException.ThrowFromInvalidRequest(); 123 | } 124 | } 125 | 126 | private void ParseCookies() 127 | { 128 | if (this.Headers.ContainsKey(HttpHeader.Cookie)) 129 | { 130 | var allCookies = this.Headers.Get(HttpHeader.Cookie); 131 | 132 | foreach (var cookie in allCookies) 133 | { 134 | if (!cookie.Value.Contains('=')) 135 | { 136 | return; 137 | } 138 | 139 | var cookieParts = cookie 140 | .Value 141 | .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) 142 | .ToList(); 143 | 144 | if (!cookieParts.Any()) 145 | { 146 | continue; 147 | } 148 | 149 | foreach (var cookiePart in cookieParts) 150 | { 151 | var cookieKeyValuePair = cookiePart 152 | .Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); 153 | 154 | if (cookieKeyValuePair.Length == 2) 155 | { 156 | var key = cookieKeyValuePair[0].Trim(); 157 | var value = cookieKeyValuePair[1].Trim(); 158 | 159 | this.Cookies.Add(new HttpCookie(key, value, false)); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | private void ParseParameters() 167 | { 168 | if (!this.Url.Contains('?')) 169 | { 170 | return; 171 | } 172 | 173 | var query = this.Url 174 | .Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries) 175 | .Last(); 176 | 177 | this.ParseQuery(query, this.UrlParameters); 178 | } 179 | 180 | private void ParseFormData(string formDataLine) 181 | { 182 | if (this.Method == HttpRequestMethod.Get) 183 | { 184 | return; 185 | } 186 | 187 | this.ParseQuery(formDataLine, this.FormData); 188 | } 189 | 190 | private void ParseQuery(string query, IDictionary dict) 191 | { 192 | if (!query.Contains('=')) 193 | { 194 | return; 195 | } 196 | 197 | var queryPairs = query.Split(new[] { '&' }); 198 | 199 | foreach (var queryPair in queryPairs) 200 | { 201 | var queryKvp = queryPair.Split(new[] { '=' }); 202 | 203 | if (queryKvp.Length != 2) 204 | { 205 | return; 206 | } 207 | 208 | var queryKey = WebUtility.UrlDecode(queryKvp[0]); 209 | var queryValue = WebUtility.UrlDecode(queryKvp[1]); 210 | 211 | dict.Add(queryKey, queryValue); 212 | } 213 | } 214 | 215 | private void SetSession() 216 | { 217 | if (this.Cookies.ContainsKey(SessionStore.SessionCookieKey)) 218 | { 219 | var cookie = this.Cookies.Get(SessionStore.SessionCookieKey); 220 | var sessionId = cookie.Value; 221 | 222 | this.Session = SessionStore.Get(sessionId); 223 | } 224 | } 225 | 226 | public override string ToString() => this.requestText; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/HttpSession.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using Common; 4 | using Contracts; 5 | using System.Collections.Generic; 6 | 7 | public class HttpSession : IHttpSession 8 | { 9 | private readonly IDictionary values; 10 | 11 | public HttpSession(string id) 12 | { 13 | CoreValidator.ThrowIfNullOrEmpty(id, nameof(id)); 14 | 15 | this.Id = id; 16 | this.values = new Dictionary(); 17 | } 18 | 19 | public string Id { get; private set; } 20 | 21 | public void Add(string key, object value) 22 | { 23 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 24 | CoreValidator.ThrowIfNull(value, nameof(value)); 25 | 26 | this.values[key] = value; 27 | } 28 | 29 | public void Clear() => this.values.Clear(); 30 | 31 | public object Get(string key) 32 | { 33 | CoreValidator.ThrowIfNull(key, nameof(key)); 34 | 35 | if (!this.values.ContainsKey(key)) 36 | { 37 | return null; 38 | } 39 | 40 | return this.values[key]; 41 | } 42 | 43 | public T Get(string key) 44 | => (T)this.Get(key); 45 | 46 | public bool Contains(string key) => this.values.ContainsKey(key); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Response/BadRequestResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Response 2 | { 3 | using Enums; 4 | 5 | public class BadRequestResponse : HttpResponse 6 | { 7 | public BadRequestResponse() 8 | { 9 | this.StatusCode = HttpStatusCode.BadRequest; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Response/HttpResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Response 2 | { 3 | using Contracts; 4 | using Enums; 5 | using System.Text; 6 | 7 | public abstract class HttpResponse : IHttpResponse 8 | { 9 | private string statusCodeMessage => this.StatusCode.ToString(); 10 | 11 | protected HttpResponse() 12 | { 13 | this.Headers = new HttpHeaderCollection(); 14 | this.Cookies = new HttpCookieCollection(); 15 | } 16 | 17 | public IHttpHeaderCollection Headers { get; } 18 | 19 | public IHttpCookieCollection Cookies { get; } 20 | 21 | public HttpStatusCode StatusCode { get; protected set; } 22 | 23 | public override string ToString() 24 | { 25 | var response = new StringBuilder(); 26 | 27 | var statusCodeNumber = (int)this.StatusCode; 28 | response.AppendLine($"HTTP/1.1 {statusCodeNumber} {this.statusCodeMessage}"); 29 | 30 | response.AppendLine(this.Headers.ToString()); 31 | 32 | return response.ToString(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Response/InternalServerErrorResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Response 2 | { 3 | using Enums; 4 | using MyCoolWebServer.Server.Common; 5 | using System; 6 | 7 | public class InternalServerErrorResponse : ViewResponse 8 | { 9 | public InternalServerErrorResponse(Exception ex) 10 | : base(HttpStatusCode.InternalServerError, new InternalServerErrorView(ex)) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Response/NotFoundResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Response 2 | { 3 | using Common; 4 | using Enums; 5 | 6 | public class NotFoundResponse : ViewResponse 7 | { 8 | public NotFoundResponse() 9 | : base(HttpStatusCode.NotFound, new NotFoundView()) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Response/RedirectResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Response 2 | { 3 | using Common; 4 | using Enums; 5 | 6 | public class RedirectResponse : HttpResponse 7 | { 8 | public RedirectResponse(string redirectUrl) 9 | { 10 | CoreValidator.ThrowIfNullOrEmpty(redirectUrl, nameof(redirectUrl)); 11 | 12 | this.StatusCode = HttpStatusCode.Found; 13 | 14 | this.Headers.Add(HttpHeader.Location, redirectUrl); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/Response/ViewResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http.Response 2 | { 3 | using Enums; 4 | using Exceptions; 5 | using Server.Contracts; 6 | 7 | public class ViewResponse : HttpResponse 8 | { 9 | private readonly IView view; 10 | 11 | public ViewResponse(HttpStatusCode statusCode, IView view) 12 | { 13 | this.ValidateStatusCode(statusCode); 14 | 15 | this.view = view; 16 | this.StatusCode = statusCode; 17 | 18 | this.Headers.Add(HttpHeader.ContentType, "text/html"); 19 | } 20 | 21 | private void ValidateStatusCode(HttpStatusCode statusCode) 22 | { 23 | var statusCodeNumber = (int)statusCode; 24 | 25 | if (299 < statusCodeNumber && statusCodeNumber < 400) 26 | { 27 | throw new InvalidResponseException("View responses need a status code below 300 and above 400 (inclusive)."); 28 | } 29 | } 30 | 31 | public override string ToString() 32 | { 33 | return $"{base.ToString()}{this.view.View()}"; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Http/SessionStore.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Http 2 | { 3 | using System.Collections.Concurrent; 4 | 5 | public static class SessionStore 6 | { 7 | public const string SessionCookieKey = "MY_SID"; 8 | public const string CurrentUserKey = "^%Current_User_Session_Key%^"; 9 | 10 | private static readonly ConcurrentDictionary sessions = 11 | new ConcurrentDictionary(); 12 | 13 | public static HttpSession Get(string id) 14 | => sessions.GetOrAdd(id, _ => new HttpSession(id)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Routing/AppRouteConfig.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Routing 2 | { 3 | using Contracts; 4 | using Enums; 5 | using Handlers; 6 | using MyCoolWebServer.Server.Http.Contracts; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | 11 | public class AppRouteConfig : IAppRouteConfig 12 | { 13 | private readonly Dictionary> routes; 14 | 15 | public AppRouteConfig() 16 | { 17 | this.AnonymousPaths = new List(); 18 | 19 | this.routes = new Dictionary>(); 20 | 21 | var availableMethods = Enum 22 | .GetValues(typeof(HttpRequestMethod)) 23 | .Cast(); 24 | 25 | foreach (var method in availableMethods) 26 | { 27 | this.routes[method] = new Dictionary(); 28 | } 29 | } 30 | 31 | public IReadOnlyDictionary> Routes => this.routes; 32 | 33 | public ICollection AnonymousPaths { get; private set; } 34 | 35 | public void Get(string route, Func handler) 36 | { 37 | this.AddRoute(route, HttpRequestMethod.Get, new RequestHandler(handler)); 38 | } 39 | 40 | public void Post(string route, Func handler) 41 | { 42 | this.AddRoute(route, HttpRequestMethod.Post, new RequestHandler(handler)); 43 | } 44 | 45 | public void AddRoute(string route, HttpRequestMethod method, RequestHandler handler) 46 | { 47 | this.routes[method].Add(route, handler); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Routing/Contracts/IAppRouteConfig.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Routing.Contracts 2 | { 3 | using Enums; 4 | using Handlers; 5 | using Http.Contracts; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | public interface IAppRouteConfig 10 | { 11 | IReadOnlyDictionary> Routes { get; } 12 | 13 | ICollection AnonymousPaths { get; } 14 | 15 | void Get(string route, Func handler); 16 | 17 | void Post(string route, Func handler); 18 | 19 | void AddRoute(string route, HttpRequestMethod method, RequestHandler handler); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Routing/Contracts/IRoutingContext.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Routing.Contracts 2 | { 3 | using Handlers; 4 | using System.Collections.Generic; 5 | 6 | public interface IRoutingContext 7 | { 8 | IEnumerable Parameters { get; } 9 | 10 | RequestHandler Handler { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Routing/Contracts/IServerRouteConfig.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Routing.Contracts 2 | { 3 | using Enums; 4 | using System.Collections.Generic; 5 | 6 | public interface IServerRouteConfig 7 | { 8 | IDictionary> Routes { get; } 9 | 10 | ICollection AnonymousPaths { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Routing/RoutingContext.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Routing 2 | { 3 | using Common; 4 | using Contracts; 5 | using Handlers; 6 | using System.Collections.Generic; 7 | 8 | public class RoutingContext : IRoutingContext 9 | { 10 | public RoutingContext(RequestHandler handler, IEnumerable parameters) 11 | { 12 | CoreValidator.ThrowIfNull(handler, nameof(handler)); 13 | CoreValidator.ThrowIfNull(handler, nameof(parameters)); 14 | 15 | this.Handler = handler; 16 | this.Parameters = parameters; 17 | } 18 | 19 | public IEnumerable Parameters { get; private set; } 20 | 21 | public RequestHandler Handler { get; private set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/Routing/ServerRouteConfig.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server.Routing 2 | { 3 | using Contracts; 4 | using Enums; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | 11 | public class ServerRouteConfig : IServerRouteConfig 12 | { 13 | private readonly IDictionary> routes; 14 | 15 | public ServerRouteConfig(IAppRouteConfig appRouteConfig) 16 | { 17 | this.AnonymousPaths = new List(appRouteConfig.AnonymousPaths); 18 | 19 | this.routes = new Dictionary>(); 20 | 21 | var availableMethods = Enum 22 | .GetValues(typeof(HttpRequestMethod)) 23 | .Cast(); 24 | 25 | foreach (var method in availableMethods) 26 | { 27 | this.routes[method] = new Dictionary(); 28 | } 29 | 30 | this.InitializeRouteConfig(appRouteConfig); 31 | } 32 | 33 | public IDictionary> Routes => this.routes; 34 | 35 | public ICollection AnonymousPaths { get; private set; } 36 | 37 | private void InitializeRouteConfig(IAppRouteConfig appRouteConfig) 38 | { 39 | foreach (var registeredRoute in appRouteConfig.Routes) 40 | { 41 | var requestMethod = registeredRoute.Key; 42 | var routesWithHandlers = registeredRoute.Value; 43 | 44 | foreach (var routeWithHandler in routesWithHandlers) 45 | { 46 | var route = routeWithHandler.Key; 47 | var handler = routeWithHandler.Value; 48 | 49 | var parameters = new List(); 50 | 51 | var parsedRouteRegex = this.ParseRoute(route, parameters); 52 | 53 | var routingContext = new RoutingContext(handler, parameters); 54 | 55 | this.routes[requestMethod].Add(parsedRouteRegex, routingContext); 56 | } 57 | } 58 | } 59 | 60 | private string ParseRoute(string route, List parameters) 61 | { 62 | if (route == "/") 63 | { 64 | return "^/$"; 65 | } 66 | 67 | var result = new StringBuilder(); 68 | 69 | result.Append("^/"); 70 | 71 | var tokens = route.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); 72 | 73 | this.ParseTokens(tokens, parameters, result); 74 | 75 | return result.ToString(); 76 | } 77 | 78 | private void ParseTokens(string[] tokens, List parameters, StringBuilder result) 79 | { 80 | for (int i = 0; i < tokens.Length; i++) 81 | { 82 | var end = i == tokens.Length - 1 ? "$" : "/"; 83 | var currentToken = tokens[i]; 84 | 85 | if (!currentToken.StartsWith('{') && !currentToken.EndsWith('}')) 86 | { 87 | result.Append($"{currentToken}{end}"); 88 | continue; 89 | } 90 | 91 | var parameterRegex = new Regex("<\\w+>"); 92 | var parameterMatch = parameterRegex.Match(currentToken); 93 | 94 | if (!parameterMatch.Success) 95 | { 96 | throw new InvalidOperationException($"Route parameter in '{currentToken}' is not valid."); 97 | } 98 | 99 | var match = parameterMatch.Value; 100 | var parameter = match.Substring(1, match.Length - 2); 101 | 102 | parameters.Add(parameter); 103 | 104 | var currentTokenWithoutCurlyBrackets = currentToken.Substring(1, currentToken.Length - 2); 105 | 106 | result.Append($"{currentTokenWithoutCurlyBrackets}{end}"); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MyCoolWebServer/Server/WebServer.cs: -------------------------------------------------------------------------------- 1 | namespace MyCoolWebServer.Server 2 | { 3 | using Contracts; 4 | using Routing; 5 | using Routing.Contracts; 6 | using System; 7 | using System.Net; 8 | using System.Net.Sockets; 9 | using System.Threading.Tasks; 10 | 11 | public class WebServer : IRunnable 12 | { 13 | private const string localHostIpAddress = "127.0.0.1"; 14 | 15 | private readonly int port; 16 | 17 | private readonly IServerRouteConfig serverRouteConfig; 18 | 19 | private readonly TcpListener listener; 20 | 21 | private bool isRunning; 22 | 23 | public WebServer(int port, IAppRouteConfig appRouteConfig) 24 | { 25 | this.port = port; 26 | this.listener = new TcpListener(IPAddress.Parse(localHostIpAddress), port); 27 | 28 | this.serverRouteConfig = new ServerRouteConfig(appRouteConfig); 29 | } 30 | 31 | public void Run() 32 | { 33 | this.listener.Start(); 34 | this.isRunning = true; 35 | 36 | Console.WriteLine($"Server running on {localHostIpAddress}:{this.port}"); 37 | 38 | Task.Run(this.ListenLoop).Wait(); 39 | } 40 | 41 | private async Task ListenLoop() 42 | { 43 | while (this.isRunning) 44 | { 45 | var client = await this.listener.AcceptSocketAsync(); 46 | var connectionHandler = new ConnectionHandler(client, this.serverRouteConfig); 47 | await connectionHandler.ProcessRequestAsync(); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My Cool Web Server 2 | 3 | Simple web server built from scratch as an exercise for [https://softuni.bg/trainings/1736/c-sharp-web-development-basics-september-2017](https://softuni.bg/trainings/1736/c-sharp-web-development-basics-september-2017). Enjoy! :) --------------------------------------------------------------------------------