├── .gitignore ├── GameStore.App ├── Controllers │ ├── AdminController.cs │ ├── BaseController.cs │ ├── HomeController.cs │ ├── OrdersController.cs │ └── UsersController.cs ├── Data │ ├── GameStoreDbContext.cs │ └── Models │ │ ├── Game.cs │ │ ├── Order.cs │ │ └── User.cs ├── GameStore.App.csproj ├── Infrastructure │ ├── DependencyControllerRouter.cs │ ├── HtmlHelpers.cs │ ├── HttpSessionExtensions.cs │ ├── Mapping │ │ ├── AutoMapperConfiguration.cs │ │ ├── IHaveCustomMapping.cs │ │ └── IMapFrom.cs │ ├── StringExtensions.cs │ └── Validation │ │ ├── Games │ │ ├── DescriptionAttribute.cs │ │ ├── ThumbnailUrlAttribute.cs │ │ ├── TitleAttribute.cs │ │ └── VideoIdAttribute.cs │ │ ├── RequiredAttribute.cs │ │ └── Users │ │ ├── EmailAttribute.cs │ │ └── PasswordAttribute.cs ├── Launcher.cs ├── Migrations │ ├── 20171024105903_UsersTable.Designer.cs │ ├── 20171024105903_UsersTable.cs │ ├── 20171024121234_UsersIsAdminColumn.Designer.cs │ ├── 20171024121234_UsersIsAdminColumn.cs │ ├── 20171024124624_GamesTable.Designer.cs │ ├── 20171024124624_GamesTable.cs │ └── GameStoreDbContextModelSnapshot.cs ├── Models │ ├── Games │ │ ├── GameAdminModel.cs │ │ └── GameListingAdminModel.cs │ ├── Home │ │ └── GameListingHomeModel.cs │ ├── Orders │ │ ├── GameListingOrdersModel.cs │ │ └── ShoppingCart.cs │ └── Users │ │ ├── LoginModel.cs │ │ └── RegisterModel.cs ├── Resources │ ├── css │ │ └── bootstrap.min.css │ └── js │ │ ├── bootstrap.min.js │ │ └── jquery-3.1.1.min.js ├── Services │ ├── Contracts │ │ ├── IGameService.cs │ │ ├── IOrderService.cs │ │ └── IUserService.cs │ ├── GameService.cs │ ├── OrderService.cs │ └── UserService.cs └── Views │ ├── Admin │ ├── Add.html │ ├── All.html │ ├── Delete.html │ └── Edit.html │ ├── Home │ └── Index.html │ ├── Layout.html │ ├── Orders │ └── Cart.html │ └── Users │ ├── Login.html │ └── Register.html ├── LICENSE ├── README.md ├── SimpleMvc.Framework ├── ActionResults │ ├── NotFoundResult.cs │ ├── RedirectResult.cs │ └── ViewResult.cs ├── Attributes │ ├── Methods │ │ ├── HttpGetAttribute.cs │ │ ├── HttpMethodAttribute.cs │ │ └── HttpPostAttribute.cs │ └── Validation │ │ ├── NumberRangeAttribute.cs │ │ ├── PropertyValidationAttribute.cs │ │ └── RegexAttribute.cs ├── Contracts │ ├── IActionResult.cs │ ├── IRedirectable.cs │ ├── IRenderable.cs │ └── IViewable.cs ├── Controllers │ └── Controller.cs ├── Errors │ └── Error.html ├── Helpers │ ├── ControllerHelpers.cs │ └── StringExtensions.cs ├── Models │ └── ViewModel.cs ├── MvcContext.cs ├── MvcEngine.cs ├── Routers │ ├── ControllerRouter.cs │ └── ResourceRouter.cs ├── Security │ └── Authentication.cs ├── SimpleMvc.Framework.csproj └── ViewEngine │ └── View.cs ├── SimpleMvc.sln └── WebServer ├── Common ├── CoreValidator.cs ├── InternalServerErrorView.cs └── NotFoundView.cs ├── ConnectionHandler.cs ├── Contracts ├── IHandleable.cs ├── IRunnable.cs └── IView.cs ├── Enums ├── HttpRequestMethod.cs └── HttpStatusCode.cs ├── Exceptions ├── BadRequestException.cs └── InvalidResponseException.cs ├── Http ├── Contracts │ ├── IHttpCookieCollection.cs │ ├── IHttpHeaderCollection.cs │ ├── IHttpRequest.cs │ ├── IHttpResponse.cs │ └── IHttpSession.cs ├── HttpCookie.cs ├── HttpCookieCollection.cs ├── HttpHeader.cs ├── HttpHeaderCollection.cs ├── HttpRequest.cs ├── HttpSession.cs ├── Response │ ├── BadRequestResponse.cs │ ├── ContentResponse.cs │ ├── FileResponse.cs │ ├── HttpResponse.cs │ ├── InternalServerErrorResponse.cs │ ├── NotFoundResponse.cs │ └── RedirectResponse.cs └── SessionStore.cs ├── WebServer.cs └── WebServer.csproj /.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 | -------------------------------------------------------------------------------- /GameStore.App/Controllers/AdminController.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Controllers 2 | { 3 | using Data.Models; 4 | using Infrastructure; 5 | using Models.Games; 6 | using Services.Contracts; 7 | using SimpleMvc.Framework.Attributes.Methods; 8 | using SimpleMvc.Framework.Contracts; 9 | using System.Linq; 10 | 11 | public class AdminController : BaseController 12 | { 13 | public const string GameError = "

Check your form for errors.

Title has to begin with uppercase letter and has length between 3 and 100 symbols (inclusive).

Price must be a positive number with precision up to 2 digits after floating point.

Size must be a positive number with precision up to 1 digit after floating point.

Videos should only be from YouTube.

Thumbnail URL should be a plain text starting with http://, https://.

Description must be at least 20 symbols.

"; 14 | 15 | private readonly IGameService games; 16 | 17 | public AdminController(IGameService games) 18 | { 19 | this.games = games; 20 | } 21 | 22 | public IActionResult Add() 23 | { 24 | if (!this.IsAdmin) 25 | { 26 | return this.RedirectToHome(); 27 | } 28 | 29 | return this.View(); 30 | } 31 | 32 | [HttpPost] 33 | public IActionResult Add(GameAdminModel model) 34 | { 35 | if (!this.IsAdmin) 36 | { 37 | return this.RedirectToHome(); 38 | } 39 | 40 | if (!this.IsValidModel(model)) 41 | { 42 | this.ShowError(GameError); 43 | return this.View(); 44 | } 45 | 46 | this.games.Create( 47 | model.Title, 48 | model.Description, 49 | model.ThumbnailUrl, 50 | model.Price, 51 | model.Size, 52 | model.VideoId, 53 | model.ReleaseDate); 54 | 55 | return this.RedirectToAllGames(); 56 | } 57 | 58 | public IActionResult Edit(int id) 59 | { 60 | if (!this.IsAdmin) 61 | { 62 | return this.RedirectToHome(); 63 | } 64 | 65 | var game = this.games.GetById(id); 66 | 67 | if (game == null) 68 | { 69 | return this.NotFound(); 70 | } 71 | 72 | this.SetGameViewData(game); 73 | 74 | return this.View(); 75 | } 76 | 77 | [HttpPost] 78 | public IActionResult Edit(int id, GameAdminModel model) 79 | { 80 | if (!this.IsAdmin) 81 | { 82 | return this.RedirectToHome(); 83 | } 84 | 85 | if (!this.IsValidModel(model)) 86 | { 87 | this.ShowError(GameError); 88 | return this.View(); 89 | } 90 | 91 | this.games.Update( 92 | id, 93 | model.Title, 94 | model.Description, 95 | model.ThumbnailUrl, 96 | model.Price, 97 | model.Size, 98 | model.VideoId, 99 | model.ReleaseDate); 100 | 101 | return this.RedirectToAllGames(); 102 | } 103 | 104 | public IActionResult Delete(int id) 105 | { 106 | if (!this.IsAdmin) 107 | { 108 | return this.RedirectToHome(); 109 | } 110 | 111 | var game = this.games.GetById(id); 112 | 113 | if (game == null) 114 | { 115 | return this.NotFound(); 116 | } 117 | 118 | this.ViewModel["id"] = id.ToString(); 119 | this.SetGameViewData(game); 120 | 121 | return this.View(); 122 | } 123 | 124 | [HttpPost] 125 | public IActionResult Destroy(int id) 126 | { 127 | if (!this.IsAdmin) 128 | { 129 | return this.RedirectToHome(); 130 | } 131 | 132 | var game = this.games.GetById(id); 133 | 134 | if (game == null) 135 | { 136 | return this.NotFound(); 137 | } 138 | 139 | this.games.Delete(id); 140 | 141 | return this.RedirectToAllGames(); 142 | } 143 | 144 | public IActionResult All() 145 | { 146 | if (!this.IsAdmin) 147 | { 148 | return this.RedirectToHome(); 149 | } 150 | 151 | var allGames = this.games 152 | .All() 153 | .Select(g => g.ToHtml()); 154 | 155 | this.ViewModel["games"] = string.Join(string.Empty, allGames); 156 | 157 | return this.View(); 158 | } 159 | 160 | private void SetGameViewData(Game game) 161 | { 162 | this.ViewModel["title"] = game.Title; 163 | this.ViewModel["description"] = game.Description; 164 | this.ViewModel["thumbnail"] = game.ThumbnailUrl; 165 | this.ViewModel["price"] = game.Price.ToString("F2"); 166 | this.ViewModel["size"] = game.Size.ToString("F1"); 167 | this.ViewModel["video-id"] = game.VideoId; 168 | this.ViewModel["release-date"] = game.ReleaseDate.ToString("yyyy-MM-dd"); 169 | } 170 | 171 | private IActionResult RedirectToAllGames() 172 | => this.Redirect("/admin/all"); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /GameStore.App/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Controllers 2 | { 3 | using Data; 4 | using Data.Models; 5 | using SimpleMvc.Framework.Contracts; 6 | using SimpleMvc.Framework.Controllers; 7 | using System.Linq; 8 | 9 | public abstract class BaseController : Controller 10 | { 11 | protected BaseController() 12 | { 13 | this.ViewModel["anonymousDisplay"] = "flex"; 14 | this.ViewModel["userDisplay"] = "none"; 15 | this.ViewModel["adminDisplay"] = "none"; 16 | this.ViewModel["show-error"] = "none"; 17 | this.ViewModel["show-success"] = "none"; 18 | } 19 | 20 | protected User Profile { get; private set; } 21 | 22 | protected void ShowError(string error) 23 | { 24 | this.ViewModel["show-error"] = "block"; 25 | this.ViewModel["error"] = error; 26 | } 27 | 28 | protected void ShowSuccess(string successMessage) 29 | { 30 | this.ViewModel["show-success"] = "block"; 31 | this.ViewModel["success"] = successMessage; 32 | } 33 | 34 | protected IActionResult RedirectToHome() 35 | => this.Redirect("/"); 36 | 37 | protected IActionResult RedirectToLogin() 38 | => this.Redirect("/users/login"); 39 | 40 | protected override void InitializeController() 41 | { 42 | base.InitializeController(); 43 | 44 | if (this.User.IsAuthenticated) 45 | { 46 | this.ViewModel["anonymousDisplay"] = "none"; 47 | this.ViewModel["userDisplay"] = "flex"; 48 | 49 | using (var db = new GameStoreDbContext()) 50 | { 51 | this.Profile = db 52 | .Users 53 | .First(u => u.Email == this.User.Name); 54 | 55 | if (this.Profile.IsAdmin) 56 | { 57 | this.ViewModel["userDisplay"] = "none"; 58 | this.ViewModel["adminDisplay"] = "flex"; 59 | } 60 | } 61 | } 62 | } 63 | 64 | protected bool IsAdmin => this.User.IsAuthenticated && this.Profile.IsAdmin; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GameStore.App/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Controllers 2 | { 3 | using Infrastructure; 4 | using Models.Home; 5 | using Services.Contracts; 6 | using SimpleMvc.Framework.Contracts; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | public class HomeController : BaseController 11 | { 12 | private readonly IGameService games; 13 | 14 | public HomeController(IGameService games) 15 | { 16 | this.games = games; 17 | } 18 | 19 | public IActionResult Index() 20 | { 21 | var ownedGames = this.User.IsAuthenticated 22 | && this.Request.UrlParameters.ContainsKey("filter") 23 | && this.Request.UrlParameters["filter"] == "Owned"; 24 | 25 | var gameCards = this.games 26 | .All(ownedGames ? (int?)this.Profile.Id : null) 27 | .Select(g => g.ToHtml(this.IsAdmin)) 28 | .ToList(); 29 | 30 | var result = new StringBuilder(); 31 | 32 | for (int i = 0; i < gameCards.Count; i++) 33 | { 34 | if (i % 3 == 0) 35 | { 36 | result.Append(@"
"); 37 | } 38 | 39 | result.Append(gameCards[i]); 40 | 41 | if (i % 3 == 2 || i == gameCards.Count - 1) 42 | { 43 | result.Append("
"); 44 | } 45 | } 46 | 47 | this.ViewModel["games"] = result.ToString(); 48 | 49 | return this.View(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GameStore.App/Controllers/OrdersController.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Controllers 2 | { 3 | using Infrastructure; 4 | using Models.Orders; 5 | using Services.Contracts; 6 | using SimpleMvc.Framework.Attributes.Methods; 7 | using SimpleMvc.Framework.Contracts; 8 | using System.Linq; 9 | 10 | public class OrdersController : BaseController 11 | { 12 | private readonly IGameService games; 13 | private readonly IOrderService orders; 14 | 15 | public OrdersController( 16 | IGameService games, 17 | IOrderService orders) 18 | { 19 | this.games = games; 20 | this.orders = orders; 21 | } 22 | 23 | public IActionResult Buy(int id) 24 | { 25 | if (!this.games.Exists(id)) 26 | { 27 | return this.NotFound(); 28 | } 29 | 30 | this.Request 31 | .Session 32 | .GetShoppingCart() 33 | .AddGame(id); 34 | 35 | return this.Redirect("/orders/cart"); 36 | } 37 | 38 | public IActionResult Remove(int id) 39 | { 40 | this.Request 41 | .Session 42 | .GetShoppingCart() 43 | .RemoveGame(id); 44 | 45 | return this.Redirect("/orders/cart"); 46 | } 47 | 48 | public IActionResult Cart() 49 | { 50 | var shoppingCart = this.Request.Session.GetShoppingCart(); 51 | 52 | var gameIds = shoppingCart.AllGames(); 53 | 54 | var gamesToBuy = this.games.ByIds(gameIds); 55 | 56 | var allGames = gamesToBuy.Select(g => g.ToHtml()); 57 | var totalPrice = 0m; 58 | 59 | if (gamesToBuy.Any()) 60 | { 61 | totalPrice = gamesToBuy.Sum(g => g.Price); 62 | } 63 | 64 | this.ViewModel["games"] = string.Join(string.Empty, allGames); 65 | this.ViewModel["total-price"] = totalPrice.ToString(); 66 | 67 | return this.View(); 68 | } 69 | 70 | [HttpPost] 71 | public IActionResult Finish() 72 | { 73 | if (!this.User.IsAuthenticated) 74 | { 75 | return this.RedirectToLogin(); 76 | } 77 | 78 | var shoppingCart = this.Request.Session.GetShoppingCart(); 79 | 80 | var gameIds = shoppingCart.AllGames(); 81 | 82 | this.orders.Purchase(this.Profile.Id, gameIds); 83 | 84 | shoppingCart.Clear(); 85 | 86 | return this.RedirectToHome(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /GameStore.App/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Controllers 2 | { 3 | using Models.Users; 4 | using Services.Contracts; 5 | using SimpleMvc.Framework.Attributes.Methods; 6 | using SimpleMvc.Framework.Contracts; 7 | 8 | public class UsersController : BaseController 9 | { 10 | private const string RegisterError = "

Check your form for errors.

E-mails must have at least one '@' and one '.' symbols.

Passwords must be at least 6 symbols and must contain at least 1 uppercase, 1 lowercase letter and 1 digit.

Confirm password must match the provided password.

"; 11 | private const string EmailExistsError = "E-mail is already taken."; 12 | private const string LoginError = "

Invalid credentials.

"; 13 | 14 | private IUserService users; 15 | 16 | public UsersController(IUserService users) 17 | { 18 | this.users = users; 19 | } 20 | 21 | public IActionResult Register() => this.View(); 22 | 23 | [HttpPost] 24 | public IActionResult Register(RegisterModel model) 25 | { 26 | if (model.Password != model.ConfirmPassword 27 | || !this.IsValidModel(model)) 28 | { 29 | this.ShowError(RegisterError); 30 | return this.View(); 31 | } 32 | 33 | var result = this.users.Create( 34 | model.Email, 35 | model.Password, 36 | model.Name); 37 | 38 | if (result) 39 | { 40 | return this.RedirectToLogin(); 41 | } 42 | else 43 | { 44 | this.ShowError(EmailExistsError); 45 | return this.View(); 46 | } 47 | } 48 | 49 | public IActionResult Login() => this.View(); 50 | 51 | [HttpPost] 52 | public IActionResult Login(LoginModel model) 53 | { 54 | if (!this.IsValidModel(model)) 55 | { 56 | this.ShowError(LoginError); 57 | return this.View(); 58 | } 59 | 60 | if (this.users.UserExists(model.Email, model.Password)) 61 | { 62 | this.SignIn(model.Email); 63 | return this.RedirectToHome(); 64 | } 65 | else 66 | { 67 | this.ShowError(LoginError); 68 | return this.View(); 69 | } 70 | } 71 | 72 | public IActionResult Logout() 73 | { 74 | this.SignOut(); 75 | return this.RedirectToHome(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /GameStore.App/Data/GameStoreDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.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 | public DbSet Orders { get; set; } 13 | 14 | protected override void OnConfiguring(DbContextOptionsBuilder builder) 15 | { 16 | builder 17 | .UseSqlServer("Server=.;Database=GameStoreExamDb;Integrated Security=True;"); 18 | } 19 | 20 | protected override void OnModelCreating(ModelBuilder builder) 21 | { 22 | builder 23 | .Entity() 24 | .HasIndex(u => u.Email) 25 | .IsUnique(); 26 | 27 | builder 28 | .Entity() 29 | .HasKey(o => new { o.UserId, o.GameId }); 30 | 31 | builder 32 | .Entity() 33 | .HasOne(o => o.User) 34 | .WithMany(u => u.Orders) 35 | .HasForeignKey(o => o.UserId); 36 | 37 | builder 38 | .Entity() 39 | .HasOne(o => o.Game) 40 | .WithMany(g => g.Orders) 41 | .HasForeignKey(o => o.GameId); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GameStore.App/Data/Models/Game.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Data.Models 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | public class Game 8 | { 9 | public int Id { get; set; } 10 | 11 | [Required] 12 | [MinLength(3)] 13 | [MaxLength(100)] 14 | public string Title { get; set; } 15 | 16 | [Range(0, double.MaxValue)] 17 | public decimal Price { get; set; } 18 | 19 | // In GB 20 | [Range(0, double.MaxValue)] 21 | public double Size { get; set; } 22 | 23 | [Required] 24 | [MinLength(11)] 25 | [MaxLength(11)] 26 | public string VideoId { get; set; } 27 | 28 | public string ThumbnailUrl { get; set; } 29 | 30 | [Required] 31 | [MinLength(20)] 32 | public string Description { get; set; } 33 | 34 | public DateTime ReleaseDate { get; set; } 35 | 36 | public List Orders { get; set; } = new List(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GameStore.App/Data/Models/Order.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Data.Models 2 | { 3 | public class Order 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 | -------------------------------------------------------------------------------- /GameStore.App/Data/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Data.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | public class User 7 | { 8 | public int Id { get; set; } 9 | 10 | [Required] 11 | [MaxLength(50)] 12 | public string Email { get; set; } 13 | 14 | [Required] 15 | [MinLength(6)] 16 | [MaxLength(50)] 17 | public string Password { get; set; } 18 | 19 | [MaxLength(100)] 20 | public string Name { get; set; } 21 | 22 | public bool IsAdmin { get; set; } 23 | 24 | public List Orders { get; set; } = new List(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GameStore.App/GameStore.App.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/DependencyControllerRouter.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure 2 | { 3 | using Data; 4 | using Services; 5 | using Services.Contracts; 6 | using SimpleMvc.Framework.Controllers; 7 | using SimpleMvc.Framework.Routers; 8 | using SimpleInjector; 9 | using SimpleInjector.Lifestyles; 10 | using System; 11 | 12 | public class DependencyControllerRouter : ControllerRouter 13 | { 14 | private readonly Container container; 15 | 16 | public DependencyControllerRouter() 17 | { 18 | this.container = new Container(); 19 | this.container.Options.DefaultScopedLifestyle 20 | = new AsyncScopedLifestyle(); 21 | } 22 | 23 | public Container Container => this.container; 24 | 25 | public static DependencyControllerRouter Get() 26 | { 27 | var router = new DependencyControllerRouter(); 28 | 29 | var container = router.Container; 30 | 31 | container.Register(); 32 | container.Register(); 33 | container.Register(); 34 | container.Register(Lifestyle.Scoped); 35 | 36 | container.Verify(); 37 | 38 | return router; 39 | } 40 | 41 | protected override Controller CreateController(Type controllerType) 42 | { 43 | AsyncScopedLifestyle.BeginScope(this.Container); 44 | return (Controller)this.Container.GetInstance(controllerType); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/HtmlHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure 2 | { 3 | using Models.Games; 4 | using Models.Home; 5 | using Models.Orders; 6 | 7 | public static class HtmlHelpers 8 | { 9 | public static string ToHtml(this GameListingHomeModel game, bool isAdmin) 10 | => $@" 11 |
12 | 15 |
16 |

{game.Title}

17 |

Price - {game.Price}€

18 |

Size - {game.Size} GB

19 |

{game.Description.Shortify(300)}

20 |
21 |
22 | {(!isAdmin ? string.Empty : $@" 23 | Edit 24 | Delete 25 | ")} 26 | 27 | Info 28 | Buy 29 |
30 |
"; 31 | 32 | public static string ToHtml(this GameListingAdminModel game) 33 | => $@" 34 | 35 | {game.Id} 36 | {game.Name} 37 | {game.Size} GB 38 | {game.Price} € 39 | 40 | Edit 41 | Delete 42 | 43 | "; 44 | 45 | public static string ToHtml(this GameListingOrdersModel game) 46 | => $@" 47 |
48 |
49 | X 50 | 52 |
53 | 54 |

{game.Title}

55 |
56 |

57 | {game.Description.Shortify(300)} 58 |

59 |
60 |
61 |

{game.Price}€

62 |
63 |
64 |
"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/HttpSessionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure 2 | { 3 | using Models.Orders; 4 | using WebServer.Http.Contracts; 5 | 6 | public static class HttpSessionExtensions 7 | { 8 | private const string ShoppingCartSessionKey = "%$Shopping_Cart$%"; 9 | 10 | public static ShoppingCart GetShoppingCart(this IHttpSession session) 11 | { 12 | var shoppingCart = session.Get(ShoppingCartSessionKey); 13 | if (shoppingCart == null) 14 | { 15 | shoppingCart = new ShoppingCart(); 16 | session.Add(ShoppingCartSessionKey, shoppingCart); 17 | } 18 | 19 | return shoppingCart; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Mapping/AutoMapperConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Mapping 2 | { 3 | using AutoMapper; 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | public static class AutoMapperConfiguration 9 | { 10 | public static void Initialize() 11 | { 12 | Mapper.Initialize(config => 13 | { 14 | var allTypes = Assembly 15 | .GetEntryAssembly() 16 | .GetTypes(); 17 | 18 | var mappedTypes = allTypes 19 | .Where(t => t 20 | .GetInterfaces() 21 | .Where(i => i.IsGenericType) 22 | .Any(i => i.GetGenericTypeDefinition() == typeof(IMapFrom<>))) 23 | .Select(t => new 24 | { 25 | Destination = t, 26 | Source = t.GetInterfaces() 27 | .Where(i => i.IsGenericType 28 | && i.GetGenericTypeDefinition() == typeof(IMapFrom<>)) 29 | .SelectMany(i => i.GetGenericArguments()) 30 | .First() 31 | }) 32 | .ToList(); 33 | 34 | foreach (var type in mappedTypes) 35 | { 36 | config.CreateMap(type.Source, type.Destination); 37 | } 38 | 39 | var customMappedTypes = allTypes 40 | .Where(t => t.IsClass 41 | && typeof(IHaveCustomMapping).IsAssignableFrom(t)) 42 | .Select(t => (IHaveCustomMapping)Activator.CreateInstance(t)) 43 | .ToList(); 44 | 45 | foreach (var type in customMappedTypes) 46 | { 47 | type.Configure(config); 48 | } 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Mapping/IHaveCustomMapping.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Mapping 2 | { 3 | using AutoMapper; 4 | 5 | public interface IHaveCustomMapping 6 | { 7 | void Configure(IMapperConfigurationExpression config); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Mapping/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Mapping 2 | { 3 | public interface IMapFrom 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure 2 | { 3 | public static class StringExtensions 4 | { 5 | public static string Shortify(this string input, int length) 6 | { 7 | if (input == null) 8 | { 9 | return input; 10 | } 11 | 12 | return input.Length > length ? $"{input.Substring(length)}..." : input; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/Games/DescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation.Games 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class DescriptionAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | { 9 | var description = value as string; 10 | if (description == null) 11 | { 12 | return true; 13 | } 14 | 15 | return description.Length >= 20; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/Games/ThumbnailUrlAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation.Games 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class ThumbnailUrlAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | { 9 | var thumbnailUrl = value as string; 10 | if (thumbnailUrl == null) 11 | { 12 | return true; 13 | } 14 | 15 | return thumbnailUrl.StartsWith("http://") 16 | || thumbnailUrl.StartsWith("https://"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/Games/TitleAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation.Games 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class TitleAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | { 9 | var title = value as string; 10 | if (title == null) 11 | { 12 | return true; 13 | } 14 | 15 | return title.Length > 0 16 | && char.IsUpper(title[0]) 17 | && title.Length >= 3 18 | && title.Length <= 100; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/Games/VideoIdAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation.Games 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class VideoIdAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | { 9 | var videoId = value as string; 10 | if (videoId == null) 11 | { 12 | return true; 13 | } 14 | 15 | return videoId.Length == 11; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/RequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class RequiredAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | => new System 9 | .ComponentModel 10 | .DataAnnotations 11 | .RequiredAttribute() 12 | .IsValid(value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/Users/EmailAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation.Users 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class EmailAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | { 9 | var email = value as string; 10 | if (email == null) 11 | { 12 | return true; 13 | } 14 | 15 | return email.Contains(".") && email.Contains("@"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GameStore.App/Infrastructure/Validation/Users/PasswordAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Infrastructure.Validation.Users 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | using System.Linq; 5 | 6 | public class PasswordAttribute : PropertyValidationAttribute 7 | { 8 | public override bool IsValid(object value) 9 | { 10 | var password = value as string; 11 | if (password == null) 12 | { 13 | return true; 14 | } 15 | 16 | return password.Any(s => char.IsDigit(s)) 17 | && password.Any(s => char.IsUpper(s)) 18 | && password.Any(s => char.IsLower(s)) 19 | && password.Length >= 6; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GameStore.App/Launcher.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App 2 | { 3 | using Data; 4 | using Infrastructure; 5 | using Infrastructure.Mapping; 6 | using Microsoft.EntityFrameworkCore; 7 | using SimpleMvc.Framework; 8 | using SimpleMvc.Framework.Routers; 9 | using WebServer; 10 | 11 | public class Launcher 12 | { 13 | static Launcher() 14 | { 15 | using (var db = new GameStoreDbContext()) 16 | { 17 | db.Database.Migrate(); 18 | } 19 | 20 | AutoMapperConfiguration.Initialize(); 21 | } 22 | 23 | public static void Main() 24 | => MvcEngine.Run( 25 | new WebServer(1337, 26 | DependencyControllerRouter.Get(), 27 | new ResourceRouter())); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/20171024105903_UsersTable.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using GameStore.App.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using System; 10 | 11 | namespace GameStore.App.Migrations 12 | { 13 | [DbContext(typeof(GameStoreDbContext))] 14 | [Migration("20171024105903_UsersTable")] 15 | partial class UsersTable 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("GameStore.App.Data.Models.User", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Email") 30 | .IsRequired() 31 | .HasMaxLength(50); 32 | 33 | b.Property("Name") 34 | .HasMaxLength(100); 35 | 36 | b.Property("Password") 37 | .IsRequired() 38 | .HasMaxLength(50); 39 | 40 | b.HasKey("Id"); 41 | 42 | b.HasIndex("Email") 43 | .IsUnique(); 44 | 45 | b.ToTable("Users"); 46 | }); 47 | #pragma warning restore 612, 618 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/20171024105903_UsersTable.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | public partial class UsersTable : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Users", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "int", nullable: false) 15 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 16 | Email = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), 17 | Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), 18 | Password = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Users", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateIndex( 26 | name: "IX_Users_Email", 27 | table: "Users", 28 | column: "Email", 29 | unique: true); 30 | } 31 | 32 | protected override void Down(MigrationBuilder migrationBuilder) 33 | { 34 | migrationBuilder.DropTable( 35 | name: "Users"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/20171024121234_UsersIsAdminColumn.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using GameStore.App.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using System; 10 | 11 | namespace GameStore.App.Migrations 12 | { 13 | [DbContext(typeof(GameStoreDbContext))] 14 | [Migration("20171024121234_UsersIsAdminColumn")] 15 | partial class UsersIsAdminColumn 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("GameStore.App.Data.Models.User", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Email") 30 | .IsRequired() 31 | .HasMaxLength(50); 32 | 33 | b.Property("IsAdmin"); 34 | 35 | b.Property("Name") 36 | .HasMaxLength(100); 37 | 38 | b.Property("Password") 39 | .IsRequired() 40 | .HasMaxLength(50); 41 | 42 | b.HasKey("Id"); 43 | 44 | b.HasIndex("Email") 45 | .IsUnique(); 46 | 47 | b.ToTable("Users"); 48 | }); 49 | #pragma warning restore 612, 618 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/20171024121234_UsersIsAdminColumn.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | public partial class UsersIsAdminColumn : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "IsAdmin", 11 | table: "Users", 12 | type: "bit", 13 | nullable: false, 14 | defaultValue: false); 15 | } 16 | 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | migrationBuilder.DropColumn( 20 | name: "IsAdmin", 21 | table: "Users"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/20171024124624_GamesTable.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using GameStore.App.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using System; 10 | 11 | namespace GameStore.App.Migrations 12 | { 13 | [DbContext(typeof(GameStoreDbContext))] 14 | [Migration("20171024124624_GamesTable")] 15 | partial class GamesTable 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("GameStore.App.Data.Models.Game", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Description") 30 | .IsRequired(); 31 | 32 | b.Property("Price"); 33 | 34 | b.Property("ReleaseDate"); 35 | 36 | b.Property("Size"); 37 | 38 | b.Property("ThumbnailUrl"); 39 | 40 | b.Property("Title") 41 | .IsRequired() 42 | .HasMaxLength(100); 43 | 44 | b.Property("VideoId") 45 | .IsRequired() 46 | .HasMaxLength(11); 47 | 48 | b.HasKey("Id"); 49 | 50 | b.ToTable("Games"); 51 | }); 52 | 53 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b => 54 | { 55 | b.Property("UserId"); 56 | 57 | b.Property("GameId"); 58 | 59 | b.HasKey("UserId", "GameId"); 60 | 61 | b.HasIndex("GameId"); 62 | 63 | b.ToTable("Orders"); 64 | }); 65 | 66 | modelBuilder.Entity("GameStore.App.Data.Models.User", b => 67 | { 68 | b.Property("Id") 69 | .ValueGeneratedOnAdd(); 70 | 71 | b.Property("Email") 72 | .IsRequired() 73 | .HasMaxLength(50); 74 | 75 | b.Property("IsAdmin"); 76 | 77 | b.Property("Name") 78 | .HasMaxLength(100); 79 | 80 | b.Property("Password") 81 | .IsRequired() 82 | .HasMaxLength(50); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.HasIndex("Email") 87 | .IsUnique(); 88 | 89 | b.ToTable("Users"); 90 | }); 91 | 92 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b => 93 | { 94 | b.HasOne("GameStore.App.Data.Models.Game", "Game") 95 | .WithMany("Orders") 96 | .HasForeignKey("GameId") 97 | .OnDelete(DeleteBehavior.Cascade); 98 | 99 | b.HasOne("GameStore.App.Data.Models.User", "User") 100 | .WithMany("Orders") 101 | .HasForeignKey("UserId") 102 | .OnDelete(DeleteBehavior.Cascade); 103 | }); 104 | #pragma warning restore 612, 618 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/20171024124624_GamesTable.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using System; 6 | 7 | public partial class GamesTable : 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 | Price = table.Column(type: "decimal(18, 2)", nullable: false), 19 | ReleaseDate = table.Column(type: "datetime2", nullable: false), 20 | Size = table.Column(type: "float", nullable: false), 21 | ThumbnailUrl = table.Column(type: "nvarchar(max)", nullable: true), 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: "Orders", 32 | columns: table => new 33 | { 34 | UserId = table.Column(type: "int", nullable: false), 35 | GameId = table.Column(type: "int", nullable: false) 36 | }, 37 | constraints: table => 38 | { 39 | table.PrimaryKey("PK_Orders", x => new { x.UserId, x.GameId }); 40 | table.ForeignKey( 41 | name: "FK_Orders_Games_GameId", 42 | column: x => x.GameId, 43 | principalTable: "Games", 44 | principalColumn: "Id", 45 | onDelete: ReferentialAction.Cascade); 46 | table.ForeignKey( 47 | name: "FK_Orders_Users_UserId", 48 | column: x => x.UserId, 49 | principalTable: "Users", 50 | principalColumn: "Id", 51 | onDelete: ReferentialAction.Cascade); 52 | }); 53 | 54 | migrationBuilder.CreateIndex( 55 | name: "IX_Orders_GameId", 56 | table: "Orders", 57 | column: "GameId"); 58 | } 59 | 60 | protected override void Down(MigrationBuilder migrationBuilder) 61 | { 62 | migrationBuilder.DropTable( 63 | name: "Orders"); 64 | 65 | migrationBuilder.DropTable( 66 | name: "Games"); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /GameStore.App/Migrations/GameStoreDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace GameStore.App.Migrations 3 | { 4 | using GameStore.App.Data; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.EntityFrameworkCore.Metadata; 8 | using System; 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("GameStore.App.Data.Models.Game", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd(); 24 | 25 | b.Property("Description") 26 | .IsRequired(); 27 | 28 | b.Property("Price"); 29 | 30 | b.Property("ReleaseDate"); 31 | 32 | b.Property("Size"); 33 | 34 | b.Property("ThumbnailUrl"); 35 | 36 | b.Property("Title") 37 | .IsRequired() 38 | .HasMaxLength(100); 39 | 40 | b.Property("VideoId") 41 | .IsRequired() 42 | .HasMaxLength(11); 43 | 44 | b.HasKey("Id"); 45 | 46 | b.ToTable("Games"); 47 | }); 48 | 49 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b => 50 | { 51 | b.Property("UserId"); 52 | 53 | b.Property("GameId"); 54 | 55 | b.HasKey("UserId", "GameId"); 56 | 57 | b.HasIndex("GameId"); 58 | 59 | b.ToTable("Orders"); 60 | }); 61 | 62 | modelBuilder.Entity("GameStore.App.Data.Models.User", b => 63 | { 64 | b.Property("Id") 65 | .ValueGeneratedOnAdd(); 66 | 67 | b.Property("Email") 68 | .IsRequired() 69 | .HasMaxLength(50); 70 | 71 | b.Property("IsAdmin"); 72 | 73 | b.Property("Name") 74 | .HasMaxLength(100); 75 | 76 | b.Property("Password") 77 | .IsRequired() 78 | .HasMaxLength(50); 79 | 80 | b.HasKey("Id"); 81 | 82 | b.HasIndex("Email") 83 | .IsUnique(); 84 | 85 | b.ToTable("Users"); 86 | }); 87 | 88 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b => 89 | { 90 | b.HasOne("GameStore.App.Data.Models.Game", "Game") 91 | .WithMany("Orders") 92 | .HasForeignKey("GameId") 93 | .OnDelete(DeleteBehavior.Cascade); 94 | 95 | b.HasOne("GameStore.App.Data.Models.User", "User") 96 | .WithMany("Orders") 97 | .HasForeignKey("UserId") 98 | .OnDelete(DeleteBehavior.Cascade); 99 | }); 100 | #pragma warning restore 612, 618 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /GameStore.App/Models/Games/GameAdminModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Games 2 | { 3 | using Infrastructure.Validation; 4 | using Infrastructure.Validation.Games; 5 | using SimpleMvc.Framework.Attributes.Validation; 6 | using System; 7 | 8 | public class GameAdminModel 9 | { 10 | [Required] 11 | [Title] 12 | public string Title { get; set; } 13 | 14 | [Required] 15 | [Description] 16 | public string Description { get; set; } 17 | 18 | [ThumbnailUrl] 19 | public string ThumbnailUrl { get; set; } 20 | 21 | [NumberRange(0, double.MaxValue)] 22 | public decimal Price { get; set; } 23 | 24 | [NumberRange(0, double.MaxValue)] 25 | public double Size { get; set; } 26 | 27 | [Required] 28 | [VideoId] 29 | public string VideoId { get; set; } 30 | 31 | public DateTime ReleaseDate { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GameStore.App/Models/Games/GameListingAdminModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Games 2 | { 3 | using AutoMapper; 4 | using Data.Models; 5 | using Infrastructure.Mapping; 6 | 7 | public class GameListingAdminModel : IMapFrom, IHaveCustomMapping 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | 13 | public double Size { get; set; } 14 | 15 | public decimal Price { get; set; } 16 | 17 | public void Configure(IMapperConfigurationExpression config) 18 | { 19 | config 20 | .CreateMap() 21 | .ForMember(gla => gla.Name, cfg => cfg.MapFrom(g => g.Title)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GameStore.App/Models/Home/GameListingHomeModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Home 2 | { 3 | using Data.Models; 4 | using Infrastructure.Mapping; 5 | 6 | public class GameListingHomeModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Title { get; set; } 11 | 12 | public decimal Price { get; set; } 13 | 14 | public double Size { get; set; } 15 | 16 | public string ThumbnailUrl { get; set; } 17 | 18 | public string VideoId { get; set; } 19 | 20 | public string Description { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GameStore.App/Models/Orders/GameListingOrdersModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Orders 2 | { 3 | using Data.Models; 4 | using Infrastructure.Mapping; 5 | 6 | public class GameListingOrdersModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Title { get; set; } 11 | 12 | public decimal Price { get; set; } 13 | 14 | public string VideoId { get; set; } 15 | 16 | public string ThumbnailUrl { get; set; } 17 | 18 | public string Description { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GameStore.App/Models/Orders/ShoppingCart.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Orders 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class ShoppingCart 6 | { 7 | private readonly ICollection gameIds; 8 | 9 | public ShoppingCart() 10 | { 11 | this.gameIds = new List(); 12 | } 13 | 14 | public void AddGame(int gameId) 15 | { 16 | if (!this.gameIds.Contains(gameId)) 17 | { 18 | this.gameIds.Add(gameId); 19 | } 20 | } 21 | 22 | public void RemoveGame(int gameId) => this.gameIds.Remove(gameId); 23 | 24 | public IEnumerable AllGames() => new List(this.gameIds); 25 | 26 | public void Clear() => this.gameIds.Clear(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GameStore.App/Models/Users/LoginModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Users 2 | { 3 | using Infrastructure.Validation; 4 | 5 | public class LoginModel 6 | { 7 | [Required] 8 | public string Email { get; set; } 9 | 10 | [Required] 11 | public string Password { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GameStore.App/Models/Users/RegisterModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Models.Users 2 | { 3 | using Infrastructure.Validation; 4 | using Infrastructure.Validation.Users; 5 | 6 | public class RegisterModel 7 | { 8 | [Required] 9 | [Email] 10 | public string Email { get; set; } 11 | 12 | public string Name { get; set; } 13 | 14 | [Required] 15 | [Password] 16 | public string Password { get; set; } 17 | 18 | [Required] 19 | public string ConfirmPassword { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GameStore.App/Resources/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.0.0-beta (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");!function(t){var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(),function(){function t(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function e(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o=function(){function t(t,e){for(var n=0;n0?n:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(e){t(e).trigger(s.end)},supportsTransitionEnd:function(){return Boolean(s)},typeCheckConfig:function(t,i,o){for(var r in o)if(o.hasOwnProperty(r)){var s=o[r],a=i[r],l=a&&n(a)?"element":e(a);if(!new RegExp(s).test(l))throw new Error(t.toUpperCase()+': Option "'+r+'" provided type "'+l+'" but expected type "'+s+'".')}}};return s=o(),t.fn.emulateTransitionEnd=r,l.supportsTransitionEnd()&&(t.event.special[l.TRANSITION_END]=i()),l}(jQuery),s=(function(t){var e="alert",i=t.fn[e],s={DISMISS:'[data-dismiss="alert"]'},a={CLOSE:"close.bs.alert",CLOSED:"closed.bs.alert",CLICK_DATA_API:"click.bs.alert.data-api"},l={ALERT:"alert",FADE:"fade",SHOW:"show"},h=function(){function e(t){n(this,e),this._element=t}return e.prototype.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.prototype.dispose=function(){t.removeData(this._element,"bs.alert"),this._element=null},e.prototype._getRootElement=function(e){var n=r.getSelectorFromElement(e),i=!1;return n&&(i=t(n)[0]),i||(i=t(e).closest("."+l.ALERT)[0]),i},e.prototype._triggerCloseEvent=function(e){var n=t.Event(a.CLOSE);return t(e).trigger(n),n},e.prototype._removeElement=function(e){var n=this;t(e).removeClass(l.SHOW),r.supportsTransitionEnd()&&t(e).hasClass(l.FADE)?t(e).one(r.TRANSITION_END,function(t){return n._destroyElement(e,t)}).emulateTransitionEnd(150):this._destroyElement(e)},e.prototype._destroyElement=function(e){t(e).detach().trigger(a.CLOSED).remove()},e._jQueryInterface=function(n){return this.each(function(){var i=t(this),o=i.data("bs.alert");o||(o=new e(this),i.data("bs.alert",o)),"close"===n&&o[n](this)})},e._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},o(e,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}}]),e}();t(document).on(a.CLICK_DATA_API,s.DISMISS,h._handleDismiss(new h)),t.fn[e]=h._jQueryInterface,t.fn[e].Constructor=h,t.fn[e].noConflict=function(){return t.fn[e]=i,h._jQueryInterface}}(jQuery),function(t){var e="button",i=t.fn[e],r={ACTIVE:"active",BUTTON:"btn",FOCUS:"focus"},s={DATA_TOGGLE_CARROT:'[data-toggle^="button"]',DATA_TOGGLE:'[data-toggle="buttons"]',INPUT:"input",ACTIVE:".active",BUTTON:".btn"},a={CLICK_DATA_API:"click.bs.button.data-api",FOCUS_BLUR_DATA_API:"focus.bs.button.data-api blur.bs.button.data-api"},l=function(){function e(t){n(this,e),this._element=t}return e.prototype.toggle=function(){var e=!0,n=!0,i=t(this._element).closest(s.DATA_TOGGLE)[0];if(i){var o=t(this._element).find(s.INPUT)[0];if(o){if("radio"===o.type)if(o.checked&&t(this._element).hasClass(r.ACTIVE))e=!1;else{var a=t(i).find(s.ACTIVE)[0];a&&t(a).removeClass(r.ACTIVE)}if(e){if(o.hasAttribute("disabled")||i.hasAttribute("disabled")||o.classList.contains("disabled")||i.classList.contains("disabled"))return;o.checked=!t(this._element).hasClass(r.ACTIVE),t(o).trigger("change")}o.focus(),n=!1}}n&&this._element.setAttribute("aria-pressed",!t(this._element).hasClass(r.ACTIVE)),e&&t(this._element).toggleClass(r.ACTIVE)},e.prototype.dispose=function(){t.removeData(this._element,"bs.button"),this._element=null},e._jQueryInterface=function(n){return this.each(function(){var i=t(this).data("bs.button");i||(i=new e(this),t(this).data("bs.button",i)),"toggle"===n&&i[n]()})},o(e,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}}]),e}();t(document).on(a.CLICK_DATA_API,s.DATA_TOGGLE_CARROT,function(e){e.preventDefault();var n=e.target;t(n).hasClass(r.BUTTON)||(n=t(n).closest(s.BUTTON)),l._jQueryInterface.call(t(n),"toggle")}).on(a.FOCUS_BLUR_DATA_API,s.DATA_TOGGLE_CARROT,function(e){var n=t(e.target).closest(s.BUTTON)[0];t(n).toggleClass(r.FOCUS,/^focus(in)?$/.test(e.type))}),t.fn[e]=l._jQueryInterface,t.fn[e].Constructor=l,t.fn[e].noConflict=function(){return t.fn[e]=i,l._jQueryInterface}}(jQuery),function(t){var e="carousel",s="bs.carousel",a="."+s,l=t.fn[e],h={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},c={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},u={NEXT:"next",PREV:"prev",LEFT:"left",RIGHT:"right"},d={SLIDE:"slide"+a,SLID:"slid"+a,KEYDOWN:"keydown"+a,MOUSEENTER:"mouseenter"+a,MOUSELEAVE:"mouseleave"+a,TOUCHEND:"touchend"+a,LOAD_DATA_API:"load.bs.carousel.data-api",CLICK_DATA_API:"click.bs.carousel.data-api"},f={CAROUSEL:"carousel",ACTIVE:"active",SLIDE:"slide",RIGHT:"carousel-item-right",LEFT:"carousel-item-left",NEXT:"carousel-item-next",PREV:"carousel-item-prev",ITEM:"carousel-item"},p={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},_=function(){function l(e,i){n(this,l),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(i),this._element=t(e)[0],this._indicatorsElement=t(this._element).find(p.INDICATORS)[0],this._addEventListeners()}return l.prototype.next=function(){this._isSliding||this._slide(u.NEXT)},l.prototype.nextWhenVisible=function(){document.hidden||this.next()},l.prototype.prev=function(){this._isSliding||this._slide(u.PREV)},l.prototype.pause=function(e){e||(this._isPaused=!0),t(this._element).find(p.NEXT_PREV)[0]&&r.supportsTransitionEnd()&&(r.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},l.prototype.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},l.prototype.to=function(e){var n=this;this._activeElement=t(this._element).find(p.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)t(this._element).one(d.SLID,function(){return n.to(e)});else{if(i===e)return this.pause(),void this.cycle();var o=e>i?u.NEXT:u.PREV;this._slide(o,this._items[e])}},l.prototype.dispose=function(){t(this._element).off(a),t.removeData(this._element,s),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},l.prototype._getConfig=function(n){return n=t.extend({},h,n),r.typeCheckConfig(e,n,c),n},l.prototype._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(d.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&(t(this._element).on(d.MOUSEENTER,function(t){return e.pause(t)}).on(d.MOUSELEAVE,function(t){return e.cycle(t)}),"ontouchstart"in document.documentElement&&t(this._element).on(d.TOUCHEND,function(){e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout(function(t){return e.cycle(t)},500+e._config.interval)}))},l.prototype._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next();break;default:return}},l.prototype._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(p.ITEM)),this._items.indexOf(e)},l.prototype._getItemByDirection=function(t,e){var n=t===u.NEXT,i=t===u.PREV,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+(t===u.PREV?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},l.prototype._triggerSlideEvent=function(e,n){var i=this._getItemIndex(e),o=this._getItemIndex(t(this._element).find(p.ACTIVE_ITEM)[0]),r=t.Event(d.SLIDE,{relatedTarget:e,direction:n,from:o,to:i});return t(this._element).trigger(r),r},l.prototype._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(p.ACTIVE).removeClass(f.ACTIVE);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(f.ACTIVE)}},l.prototype._slide=function(e,n){var i=this,o=t(this._element).find(p.ACTIVE_ITEM)[0],s=this._getItemIndex(o),a=n||o&&this._getItemByDirection(e,o),l=this._getItemIndex(a),h=Boolean(this._interval),c=void 0,_=void 0,g=void 0;if(e===u.NEXT?(c=f.LEFT,_=f.NEXT,g=u.LEFT):(c=f.RIGHT,_=f.PREV,g=u.RIGHT),a&&t(a).hasClass(f.ACTIVE))this._isSliding=!1;else if(!this._triggerSlideEvent(a,g).isDefaultPrevented()&&o&&a){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(a);var m=t.Event(d.SLID,{relatedTarget:a,direction:g,from:s,to:l});r.supportsTransitionEnd()&&t(this._element).hasClass(f.SLIDE)?(t(a).addClass(_),r.reflow(a),t(o).addClass(c),t(a).addClass(c),t(o).one(r.TRANSITION_END,function(){t(a).removeClass(c+" "+_).addClass(f.ACTIVE),t(o).removeClass(f.ACTIVE+" "+_+" "+c),i._isSliding=!1,setTimeout(function(){return t(i._element).trigger(m)},0)}).emulateTransitionEnd(600)):(t(o).removeClass(f.ACTIVE),t(a).addClass(f.ACTIVE),this._isSliding=!1,t(this._element).trigger(m)),h&&this.cycle()}},l._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(s),o=t.extend({},h,t(this).data());"object"===(void 0===e?"undefined":i(e))&&t.extend(o,e);var r="string"==typeof e?e:o.slide;if(n||(n=new l(this,o),t(this).data(s,n)),"number"==typeof e)n.to(e);else if("string"==typeof r){if(void 0===n[r])throw new Error('No method named "'+r+'"');n[r]()}else o.interval&&(n.pause(),n.cycle())})},l._dataApiClickHandler=function(e){var n=r.getSelectorFromElement(this);if(n){var i=t(n)[0];if(i&&t(i).hasClass(f.CAROUSEL)){var o=t.extend({},t(i).data(),t(this).data()),a=this.getAttribute("data-slide-to");a&&(o.interval=!1),l._jQueryInterface.call(t(i),o),a&&t(i).data(s).to(a),e.preventDefault()}}},o(l,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}},{key:"Default",get:function(){return h}}]),l}();t(document).on(d.CLICK_DATA_API,p.DATA_SLIDE,_._dataApiClickHandler),t(window).on(d.LOAD_DATA_API,function(){t(p.DATA_RIDE).each(function(){var e=t(this);_._jQueryInterface.call(e,e.data())})}),t.fn[e]=_._jQueryInterface,t.fn[e].Constructor=_,t.fn[e].noConflict=function(){return t.fn[e]=l,_._jQueryInterface}}(jQuery),function(t){var e="collapse",s="bs.collapse",a=t.fn[e],l={toggle:!0,parent:""},h={toggle:"boolean",parent:"string"},c={SHOW:"show.bs.collapse",SHOWN:"shown.bs.collapse",HIDE:"hide.bs.collapse",HIDDEN:"hidden.bs.collapse",CLICK_DATA_API:"click.bs.collapse.data-api"},u={SHOW:"show",COLLAPSE:"collapse",COLLAPSING:"collapsing",COLLAPSED:"collapsed"},d={WIDTH:"width",HEIGHT:"height"},f={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},p=function(){function a(e,i){n(this,a),this._isTransitioning=!1,this._element=e,this._config=this._getConfig(i),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var o=t(f.DATA_TOGGLE),s=0;s0&&this._triggerArray.push(l)}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}return a.prototype.toggle=function(){t(this._element).hasClass(u.SHOW)?this.hide():this.show()},a.prototype.show=function(){var e=this;if(!this._isTransitioning&&!t(this._element).hasClass(u.SHOW)){var n=void 0,i=void 0;if(this._parent&&((n=t.makeArray(t(this._parent).children().children(f.ACTIVES))).length||(n=null)),!(n&&(i=t(n).data(s))&&i._isTransitioning)){var o=t.Event(c.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){n&&(a._jQueryInterface.call(t(n),"hide"),i||t(n).data(s,null));var l=this._getDimension();t(this._element).removeClass(u.COLLAPSE).addClass(u.COLLAPSING),this._element.style[l]=0,this._triggerArray.length&&t(this._triggerArray).removeClass(u.COLLAPSED).attr("aria-expanded",!0),this.setTransitioning(!0);var h=function(){t(e._element).removeClass(u.COLLAPSING).addClass(u.COLLAPSE).addClass(u.SHOW),e._element.style[l]="",e.setTransitioning(!1),t(e._element).trigger(c.SHOWN)};if(r.supportsTransitionEnd()){var d="scroll"+(l[0].toUpperCase()+l.slice(1));t(this._element).one(r.TRANSITION_END,h).emulateTransitionEnd(600),this._element.style[l]=this._element[d]+"px"}else h()}}}},a.prototype.hide=function(){var e=this;if(!this._isTransitioning&&t(this._element).hasClass(u.SHOW)){var n=t.Event(c.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",r.reflow(this._element),t(this._element).addClass(u.COLLAPSING).removeClass(u.COLLAPSE).removeClass(u.SHOW),this._triggerArray.length)for(var o=0;o0},l.prototype._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:{offset:this._config.offset},flip:{enabled:this._config.flip}}};return this._inNavbar&&(t.modifiers.applyStyle={enabled:!this._inNavbar}),t},l._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(s),o="object"===(void 0===e?"undefined":i(e))?e:null;if(n||(n=new l(this,o),t(this).data(s,n)),"string"==typeof e){if(void 0===n[e])throw new Error('No method named "'+e+'"');n[e]()}})},l._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=t.makeArray(t(d.DATA_TOGGLE)),i=0;i0&&r--,40===e.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},a.prototype._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},a.prototype._checkScrollbar=function(){this._isBodyOverflowing=document.body.clientWidth=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t .dropdown-menu .active"},l=function(){function e(t){n(this,e),this._element=t}return e.prototype.show=function(){var e=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&t(this._element).hasClass(s.ACTIVE)||t(this._element).hasClass(s.DISABLED))){var n=void 0,o=void 0,l=t(this._element).closest(a.NAV_LIST_GROUP)[0],h=r.getSelectorFromElement(this._element);l&&(o=t.makeArray(t(l).find(a.ACTIVE)),o=o[o.length-1]);var c=t.Event(i.HIDE,{relatedTarget:this._element}),u=t.Event(i.SHOW,{relatedTarget:o});if(o&&t(o).trigger(c),t(this._element).trigger(u),!u.isDefaultPrevented()&&!c.isDefaultPrevented()){h&&(n=t(h)[0]),this._activate(this._element,l);var d=function(){var n=t.Event(i.HIDDEN,{relatedTarget:e._element}),r=t.Event(i.SHOWN,{relatedTarget:o});t(o).trigger(n),t(e._element).trigger(r)};n?this._activate(n,n.parentNode,d):d()}}},e.prototype.dispose=function(){t.removeData(this._element,"bs.tab"),this._element=null},e.prototype._activate=function(e,n,i){var o=this,l=t(n).find(a.ACTIVE)[0],h=i&&r.supportsTransitionEnd()&&l&&t(l).hasClass(s.FADE),c=function(){return o._transitionComplete(e,l,h,i)};l&&h?t(l).one(r.TRANSITION_END,c).emulateTransitionEnd(150):c(),l&&t(l).removeClass(s.SHOW)},e.prototype._transitionComplete=function(e,n,i,o){if(n){t(n).removeClass(s.ACTIVE);var l=t(n.parentNode).find(a.DROPDOWN_ACTIVE_CHILD)[0];l&&t(l).removeClass(s.ACTIVE),n.setAttribute("aria-expanded",!1)}if(t(e).addClass(s.ACTIVE),e.setAttribute("aria-expanded",!0),i?(r.reflow(e),t(e).addClass(s.SHOW)):t(e).removeClass(s.FADE),e.parentNode&&t(e.parentNode).hasClass(s.DROPDOWN_MENU)){var h=t(e).closest(a.DROPDOWN)[0];h&&t(h).find(a.DROPDOWN_TOGGLE).addClass(s.ACTIVE),e.setAttribute("aria-expanded",!0)}o&&o()},e._jQueryInterface=function(n){return this.each(function(){var i=t(this),o=i.data("bs.tab");if(o||(o=new e(this),i.data("bs.tab",o)),"string"==typeof n){if(void 0===o[n])throw new Error('No method named "'+n+'"');o[n]()}})},o(e,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}}]),e}();t(document).on(i.CLICK_DATA_API,a.DATA_TOGGLE,function(e){e.preventDefault(),l._jQueryInterface.call(t(this),"show")}),t.fn.tab=l._jQueryInterface,t.fn.tab.Constructor=l,t.fn.tab.noConflict=function(){return t.fn.tab=e,l._jQueryInterface}}(jQuery),function(t){if("undefined"==typeof Popper)throw new Error("Bootstrap tooltips require Popper.js (https://popper.js.org)");var e="tooltip",s=".bs.tooltip",a=t.fn[e],l=new RegExp("(^|\\s)bs-tooltip\\S+","g"),h={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)"},c={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"},u={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip"},d={SHOW:"show",OUT:"out"},f={HIDE:"hide"+s,HIDDEN:"hidden"+s,SHOW:"show"+s,SHOWN:"shown"+s,INSERTED:"inserted"+s,CLICK:"click"+s,FOCUSIN:"focusin"+s,FOCUSOUT:"focusout"+s,MOUSEENTER:"mouseenter"+s,MOUSELEAVE:"mouseleave"+s},p={FADE:"fade",SHOW:"show"},_={TOOLTIP:".tooltip",TOOLTIP_INNER:".tooltip-inner",ARROW:".arrow"},g={HOVER:"hover",FOCUS:"focus",CLICK:"click",MANUAL:"manual"},m=function(){function a(t,e){n(this,a),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}return a.prototype.enable=function(){this._isEnabled=!0},a.prototype.disable=function(){this._isEnabled=!1},a.prototype.toggleEnabled=function(){this._isEnabled=!this._isEnabled},a.prototype.toggle=function(e){if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p.SHOW))return void this._leave(null,this);this._enter(null,this)}},a.prototype.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},a.prototype.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var n=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(n);var i=t.contains(this.element.ownerDocument.documentElement,this.element);if(n.isDefaultPrevented()||!i)return;var o=this.getTipElement(),s=r.getUID(this.constructor.NAME);o.setAttribute("id",s),this.element.setAttribute("aria-describedby",s),this.setContent(),this.config.animation&&t(o).addClass(p.FADE);var l="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(o).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(o).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new Popper(this.element,o,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:_.ARROW}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(o).addClass(p.SHOW),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d.OUT&&e._leave(null,e)};r.supportsTransitionEnd()&&t(this.tip).hasClass(p.FADE)?t(this.tip).one(r.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},a.prototype.hide=function(e){var n=this,i=this.getTipElement(),o=t.Event(this.constructor.Event.HIDE),s=function(){n._hoverState!==d.SHOW&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(o),o.isDefaultPrevented()||(t(i).removeClass(p.SHOW),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[g.CLICK]=!1,this._activeTrigger[g.FOCUS]=!1,this._activeTrigger[g.HOVER]=!1,r.supportsTransitionEnd()&&t(this.tip).hasClass(p.FADE)?t(i).one(r.TRANSITION_END,s).emulateTransitionEnd(150):s(),this._hoverState="")},a.prototype.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},a.prototype.isWithContent=function(){return Boolean(this.getTitle())},a.prototype.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},a.prototype.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0]},a.prototype.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(_.TOOLTIP_INNER),this.getTitle()),e.removeClass(p.FADE+" "+p.SHOW)},a.prototype.setElementContent=function(e,n){var o=this.config.html;"object"===(void 0===n?"undefined":i(n))&&(n.nodeType||n.jquery)?o?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[o?"html":"text"](n)},a.prototype.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},a.prototype._getAttachment=function(t){return c[t.toUpperCase()]},a.prototype._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==g.MANUAL){var i=n===g.HOVER?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,o=n===g.HOVER?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(o,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=t.extend({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},a.prototype._fixTitle=function(){var t=i(this.element.getAttribute("data-original-title"));(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},a.prototype._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?g.FOCUS:g.HOVER]=!0),t(n.getTipElement()).hasClass(p.SHOW)||n._hoverState===d.SHOW?n._hoverState=d.SHOW:(clearTimeout(n._timeout),n._hoverState=d.SHOW,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===d.SHOW&&n.show()},n.config.delay.show):n.show())},a.prototype._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?g.FOCUS:g.HOVER]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d.OUT,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d.OUT&&n.hide()},n.config.delay.hide):n.hide())},a.prototype._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},a.prototype._getConfig=function(n){return(n=t.extend({},this.constructor.Default,t(this.element).data(),n)).delay&&"number"==typeof n.delay&&(n.delay={show:n.delay,hide:n.delay}),n.title&&"number"==typeof n.title&&(n.title=n.title.toString()),n.content&&"number"==typeof n.content&&(n.content=n.content.toString()),r.typeCheckConfig(e,n,this.constructor.DefaultType),n},a.prototype._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},a.prototype._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},a.prototype._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},a.prototype._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(p.FADE),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data("bs.tooltip"),o="object"===(void 0===e?"undefined":i(e))&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,o),t(this).data("bs.tooltip",n)),"string"==typeof e)){if(void 0===n[e])throw new Error('No method named "'+e+'"');n[e]()}})},o(a,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return f}},{key:"EVENT_KEY",get:function(){return s}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=a,m._jQueryInterface},m}(jQuery));!function(r){var a="popover",l=".bs.popover",h=r.fn[a],c=new RegExp("(^|\\s)bs-popover\\S+","g"),u=r.extend({},s.Default,{placement:"right",trigger:"click",content:"",template:''}),d=r.extend({},s.DefaultType,{content:"(string|element|function)"}),f={FADE:"fade",SHOW:"show"},p={TITLE:".popover-header",CONTENT:".popover-body"},_={HIDE:"hide"+l,HIDDEN:"hidden"+l,SHOW:"show"+l,SHOWN:"shown"+l,INSERTED:"inserted"+l,CLICK:"click"+l,FOCUSIN:"focusin"+l,FOCUSOUT:"focusout"+l,MOUSEENTER:"mouseenter"+l,MOUSELEAVE:"mouseleave"+l},g=function(s){function h(){return n(this,h),t(this,s.apply(this,arguments))}return e(h,s),h.prototype.isWithContent=function(){return this.getTitle()||this._getContent()},h.prototype.addAttachmentClass=function(t){r(this.getTipElement()).addClass("bs-popover-"+t)},h.prototype.getTipElement=function(){return this.tip=this.tip||r(this.config.template)[0]},h.prototype.setContent=function(){var t=r(this.getTipElement());this.setElementContent(t.find(p.TITLE),this.getTitle()),this.setElementContent(t.find(p.CONTENT),this._getContent()),t.removeClass(f.FADE+" "+f.SHOW)},h.prototype._getContent=function(){return this.element.getAttribute("data-content")||("function"==typeof this.config.content?this.config.content.call(this.element):this.config.content)},h.prototype._cleanTipClass=function(){var t=r(this.getTipElement()),e=t.attr("class").match(c);null!==e&&e.length>0&&t.removeClass(e.join(""))},h._jQueryInterface=function(t){return this.each(function(){var e=r(this).data("bs.popover"),n="object"===(void 0===t?"undefined":i(t))?t:null;if((e||!/destroy|hide/.test(t))&&(e||(e=new h(this,n),r(this).data("bs.popover",e)),"string"==typeof t)){if(void 0===e[t])throw new Error('No method named "'+t+'"');e[t]()}})},o(h,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return a}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return l}},{key:"DefaultType",get:function(){return d}}]),h}(s);r.fn[a]=g._jQueryInterface,r.fn[a].Constructor=g,r.fn[a].noConflict=function(){return r.fn[a]=h,g._jQueryInterface}}(jQuery)}(); -------------------------------------------------------------------------------- /GameStore.App/Services/Contracts/IGameService.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Services.Contracts 2 | { 3 | using Data.Models; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | public interface IGameService 8 | { 9 | void Create( 10 | string title, 11 | string description, 12 | string thumbnailUrl, 13 | decimal price, 14 | double size, 15 | string videoId, 16 | DateTime releaseDate); 17 | 18 | void Update( 19 | int id, 20 | string title, 21 | string description, 22 | string thumbnailUrl, 23 | decimal price, 24 | double size, 25 | string videoId, 26 | DateTime releaseDate); 27 | 28 | void Delete(int id); 29 | 30 | Game GetById(int id); 31 | 32 | bool Exists(int id); 33 | 34 | IEnumerable ByIds(IEnumerable ids); 35 | 36 | IEnumerable All(int? userId = null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GameStore.App/Services/Contracts/IOrderService.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Services.Contracts 2 | { 3 | using System.Collections.Generic; 4 | 5 | public interface IOrderService 6 | { 7 | void Purchase(int userId, IEnumerable gameIds); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GameStore.App/Services/Contracts/IUserService.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Services.Contracts 2 | { 3 | public interface IUserService 4 | { 5 | bool Create(string email, string password, string name); 6 | 7 | bool UserExists(string email, string password); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GameStore.App/Services/GameService.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Services 2 | { 3 | using AutoMapper.QueryableExtensions; 4 | using Contracts; 5 | using Data; 6 | using Data.Models; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | 11 | public class GameService : IGameService 12 | { 13 | private readonly GameStoreDbContext db; 14 | 15 | public GameService(GameStoreDbContext db) 16 | { 17 | this.db = db; 18 | } 19 | 20 | public void Create( 21 | string title, 22 | string description, 23 | string thumbnailUrl, 24 | decimal price, 25 | double size, 26 | string videoId, 27 | DateTime releaseDate) 28 | { 29 | var game = new Game 30 | { 31 | Title = title, 32 | Description = description, 33 | Price = price, 34 | Size = size, 35 | ThumbnailUrl = thumbnailUrl, 36 | VideoId = videoId, 37 | ReleaseDate = releaseDate 38 | }; 39 | 40 | this.db.Games.Add(game); 41 | this.db.SaveChanges(); 42 | } 43 | 44 | public void Update( 45 | int id, 46 | string title, 47 | string description, 48 | string thumbnailUrl, 49 | decimal price, 50 | double size, 51 | string videoId, 52 | DateTime releaseDate) 53 | { 54 | var game = this.db.Games.Find(id); 55 | 56 | game.Title = title; 57 | game.Description = description; 58 | game.ThumbnailUrl = thumbnailUrl; 59 | game.Price = price; 60 | game.Size = size; 61 | game.VideoId = videoId; 62 | game.ReleaseDate = releaseDate; 63 | 64 | this.db.SaveChanges(); 65 | } 66 | 67 | public void Delete(int id) 68 | { 69 | var game = this.db.Games.Find(id); 70 | this.db.Games.Remove(game); 71 | 72 | this.db.SaveChanges(); 73 | } 74 | 75 | public Game GetById(int id) 76 | => this.db.Games.Find(id); 77 | 78 | public bool Exists(int id) 79 | => this.db.Games.Any(g => g.Id == id); 80 | 81 | public IEnumerable ByIds(IEnumerable ids) 82 | => this.db 83 | .Games 84 | .Where(g => ids.Contains(g.Id)) 85 | .ProjectTo() 86 | .ToList(); 87 | 88 | public IEnumerable All(int? userId = null) 89 | { 90 | var query = this.db.Games.AsQueryable(); 91 | 92 | if (userId != null) 93 | { 94 | query = query.Where(g => g.Orders.Any(o => o.UserId == userId)); 95 | } 96 | 97 | return query 98 | .ProjectTo() 99 | .ToList(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /GameStore.App/Services/OrderService.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Services 2 | { 3 | using Contracts; 4 | using Data; 5 | using Data.Models; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | public class OrderService : IOrderService 10 | { 11 | private readonly GameStoreDbContext db; 12 | 13 | public OrderService(GameStoreDbContext db) 14 | { 15 | this.db = db; 16 | } 17 | 18 | public void Purchase(int userId, IEnumerable gameIds) 19 | { 20 | var alreadyOwnedIds = this.db 21 | .Orders 22 | .Where(o => o.UserId == userId 23 | && gameIds.Contains(o.GameId)) 24 | .Select(o => o.GameId) 25 | .ToList(); 26 | 27 | var newGamesIds = new List(gameIds); 28 | 29 | foreach (var gameId in alreadyOwnedIds) 30 | { 31 | newGamesIds.Remove(gameId); 32 | } 33 | 34 | foreach (var newGameId in newGamesIds) 35 | { 36 | var order = new Order 37 | { 38 | GameId = newGameId, 39 | UserId = userId 40 | }; 41 | 42 | db.Orders.Add(order); 43 | } 44 | 45 | db.SaveChanges(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GameStore.App/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace GameStore.App.Services 2 | { 3 | using Contracts; 4 | using Data; 5 | using Data.Models; 6 | using System.Linq; 7 | 8 | public class UserService : IUserService 9 | { 10 | private readonly GameStoreDbContext db; 11 | 12 | public UserService(GameStoreDbContext db) 13 | { 14 | this.db = db; 15 | } 16 | 17 | public bool Create(string email, string password, string name) 18 | { 19 | if (this.db.Users.Any(u => u.Email == email)) 20 | { 21 | return false; 22 | } 23 | 24 | var isAdmin = !db.Users.Any(); 25 | 26 | var user = new User 27 | { 28 | Email = email, 29 | Name = name, 30 | Password = password, 31 | IsAdmin = isAdmin 32 | }; 33 | 34 | db.Add(user); 35 | db.SaveChanges(); 36 | 37 | return true; 38 | } 39 | 40 | public bool UserExists(string email, string password) 41 | => this.db 42 | .Users 43 | .Any(u => u.Email == email && u.Password == password); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /GameStore.App/Views/Admin/Add.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 |
68 | -------------------------------------------------------------------------------- /GameStore.App/Views/Admin/All.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 |
-------------------------------------------------------------------------------- /GameStore.App/Views/Admin/Delete.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |

Delete 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 |
68 | -------------------------------------------------------------------------------- /GameStore.App/Views/Admin/Edit.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |

Edit 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 |
68 | -------------------------------------------------------------------------------- /GameStore.App/Views/Home/Index.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |

SoftUni Store

6 | 7 |
8 | Filter: 9 | 10 | 11 |
12 | 13 | {{{games}}} 14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /GameStore.App/Views/Layout.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Game Store 5 | 6 | 7 | 8 | 9 | 10 |
11 | 58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 | {{{error}}} 66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 |
74 | {{{success}}} 75 |
76 |
77 |
78 | 79 | {{{content}}} 80 | 81 |
82 |
83 |

© 2017 - Software University Foundation

84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /GameStore.App/Views/Orders/Cart.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |

Your Cart

7 |
8 |
9 | 10 |
11 | 12 | {{{games}}} 13 | 14 |
15 |
16 |

Total Price - {{{total-price}}} €

17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /GameStore.App/Views/Users/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 |
25 |
-------------------------------------------------------------------------------- /GameStore.App/Views/Users/Register.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |
5 |
6 |

Register

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 | 33 |
34 |
35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GameStoreSimpleMvc 2 | Simple MVC application built from scratch as an exercise for https://softuni.bg/trainings/1736/c-sharp-web-development-basics-september-2017. Enjoy! :) 3 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/ActionResults/NotFoundResult.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.ActionResults 2 | { 3 | using Contracts; 4 | using WebServer.Http.Contracts; 5 | using WebServer.Http.Response; 6 | 7 | public class NotFoundResult : IActionResult 8 | { 9 | public IHttpResponse Invoke() 10 | => new NotFoundResponse(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/ActionResults/RedirectResult.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.ActionResults 2 | { 3 | using Contracts; 4 | using WebServer.Http.Contracts; 5 | using WebServer.Http.Response; 6 | 7 | public class RedirectResult : IRedirectable 8 | { 9 | public RedirectResult(string redirectUrl) 10 | { 11 | this.RedirectUrl = redirectUrl; 12 | } 13 | 14 | public string RedirectUrl { get; set; } 15 | 16 | public IHttpResponse Invoke() => new RedirectResponse(this.RedirectUrl); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/ActionResults/ViewResult.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.ActionResults 2 | { 3 | using Contracts; 4 | using WebServer.Enums; 5 | using WebServer.Http.Contracts; 6 | using WebServer.Http.Response; 7 | 8 | public class ViewResult : IViewable 9 | { 10 | public ViewResult(IRenderable view) 11 | { 12 | this.View = view; 13 | } 14 | 15 | public IRenderable View { get; set; } 16 | 17 | public IHttpResponse Invoke() 18 | => new ContentResponse( 19 | HttpStatusCode.Ok, 20 | this.View.Render()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Attributes/Methods/HttpGetAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Attributes.Methods 2 | { 3 | public class HttpGetAttribute : HttpMethodAttribute 4 | { 5 | public override bool IsValid(string requestMethod) 6 | => requestMethod.ToUpper() == "GET"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Attributes/Methods/HttpMethodAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Attributes.Methods 2 | { 3 | using System; 4 | 5 | public abstract class HttpMethodAttribute : Attribute 6 | { 7 | public abstract bool IsValid(string requestMethod); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Attributes/Methods/HttpPostAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Attributes.Methods 2 | { 3 | public class HttpPostAttribute : HttpMethodAttribute 4 | { 5 | public override bool IsValid(string requestMethod) 6 | => requestMethod.ToUpper() == "POST"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Attributes/Validation/NumberRangeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Attributes.Validation 2 | { 3 | public class NumberRangeAttribute : PropertyValidationAttribute 4 | { 5 | private readonly double minimum; 6 | private readonly double maximum; 7 | 8 | public NumberRangeAttribute(double minimum, double maximum) 9 | { 10 | this.minimum = minimum; 11 | this.maximum = maximum; 12 | } 13 | 14 | public override bool IsValid(object value) 15 | { 16 | var valueAsDouble = value as double?; 17 | if (valueAsDouble == null) 18 | { 19 | return true; 20 | } 21 | 22 | return minimum <= valueAsDouble && valueAsDouble <= maximum; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Attributes/Validation/PropertyValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Attributes.Validation 2 | { 3 | using System; 4 | 5 | [AttributeUsage(AttributeTargets.Property, Inherited = true)] 6 | public abstract class PropertyValidationAttribute : Attribute 7 | { 8 | public abstract bool IsValid(object value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Attributes/Validation/RegexAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Attributes.Validation 2 | { 3 | using System.Text.RegularExpressions; 4 | 5 | public class RegexAttribute : PropertyValidationAttribute 6 | { 7 | private readonly string pattern; 8 | 9 | public RegexAttribute(string pattern) 10 | { 11 | this.pattern = $"^{pattern}$"; 12 | } 13 | 14 | public override bool IsValid(object value) 15 | { 16 | var valueAsString = value as string; 17 | if (valueAsString == null) 18 | { 19 | return true; 20 | } 21 | 22 | return Regex.IsMatch(valueAsString, this.pattern); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Contracts/IActionResult.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Contracts 2 | { 3 | using WebServer.Http.Contracts; 4 | 5 | public interface IActionResult 6 | { 7 | IHttpResponse Invoke(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Contracts/IRedirectable.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Contracts 2 | { 3 | public interface IRedirectable : IActionResult 4 | { 5 | string RedirectUrl { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Contracts/IRenderable.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Contracts 2 | { 3 | public interface IRenderable 4 | { 5 | string Render(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Contracts/IViewable.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Contracts 2 | { 3 | public interface IViewable : IActionResult 4 | { 5 | IRenderable View { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Controllers/Controller.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Controllers 2 | { 3 | using ActionResults; 4 | using Attributes.Validation; 5 | using Contracts; 6 | using Helpers; 7 | using Models; 8 | using Security; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Runtime.CompilerServices; 12 | using ViewEngine; 13 | using WebServer.Http; 14 | using WebServer.Http.Contracts; 15 | 16 | public abstract class Controller 17 | { 18 | public Controller() 19 | { 20 | this.ViewModel = new ViewModel(); 21 | this.User = new Authentication(); 22 | } 23 | 24 | protected ViewModel ViewModel { get; private set; } 25 | 26 | protected internal IHttpRequest Request { get; internal set; } 27 | 28 | protected internal Authentication User { get; private set; } 29 | 30 | protected IViewable View([CallerMemberName]string caller = "") 31 | { 32 | var controllerName = ControllerHelpers.GetControllerName(this); 33 | 34 | var viewFullQualifiedName = ControllerHelpers 35 | .GetViewFullQualifiedName(controllerName, caller); 36 | 37 | var view = new View(viewFullQualifiedName, this.ViewModel.Data); 38 | 39 | return new ViewResult(view); 40 | } 41 | 42 | protected IRedirectable Redirect(string redirectUrl) 43 | => new RedirectResult(redirectUrl); 44 | 45 | protected IActionResult NotFound() 46 | => new NotFoundResult(); 47 | 48 | protected bool IsValidModel(object model) 49 | { 50 | var properties = model.GetType().GetProperties(); 51 | 52 | foreach (var property in properties) 53 | { 54 | var attributes = property 55 | .GetCustomAttributes() 56 | .Where(a => a is PropertyValidationAttribute) 57 | .Cast(); 58 | 59 | foreach (var attribute in attributes) 60 | { 61 | var propertyValue = property.GetValue(model); 62 | 63 | if (!attribute.IsValid(propertyValue)) 64 | { 65 | return false; 66 | } 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | 73 | protected void SignIn(string name) 74 | { 75 | this.Request.Session.Add(SessionStore.CurrentUserKey, name); 76 | } 77 | 78 | protected void SignOut() 79 | { 80 | this.Request.Session.Remove(SessionStore.CurrentUserKey); 81 | } 82 | 83 | protected internal virtual void InitializeController() 84 | { 85 | var user = this.Request 86 | .Session 87 | .Get(SessionStore.CurrentUserKey); 88 | 89 | if (user != null) 90 | { 91 | this.User = new Authentication(user); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Errors/Error.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Simple MVC Framework 7 | 8 | 9 | Ooops! There is a problem with your request! 10 | 11 | {{{error}}} 12 | 13 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Helpers/ControllerHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Helpers 2 | { 3 | public static class ControllerHelpers 4 | { 5 | public static string GetControllerName(object controller) 6 | => controller.GetType() 7 | .Name 8 | .Replace(MvcContext.Get.ControllerSuffix, string.Empty); 9 | 10 | public static string GetViewFullQualifiedName( 11 | string controller, 12 | string action) 13 | => string.Format( 14 | "{0}\\{1}\\{2}", 15 | MvcContext.Get.ViewsFolder, 16 | controller, 17 | action); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Helpers/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Helpers 2 | { 3 | using System.Linq; 4 | 5 | public static class StringExtensions 6 | { 7 | public static string Capitalize(this string input) 8 | { 9 | if (input == null || input.Length == 0) 10 | { 11 | return input; 12 | } 13 | 14 | var firstLetter = char.ToUpper(input.First()); 15 | var rest = input.Substring(1); 16 | 17 | return $"{firstLetter}{rest}"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Models/ViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Models 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class ViewModel 6 | { 7 | public ViewModel() 8 | { 9 | this.Data = new Dictionary(); 10 | } 11 | 12 | public IDictionary Data { get; private set; } 13 | 14 | public string this[string key] 15 | { 16 | get => this.Data[key]; 17 | set => this.Data[key] = value; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/MvcContext.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework 2 | { 3 | public class MvcContext 4 | { 5 | private static MvcContext instance; 6 | 7 | private MvcContext() { } 8 | 9 | public static MvcContext Get 10 | => instance == null ? (instance = new MvcContext()) : instance; 11 | 12 | public string AssemblyName { get; set; } 13 | 14 | public string ControllersFolder { get; set; } = "Controllers"; 15 | 16 | public string ControllerSuffix { get; set; } = "Controller"; 17 | 18 | public string ViewsFolder { get; set; } = "Views"; 19 | 20 | public string ModelsFolder { get; set; } = "Models"; 21 | 22 | public string ResourcesFolder { get; set; } = "Resources"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/MvcEngine.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework 2 | { 3 | using System; 4 | using System.Reflection; 5 | using WebServer; 6 | 7 | public static class MvcEngine 8 | { 9 | public static void Run(WebServer server) 10 | { 11 | RegisterAssemblyName(); 12 | 13 | try 14 | { 15 | server.Run(); 16 | } 17 | catch (Exception ex) 18 | { 19 | Console.WriteLine(ex.Message); 20 | } 21 | } 22 | 23 | private static void RegisterAssemblyName() 24 | { 25 | MvcContext.Get.AssemblyName = Assembly 26 | .GetEntryAssembly() 27 | .GetName() 28 | .Name; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Routers/ControllerRouter.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Routers 2 | { 3 | using Framework.Attributes.Methods; 4 | using Framework.Helpers; 5 | using SimpleMvc.Framework.Contracts; 6 | using SimpleMvc.Framework.Controllers; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Reflection; 11 | using WebServer.Contracts; 12 | using WebServer.Exceptions; 13 | using WebServer.Http.Contracts; 14 | using WebServer.Http.Response; 15 | 16 | public class ControllerRouter : IHandleable 17 | { 18 | private IDictionary getParameters; 19 | private IDictionary postParameters; 20 | private string requestMethod; 21 | private Controller controllerInstance; 22 | private string controllerName; 23 | private string actionName; 24 | private object[] methodParameters; 25 | 26 | public IHttpResponse Handle(IHttpRequest request) 27 | { 28 | this.controllerInstance = null; 29 | this.actionName = null; 30 | this.methodParameters = null; 31 | 32 | this.getParameters = new Dictionary(request.UrlParameters); 33 | this.postParameters = new Dictionary(request.FormData); 34 | this.requestMethod = request.Method.ToString().ToUpper(); 35 | 36 | this.PrepareControllerAndActionNames(request); 37 | 38 | var methodInfo = this.GetActionForExecution(); 39 | 40 | if (methodInfo == null) 41 | { 42 | return new NotFoundResponse(); 43 | } 44 | 45 | this.PrepareMethodParameters(methodInfo); 46 | 47 | try 48 | { 49 | if (this.controllerInstance != null) 50 | { 51 | this.controllerInstance.Request = request; 52 | this.controllerInstance.InitializeController(); 53 | } 54 | 55 | return this.GetResponse(methodInfo, this.controllerInstance); 56 | } 57 | catch (Exception ex) 58 | { 59 | return new InternalServerErrorResponse(ex); 60 | } 61 | } 62 | 63 | protected virtual Controller CreateController(Type controllerType) 64 | => Activator.CreateInstance(controllerType) as Controller; 65 | 66 | private void PrepareControllerAndActionNames(IHttpRequest request) 67 | { 68 | var pathParts = request.Path.Split( 69 | new[] { '/', '?' }, 70 | StringSplitOptions.RemoveEmptyEntries); 71 | 72 | if (pathParts.Length < 2) 73 | { 74 | if (request.Path == "/") 75 | { 76 | this.controllerName = "HomeController"; 77 | this.actionName = "Index"; 78 | 79 | return; 80 | } 81 | else 82 | { 83 | BadRequestException.ThrowFromInvalidRequest(); 84 | } 85 | } 86 | 87 | this.controllerName = $"{pathParts[0].Capitalize()}{MvcContext.Get.ControllerSuffix}"; 88 | this.actionName = pathParts[1].Capitalize(); 89 | } 90 | 91 | private MethodInfo GetActionForExecution() 92 | { 93 | foreach (var method in this.GetSuitableMethods()) 94 | { 95 | var httpMethodAttributes = method 96 | .GetCustomAttributes() 97 | .Where(a => a is HttpMethodAttribute) 98 | .Cast(); 99 | 100 | if (!httpMethodAttributes.Any() && this.requestMethod == "GET") 101 | { 102 | return method; 103 | } 104 | 105 | foreach (var httpMethodAttribute in httpMethodAttributes) 106 | { 107 | if (httpMethodAttribute.IsValid(this.requestMethod)) 108 | { 109 | return method; 110 | } 111 | } 112 | } 113 | 114 | return null; 115 | } 116 | 117 | private IEnumerable GetSuitableMethods() 118 | { 119 | var controller = this.GetControllerInstance(); 120 | 121 | if (controller == null) 122 | { 123 | return new MethodInfo[0]; 124 | } 125 | 126 | return controller 127 | .GetType() 128 | .GetMethods() 129 | .Where(m => m.Name.ToLower() == actionName.ToLower()); 130 | } 131 | 132 | private object GetControllerInstance() 133 | { 134 | if (this.controllerInstance != null) 135 | { 136 | return controllerInstance; 137 | } 138 | 139 | var controllerFullQualifiedName = string.Format( 140 | "{0}.{1}.{2}, {0}", 141 | MvcContext.Get.AssemblyName, 142 | MvcContext.Get.ControllersFolder, 143 | this.controllerName); 144 | 145 | var controllerType = Type.GetType(controllerFullQualifiedName); 146 | 147 | if (controllerType == null) 148 | { 149 | return null; 150 | } 151 | 152 | this.controllerInstance = this.CreateController(controllerType); 153 | return this.controllerInstance; 154 | } 155 | 156 | private void PrepareMethodParameters(MethodInfo methodInfo) 157 | { 158 | var parameters = methodInfo.GetParameters(); 159 | 160 | this.methodParameters = new object[parameters.Length]; 161 | 162 | for (var i = 0; i < parameters.Length; i++) 163 | { 164 | var parameter = parameters[i]; 165 | 166 | if (parameter.ParameterType.IsPrimitive 167 | || parameter.ParameterType == typeof(string)) 168 | { 169 | this.ProcessPrimitiveParameter(parameter, i); 170 | } 171 | else 172 | { 173 | this.ProcessModelParameter(parameter, i); 174 | } 175 | } 176 | } 177 | 178 | private void ProcessPrimitiveParameter(ParameterInfo parameter, int index) 179 | { 180 | var getParameterValue = this.getParameters[parameter.Name]; 181 | 182 | var value = Convert.ChangeType( 183 | getParameterValue, 184 | parameter.ParameterType); 185 | 186 | this.methodParameters[index] = value; 187 | } 188 | 189 | private void ProcessModelParameter(ParameterInfo parameter, int index) 190 | { 191 | var modelType = parameter.ParameterType; 192 | var modelInstance = Activator.CreateInstance(modelType); 193 | 194 | var modelProperties = modelType.GetProperties(); 195 | 196 | foreach (var modelProperty in modelProperties) 197 | { 198 | var postParameterValue = this.postParameters[modelProperty.Name]; 199 | 200 | var value = Convert.ChangeType( 201 | postParameterValue, 202 | modelProperty.PropertyType); 203 | 204 | modelProperty.SetValue( 205 | modelInstance, 206 | value); 207 | } 208 | 209 | this.methodParameters[index] = Convert.ChangeType( 210 | modelInstance, 211 | modelType); 212 | } 213 | 214 | private IHttpResponse GetResponse(MethodInfo method, object controller) 215 | { 216 | var actionResult = method.Invoke(controller, this.methodParameters) 217 | as IActionResult; 218 | 219 | if (actionResult == null) 220 | { 221 | var methodResultAsHttpResponse = actionResult as IHttpResponse; 222 | 223 | if (methodResultAsHttpResponse != null) 224 | { 225 | return methodResultAsHttpResponse; 226 | } 227 | else 228 | { 229 | throw new InvalidOperationException("Controller actions should return either IActionResult or IHttpResponse."); 230 | } 231 | } 232 | 233 | return actionResult.Invoke(); 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Routers/ResourceRouter.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Routers 2 | { 3 | using System.IO; 4 | using System.Linq; 5 | using WebServer.Contracts; 6 | using WebServer.Enums; 7 | using WebServer.Http.Contracts; 8 | using WebServer.Http.Response; 9 | 10 | public class ResourceRouter : IHandleable 11 | { 12 | public IHttpResponse Handle(IHttpRequest request) 13 | { 14 | var fileName = request.Path.Split('/').Last(); 15 | var fileExtension = fileName.Split('.').Last(); 16 | 17 | try 18 | { 19 | var fileContents = this.ReadFile(fileName, fileExtension); 20 | 21 | return new FileResponse(HttpStatusCode.Found, fileContents); 22 | } 23 | catch 24 | { 25 | return new NotFoundResponse(); 26 | } 27 | } 28 | 29 | private byte[] ReadFile(string fileName, string fileExtension) 30 | => File.ReadAllBytes(string.Format("{0}\\{1}\\{2}", 31 | MvcContext.Get.ResourcesFolder, 32 | fileExtension, 33 | fileName)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/Security/Authentication.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.Security 2 | { 3 | public class Authentication 4 | { 5 | internal Authentication() 6 | { 7 | this.IsAuthenticated = false; 8 | } 9 | 10 | internal Authentication(string name) 11 | { 12 | this.IsAuthenticated = true; 13 | this.Name = name; 14 | } 15 | 16 | public bool IsAuthenticated { get; private set; } 17 | 18 | public string Name { get; private set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/SimpleMvc.Framework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SimpleMvc.Framework/ViewEngine/View.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleMvc.Framework.ViewEngine 2 | { 3 | using Contracts; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.IO; 7 | 8 | public class View : IRenderable 9 | { 10 | public const string BaseLayoutFileName = "Layout"; 11 | 12 | public const string ContentPlaceholder = "{{{content}}}"; 13 | 14 | public const string FileExtension = ".html"; 15 | 16 | public const string LocalErrorPath = "\\SimpleMvc.Framework\\Errors\\Error.html"; 17 | 18 | private readonly string templateFullQualifiedName; 19 | 20 | private readonly IDictionary viewData; 21 | 22 | public View(string templateFullQualifiedName, IDictionary viewData) 23 | { 24 | this.templateFullQualifiedName = templateFullQualifiedName; 25 | this.viewData = viewData; 26 | } 27 | 28 | public string Render() 29 | { 30 | var fileHtml = this.ReadFile(); 31 | 32 | if (this.viewData.Any()) 33 | { 34 | foreach (var data in this.viewData) 35 | { 36 | fileHtml = fileHtml.Replace($"{{{{{{{data.Key}}}}}}}", data.Value); 37 | } 38 | } 39 | 40 | return fileHtml; 41 | } 42 | 43 | private string ReadFile() 44 | { 45 | var layoutHtml = this.ReadLayoutFile(); 46 | 47 | var templateFullFilePath = $"{this.templateFullQualifiedName}{FileExtension}"; 48 | 49 | if (!File.Exists(templateFullFilePath)) 50 | { 51 | this.viewData["error"] = $"The requested view ({templateFullFilePath}) could not be found!"; 52 | return this.GetErrorHtml(); 53 | } 54 | 55 | var templateHtml = File.ReadAllText(templateFullFilePath); 56 | 57 | return layoutHtml.Replace(ContentPlaceholder, templateHtml); 58 | } 59 | 60 | private string ReadLayoutFile() 61 | { 62 | var layoutHtmlFile = string.Format( 63 | "{0}\\{1}{2}", 64 | MvcContext.Get.ViewsFolder, 65 | BaseLayoutFileName, 66 | FileExtension); 67 | 68 | if (!File.Exists(layoutHtmlFile)) 69 | { 70 | this.viewData["error"] = $"Layout view ({layoutHtmlFile}) could not be found!"; 71 | return this.GetErrorHtml(); 72 | } 73 | 74 | return File.ReadAllText(layoutHtmlFile); 75 | } 76 | 77 | private string GetErrorPath() 78 | { 79 | var currentDirectory = Directory.GetCurrentDirectory(); 80 | var parentDirectory = Directory.GetParent(currentDirectory); 81 | var parentDirectoryPath = parentDirectory.FullName; 82 | 83 | return $"{parentDirectoryPath}{LocalErrorPath}"; 84 | } 85 | 86 | private string GetErrorHtml() 87 | { 88 | var errorPath = this.GetErrorPath(); 89 | var errorHtml = File.ReadAllText(errorPath); 90 | return errorHtml; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /SimpleMvc.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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleMvc.Framework", "SimpleMvc.Framework\SimpleMvc.Framework.csproj", "{CC2E0567-0B1E-46D6-B06D-BE662260D137}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebServer", "WebServer\WebServer.csproj", "{537B17DC-C5A8-4F13-9D20-A2B15F069B14}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameStore.App", "GameStore.App\GameStore.App.csproj", "{C3819F45-2517-4AF9-877C-31C3683E3DF2}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {3ABA898C-743A-4BCB-94FE-871C34896B4B} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /WebServer/Common/CoreValidator.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Common/InternalServerErrorView.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Common 2 | { 3 | using System; 4 | using Contracts; 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 | -------------------------------------------------------------------------------- /WebServer/Common/NotFoundView.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Common 2 | { 3 | using Contracts; 4 | 5 | public class NotFoundView : IView 6 | { 7 | public string View() 8 | { 9 | return "

404 This page or resource you are trying to access does not exist :/

"; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /WebServer/ConnectionHandler.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer 2 | { 3 | using Common; 4 | using Http; 5 | using Http.Contracts; 6 | using System; 7 | using System.Net.Sockets; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Contracts; 11 | using System.Linq; 12 | using global::WebServer.Http.Response; 13 | 14 | public class ConnectionHandler 15 | { 16 | private readonly Socket client; 17 | private readonly IHandleable mvcRequestHandler; 18 | private readonly IHandleable resourceHandler; 19 | 20 | public ConnectionHandler( 21 | Socket client, 22 | IHandleable mvcRequestHandler, 23 | IHandleable resourceHandler) 24 | { 25 | CoreValidator.ThrowIfNull(client, nameof(client)); 26 | CoreValidator.ThrowIfNull(mvcRequestHandler, nameof(mvcRequestHandler)); 27 | CoreValidator.ThrowIfNull(resourceHandler, nameof(resourceHandler)); 28 | 29 | this.client = client; 30 | this.mvcRequestHandler = mvcRequestHandler; 31 | this.resourceHandler = resourceHandler; 32 | } 33 | 34 | public async Task ProcessRequestAsync() 35 | { 36 | var httpRequest = this.ReadRequest(); 37 | 38 | if (httpRequest != null) 39 | { 40 | var httpResponse = this.HandleRequest(httpRequest); 41 | 42 | var responseBytes = this.GetResponseBytes(httpResponse); 43 | 44 | var byteSegments = new ArraySegment(responseBytes); 45 | 46 | await this.client.SendAsync(byteSegments, SocketFlags.None); 47 | 48 | Console.WriteLine($"-----REQUEST-----"); 49 | Console.WriteLine(httpRequest); 50 | Console.WriteLine($"-----RESPONSE-----"); 51 | Console.WriteLine(httpResponse); 52 | Console.WriteLine(); 53 | } 54 | 55 | this.client.Shutdown(SocketShutdown.Both); 56 | } 57 | 58 | private IHttpRequest ReadRequest() 59 | { 60 | var result = new StringBuilder(); 61 | 62 | var data = new ArraySegment(new byte[1024]); 63 | 64 | while (true) 65 | { 66 | int numberOfBytesRead = this.client.Receive(data.Array, SocketFlags.None); 67 | 68 | if (numberOfBytesRead == 0) 69 | { 70 | break; 71 | } 72 | 73 | var bytesAsString = Encoding.UTF8.GetString(data.Array, 0, numberOfBytesRead); 74 | 75 | result.Append(bytesAsString); 76 | 77 | if (numberOfBytesRead < 1023) 78 | { 79 | break; 80 | } 81 | } 82 | 83 | if (result.Length == 0) 84 | { 85 | return null; 86 | } 87 | 88 | return new HttpRequest(result.ToString()); 89 | } 90 | 91 | private IHttpResponse HandleRequest(IHttpRequest httpRequest) 92 | { 93 | if (httpRequest.Path.Contains(".")) 94 | { 95 | return this.resourceHandler.Handle(httpRequest); 96 | } 97 | else 98 | { 99 | string sessionIdToSend = this.SetRequestSession(httpRequest); 100 | var response = this.mvcRequestHandler.Handle(httpRequest); 101 | this.SetResponseSession(response, sessionIdToSend); 102 | return response; 103 | } 104 | } 105 | 106 | private string SetRequestSession(IHttpRequest request) 107 | { 108 | if (!request.Cookies.ContainsKey(SessionStore.SessionCookieKey)) 109 | { 110 | var sessionId = Guid.NewGuid().ToString(); 111 | 112 | request.Session = SessionStore.Get(sessionId); 113 | 114 | return sessionId; 115 | } 116 | 117 | return null; 118 | } 119 | 120 | private void SetResponseSession(IHttpResponse response, string sessionIdToSend) 121 | { 122 | if (sessionIdToSend != null) 123 | { 124 | response.Headers.Add( 125 | HttpHeader.SetCookie, 126 | $"{SessionStore.SessionCookieKey}={sessionIdToSend}; HttpOnly; path=/"); 127 | } 128 | } 129 | 130 | 131 | private byte[] GetResponseBytes(IHttpResponse httpResponse) 132 | { 133 | var responseBytes = Encoding.UTF8 134 | .GetBytes(httpResponse.ToString()) 135 | .ToList(); 136 | 137 | var fileResponse = httpResponse as FileResponse; 138 | if (fileResponse != null) 139 | { 140 | responseBytes.AddRange(fileResponse.FileData); 141 | } 142 | 143 | return responseBytes.ToArray(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /WebServer/Contracts/IHandleable.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Contracts 2 | { 3 | using Http.Contracts; 4 | 5 | public interface IHandleable 6 | { 7 | IHttpResponse Handle(IHttpRequest request); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WebServer/Contracts/IRunnable.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Contracts 2 | { 3 | public interface IRunnable 4 | { 5 | void Run(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /WebServer/Contracts/IView.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Contracts 2 | { 3 | public interface IView 4 | { 5 | string View(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /WebServer/Enums/HttpRequestMethod.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Enums 2 | { 3 | public enum HttpRequestMethod 4 | { 5 | Get, 6 | Post 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /WebServer/Enums/HttpStatusCode.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Exceptions/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Exceptions/InvalidResponseException.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Contracts/IHttpCookieCollection.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Contracts/IHttpHeaderCollection.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Contracts/IHttpRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Contracts/IHttpResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Contracts/IHttpSession.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 Remove(string key); 16 | 17 | void Clear(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WebServer/Http/HttpCookie.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Http 2 | { 3 | using 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 | -------------------------------------------------------------------------------- /WebServer/Http/HttpCookieCollection.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/HttpHeader.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Http 2 | { 3 | using 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 | public const string ContentLength = "Content-Length"; 13 | public const string ContentDisposition = "Content-Disposition"; 14 | 15 | public HttpHeader(string key, string value) 16 | { 17 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key)); 18 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value)); 19 | 20 | this.Key = key; 21 | this.Value = value; 22 | } 23 | 24 | public string Key { get; private set; } 25 | 26 | public string Value { get; private set; } 27 | 28 | public override string ToString() => $"{this.Key}: {this.Value}"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /WebServer/Http/HttpHeaderCollection.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/HttpRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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( 57 | new[] { Environment.NewLine }, 58 | StringSplitOptions.None); 59 | 60 | if (!requestLines.Any()) 61 | { 62 | BadRequestException.ThrowFromInvalidRequest(); 63 | } 64 | 65 | var requestLine = requestLines.First().Split( 66 | new[] { ' ' }, 67 | StringSplitOptions.RemoveEmptyEntries); 68 | 69 | if (requestLine.Length != 3 || requestLine[2].ToLower() != "http/1.1") 70 | { 71 | BadRequestException.ThrowFromInvalidRequest(); 72 | } 73 | 74 | this.Method = this.ParseMethod(requestLine.First()); 75 | this.Url = requestLine[1]; 76 | this.Path = this.ParsePath(this.Url); 77 | 78 | this.ParseHeaders(requestLines); 79 | this.ParseCookies(); 80 | this.ParseParameters(); 81 | this.ParseFormData(requestLines.Last()); 82 | 83 | this.SetSession(); 84 | } 85 | 86 | private HttpRequestMethod ParseMethod(string method) 87 | { 88 | HttpRequestMethod parsedMethod; 89 | 90 | if (!Enum.TryParse(method, true, out parsedMethod)) 91 | { 92 | BadRequestException.ThrowFromInvalidRequest(); 93 | } 94 | 95 | return parsedMethod; 96 | } 97 | 98 | private string ParsePath(string url) 99 | => url.Split(new[] { '?', '#' }, StringSplitOptions.RemoveEmptyEntries)[0]; 100 | 101 | private void ParseHeaders(string[] requestLines) 102 | { 103 | var emptyLineAfterHeadersIndex = Array.IndexOf(requestLines, string.Empty); 104 | 105 | for (int i = 1; i < emptyLineAfterHeadersIndex; i++) 106 | { 107 | var currentLine = requestLines[i]; 108 | var headerParts = currentLine.Split(new[] { ": " }, StringSplitOptions.RemoveEmptyEntries); 109 | 110 | if (headerParts.Length != 2) 111 | { 112 | BadRequestException.ThrowFromInvalidRequest(); 113 | } 114 | 115 | var headerKey = headerParts[0]; 116 | var headerValue = headerParts[1].Trim(); 117 | 118 | var header = new HttpHeader(headerKey, headerValue); 119 | 120 | this.Headers.Add(header); 121 | } 122 | 123 | if (!this.Headers.ContainsKey(HttpHeader.Host)) 124 | { 125 | BadRequestException.ThrowFromInvalidRequest(); 126 | } 127 | } 128 | 129 | private void ParseCookies() 130 | { 131 | if (this.Headers.ContainsKey(HttpHeader.Cookie)) 132 | { 133 | var allCookies = this.Headers.Get(HttpHeader.Cookie); 134 | 135 | foreach (var cookie in allCookies) 136 | { 137 | if (!cookie.Value.Contains('=')) 138 | { 139 | return; 140 | } 141 | 142 | var cookieParts = cookie 143 | .Value 144 | .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) 145 | .ToList(); 146 | 147 | if (!cookieParts.Any()) 148 | { 149 | continue; 150 | } 151 | 152 | foreach (var cookiePart in cookieParts) 153 | { 154 | var cookieKeyValuePair = cookiePart 155 | .Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); 156 | 157 | if (cookieKeyValuePair.Length == 2) 158 | { 159 | var key = cookieKeyValuePair[0].Trim(); 160 | var value = cookieKeyValuePair[1].Trim(); 161 | 162 | this.Cookies.Add(new HttpCookie(key, value, false)); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | private void ParseParameters() 170 | { 171 | if (!this.Url.Contains('?')) 172 | { 173 | return; 174 | } 175 | 176 | var query = this.Url 177 | .Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries) 178 | .Last(); 179 | 180 | this.ParseQuery(query, this.UrlParameters); 181 | } 182 | 183 | private void ParseFormData(string formDataLine) 184 | { 185 | if (this.Method == HttpRequestMethod.Get) 186 | { 187 | return; 188 | } 189 | 190 | this.ParseQuery(formDataLine, this.FormData); 191 | } 192 | 193 | private void ParseQuery(string query, IDictionary dict) 194 | { 195 | if (!query.Contains('=')) 196 | { 197 | return; 198 | } 199 | 200 | var queryPairs = query.Split(new[] { '&' }); 201 | 202 | foreach (var queryPair in queryPairs) 203 | { 204 | var queryKvp = queryPair.Split(new[] { '=' }); 205 | 206 | if (queryKvp.Length != 2) 207 | { 208 | return; 209 | } 210 | 211 | var queryKey = WebUtility.UrlDecode(queryKvp[0]); 212 | var queryValue = WebUtility.UrlDecode(queryKvp[1]); 213 | 214 | dict.Add(queryKey, queryValue); 215 | } 216 | } 217 | 218 | private void SetSession() 219 | { 220 | if (this.Cookies.ContainsKey(SessionStore.SessionCookieKey)) 221 | { 222 | var cookie = this.Cookies.Get(SessionStore.SessionCookieKey); 223 | var sessionId = cookie.Value; 224 | 225 | this.Session = SessionStore.Get(sessionId); 226 | } 227 | } 228 | 229 | public override string ToString() => this.requestText; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /WebServer/Http/HttpSession.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 Remove(string key) 30 | { 31 | CoreValidator.ThrowIfNull(key, nameof(key)); 32 | 33 | if (this.values.ContainsKey(key)) 34 | { 35 | this.values.Remove(key); 36 | } 37 | } 38 | 39 | public void Clear() => this.values.Clear(); 40 | 41 | public object Get(string key) 42 | { 43 | CoreValidator.ThrowIfNull(key, nameof(key)); 44 | 45 | if (!this.values.ContainsKey(key)) 46 | { 47 | return null; 48 | } 49 | 50 | return this.values[key]; 51 | } 52 | 53 | public T Get(string key) 54 | => (T)this.Get(key); 55 | 56 | public bool Contains(string key) => this.values.ContainsKey(key); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WebServer/Http/Response/BadRequestResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Response/ContentResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Http.Response 2 | { 3 | using Enums; 4 | using Exceptions; 5 | using global::WebServer.Contracts; 6 | 7 | public class ContentResponse : HttpResponse 8 | { 9 | private readonly string content; 10 | 11 | public ContentResponse(HttpStatusCode statusCode, string content) 12 | { 13 | this.ValidateStatusCode(statusCode); 14 | 15 | this.content = content; 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.content}"; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /WebServer/Http/Response/FileResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Http.Response 2 | { 3 | using Common; 4 | using Enums; 5 | using System; 6 | 7 | public class FileResponse : HttpResponse 8 | { 9 | public FileResponse(HttpStatusCode statusCode, byte[] fileContents) 10 | { 11 | CoreValidator.ThrowIfNull(fileContents, nameof(fileContents)); 12 | 13 | this.ValidateStatusCode(statusCode); 14 | 15 | this.StatusCode = statusCode; 16 | this.FileData = fileContents; 17 | 18 | this.Headers.Add(HttpHeader.ContentLength, fileContents.Length.ToString()); 19 | this.Headers.Add(HttpHeader.ContentDisposition, "attachment"); 20 | } 21 | 22 | private void ValidateStatusCode(HttpStatusCode statusCode) 23 | { 24 | var statusCodeNumber = (int)statusCode; 25 | 26 | if (statusCodeNumber <= 299 || statusCodeNumber >= 400) 27 | { 28 | throw new InvalidOperationException("File responses need to have status code between 300 and 399."); 29 | } 30 | } 31 | 32 | public byte[] FileData { get; private set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /WebServer/Http/Response/HttpResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/Response/InternalServerErrorResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Http.Response 2 | { 3 | using System; 4 | using Enums; 5 | using Common; 6 | 7 | public class InternalServerErrorResponse : ContentResponse 8 | { 9 | public InternalServerErrorResponse(Exception ex) 10 | : base(HttpStatusCode.InternalServerError, new InternalServerErrorView(ex).View()) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebServer/Http/Response/NotFoundResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.Http.Response 2 | { 3 | using Common; 4 | using Enums; 5 | 6 | public class NotFoundResponse : ContentResponse 7 | { 8 | public NotFoundResponse() 9 | : base(HttpStatusCode.NotFound, new NotFoundView().View()) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WebServer/Http/Response/RedirectResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/Http/SessionStore.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer.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 | -------------------------------------------------------------------------------- /WebServer/WebServer.cs: -------------------------------------------------------------------------------- 1 | namespace WebServer 2 | { 3 | using Contracts; 4 | using System; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Threading.Tasks; 8 | 9 | public class WebServer : IRunnable 10 | { 11 | private const string localHostIpAddress = "127.0.0.1"; 12 | 13 | private readonly int port; 14 | private readonly IHandleable mvcRequestHandler; 15 | private readonly IHandleable resourceHandler; 16 | private readonly TcpListener listener; 17 | private bool isRunning; 18 | 19 | public WebServer(int port, IHandleable mvcRequestHandler, IHandleable resourceHandler) 20 | { 21 | this.port = port; 22 | this.listener = new TcpListener(IPAddress.Parse(localHostIpAddress), port); 23 | 24 | this.mvcRequestHandler = mvcRequestHandler; 25 | this.resourceHandler = resourceHandler; 26 | } 27 | 28 | public void Run() 29 | { 30 | this.listener.Start(); 31 | this.isRunning = true; 32 | 33 | Console.WriteLine($"Server running on {localHostIpAddress}:{this.port}"); 34 | 35 | Task.Run(this.ListenLoop).Wait(); 36 | } 37 | 38 | private async Task ListenLoop() 39 | { 40 | while (this.isRunning) 41 | { 42 | var client = await this.listener.AcceptSocketAsync(); 43 | var connectionHandler = new ConnectionHandler(client, this.mvcRequestHandler, this.resourceHandler); 44 | await connectionHandler.ProcessRequestAsync(); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /WebServer/WebServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------