├── .gitignore ├── LICENSE ├── ModPanel.App ├── Controllers │ ├── AdminController.cs │ ├── BaseController.cs │ ├── HomeController.cs │ ├── PostsController.cs │ └── UsersController.cs ├── Data │ ├── Migrations │ │ ├── 20171026152454_UsersTable.Designer.cs │ │ ├── 20171026152454_UsersTable.cs │ │ ├── 20171026165138_PostsTable.Designer.cs │ │ ├── 20171026165138_PostsTable.cs │ │ ├── 20171026175957_LogsTable.Designer.cs │ │ ├── 20171026175957_LogsTable.cs │ │ ├── 20171026183416_PostsCreatedOnColumn.Designer.cs │ │ ├── 20171026183416_PostsCreatedOnColumn.cs │ │ └── ModPanelDbContextModelSnapshot.cs │ ├── ModPanelDbContext.cs │ └── Models │ │ ├── Log.cs │ │ ├── LogType.cs │ │ ├── PositionType.cs │ │ ├── Post.cs │ │ └── User.cs ├── Infrastructure │ ├── DependencyControllerRouter.cs │ ├── EnumExtensions.cs │ ├── HtmlHelpers.cs │ ├── Mapping │ │ ├── AutoMapperConfiguration.cs │ │ ├── IHaveCustomMapping.cs │ │ └── IMapFrom.cs │ ├── StringExtensions.cs │ └── Validation │ │ ├── Posts │ │ ├── ContentAttribute.cs │ │ └── TitleAttribute.cs │ │ ├── RequiredAttribute.cs │ │ └── Users │ │ ├── EmailAttribute.cs │ │ └── PasswordAttribute.cs ├── Launcher.cs ├── ModPanel.App.csproj ├── Models │ ├── Admin │ │ └── AdminUserModel.cs │ ├── Home │ │ └── HomeListingModel.cs │ ├── Logs │ │ └── LogModel.cs │ ├── Posts │ │ ├── PostListingModel.cs │ │ └── PostModel.cs │ └── Users │ │ ├── LoginModel.cs │ │ └── RegisterModel.cs ├── Services │ ├── Contracts │ │ ├── ILogService.cs │ │ ├── IPostService.cs │ │ └── IUserService.cs │ ├── LogService.cs │ ├── PostService.cs │ └── UserService.cs └── Views │ ├── Admin │ ├── Delete.html │ ├── Edit.html │ ├── Log.html │ ├── Posts.html │ └── Users.html │ ├── Home │ └── Index.html │ ├── Layout.html │ ├── Posts │ └── Create.html │ └── Users │ ├── Login.html │ └── Register.html ├── 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ModPanel.App/Controllers/AdminController.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Controllers 2 | { 3 | using Data.Models; 4 | using Infrastructure; 5 | using Models.Posts; 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 | private const string EditError = "

Check your form for errors.

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

Content must be more than 10 symbols (inclusive).

"; 14 | 15 | private readonly IUserService users; 16 | private readonly IPostService posts; 17 | private readonly ILogService logs; 18 | 19 | public AdminController( 20 | IUserService users, 21 | IPostService posts, 22 | ILogService logs) 23 | { 24 | this.users = users; 25 | this.posts = posts; 26 | this.logs = logs; 27 | } 28 | 29 | public IActionResult Users() 30 | { 31 | if (!this.IsAdmin) 32 | { 33 | return this.RedirectToLogin(); 34 | } 35 | 36 | var rows = this.users 37 | .All() 38 | .Select(u => $@" 39 | 40 | {u.Id} 41 | {u.Email} 42 | {u.Position.ToFriendlyName()} 43 | {u.Posts} 44 | 45 | {(u.IsApproved ? string.Empty : $@"Approve")} 46 | 47 | "); 48 | 49 | this.ViewModel["users"] = string.Join(string.Empty, rows); 50 | 51 | this.Log(LogType.OpenMenu, nameof(Users)); 52 | 53 | return this.View(); 54 | } 55 | 56 | public IActionResult Approve(int id) 57 | { 58 | if (!this.IsAdmin) 59 | { 60 | return this.RedirectToLogin(); 61 | } 62 | 63 | var userEmail = this.users.Approve(id); 64 | 65 | if (userEmail != null) 66 | { 67 | this.Log(LogType.UserApproval, userEmail); 68 | } 69 | 70 | return this.Redirect("/admin/users"); 71 | } 72 | 73 | public IActionResult Posts() 74 | { 75 | if (!this.IsAdmin) 76 | { 77 | return this.RedirectToLogin(); 78 | } 79 | 80 | var rows = this.posts 81 | .All() 82 | .Select(p => $@" 83 | 84 | {p.Id} 85 | {p.Title} 86 | 87 | Edit 88 | Delete 89 | 90 | "); 91 | 92 | this.ViewModel["posts"] = string.Join(string.Empty, rows); 93 | 94 | this.Log(LogType.OpenMenu, nameof(Posts)); 95 | 96 | return this.View(); 97 | } 98 | 99 | public IActionResult Edit(int id) 100 | => this.PrepareEditAndDeleteView(id) 101 | ?? this.View(); 102 | 103 | [HttpPost] 104 | public IActionResult Edit(int id, PostModel model) 105 | { 106 | if (!this.IsAdmin) 107 | { 108 | return this.RedirectToLogin(); 109 | } 110 | 111 | if (!this.IsValidModel(model)) 112 | { 113 | this.ShowError(EditError); 114 | return this.View(); 115 | } 116 | 117 | this.posts.Update(id, model.Title, model.Content); 118 | 119 | this.Log(LogType.EditPost, model.Title); 120 | 121 | return this.Redirect("/admin/posts"); 122 | } 123 | 124 | public IActionResult Delete(int id) 125 | { 126 | this.ViewModel["id"] = id.ToString(); 127 | 128 | return this.PrepareEditAndDeleteView(id) ?? this.View(); 129 | } 130 | 131 | [HttpPost] 132 | public IActionResult Confirm(int id) 133 | { 134 | if (!this.IsAdmin) 135 | { 136 | return this.RedirectToLogin(); 137 | } 138 | 139 | var postTitle = this.posts.Delete(id); 140 | 141 | if (postTitle != null) 142 | { 143 | this.Log(LogType.DeletePost, postTitle); 144 | } 145 | 146 | return this.Redirect("/admin/posts"); 147 | } 148 | 149 | private IActionResult PrepareEditAndDeleteView(int id) 150 | { 151 | if (!this.IsAdmin) 152 | { 153 | return this.RedirectToLogin(); 154 | } 155 | 156 | var post = this.posts.GetById(id); 157 | 158 | if (post == null) 159 | { 160 | return this.NotFound(); 161 | } 162 | 163 | this.ViewModel["title"] = post.Title; 164 | this.ViewModel["content"] = post.Content; 165 | 166 | return null; 167 | } 168 | 169 | public IActionResult Log() 170 | { 171 | this.Log(LogType.OpenMenu, nameof(Log)); 172 | 173 | var rows = this.logs 174 | .All() 175 | .Select(l => l.ToHtml()); 176 | 177 | this.ViewModel["logs"] = string.Join(string.Empty, rows); 178 | 179 | return this.View(); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /ModPanel.App/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Controllers 2 | { 3 | using Data; 4 | using Data.Models; 5 | using Services; 6 | using Services.Contracts; 7 | using SimpleMvc.Framework.Contracts; 8 | using SimpleMvc.Framework.Controllers; 9 | using System.Linq; 10 | 11 | public abstract class BaseController : Controller 12 | { 13 | private readonly ILogService logs; 14 | 15 | protected BaseController() 16 | { 17 | this.logs = new LogService(new ModPanelDbContext()); 18 | 19 | this.ViewModel["anonymousDisplay"] = "flex"; 20 | this.ViewModel["userDisplay"] = "none"; 21 | this.ViewModel["adminDisplay"] = "none"; 22 | this.ViewModel["show-error"] = "none"; 23 | } 24 | 25 | protected User Profile { get; private set; } 26 | 27 | protected bool IsAdmin => this.User.IsAuthenticated && this.Profile.IsAdmin; 28 | 29 | protected void ShowError(string error) 30 | { 31 | this.ViewModel["show-error"] = "block"; 32 | this.ViewModel["error"] = error; 33 | } 34 | 35 | protected IActionResult RedirectToHome() 36 | { 37 | return this.Redirect("/"); 38 | } 39 | 40 | protected IActionResult RedirectToLogin() 41 | { 42 | return this.Redirect("/users/login"); 43 | } 44 | 45 | protected void Log(LogType type, string additionalInformation) 46 | => this.logs.Create( 47 | this.Profile.Email, 48 | type, 49 | additionalInformation); 50 | 51 | protected override void InitializeController() 52 | { 53 | base.InitializeController(); 54 | 55 | if (this.User.IsAuthenticated) 56 | { 57 | this.ViewModel["anonymousDisplay"] = "none"; 58 | this.ViewModel["userDisplay"] = "flex"; 59 | 60 | using (var db = new ModPanelDbContext()) 61 | { 62 | this.Profile = db 63 | .Users 64 | .First(u => u.Email == this.User.Name); 65 | 66 | if (this.Profile.IsAdmin) 67 | { 68 | this.ViewModel["adminDisplay"] = "flex"; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ModPanel.App/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Controllers 2 | { 3 | using Infrastructure; 4 | using Services.Contracts; 5 | using SimpleMvc.Framework.Contracts; 6 | using System; 7 | using System.Linq; 8 | 9 | public class HomeController : BaseController 10 | { 11 | private readonly IPostService posts; 12 | private readonly ILogService logs; 13 | 14 | public HomeController( 15 | IPostService posts, 16 | ILogService logs) 17 | { 18 | this.posts = posts; 19 | this.logs = logs; 20 | } 21 | 22 | public IActionResult Index() 23 | { 24 | this.ViewModel["guestDisplay"] = "block"; 25 | this.ViewModel["authenticated"] = "none"; 26 | this.ViewModel["admin"] = "none"; 27 | 28 | if (this.User.IsAuthenticated) 29 | { 30 | this.ViewModel["guestDisplay"] = "none"; 31 | this.ViewModel["authenticated"] = "flex"; 32 | 33 | string search = null; 34 | if (this.Request.UrlParameters.ContainsKey("search")) 35 | { 36 | search = this.Request.UrlParameters["search"]; 37 | } 38 | 39 | var postsData = this.posts.AllWithDetails(search); 40 | 41 | var postsCards = postsData 42 | .Select(p => $@" 43 |
44 |
45 |

{p.Title}

46 |

47 | {p.Content} 48 |

49 |
50 |
51 | 52 | Created on {(p.CreatedOn ?? DateTime.UtcNow).ToShortDateString()} by 53 | 54 | {p.CreatedBy} 55 | 56 | 57 |
58 |
"); 59 | 60 | this.ViewModel["posts"] = postsCards.Any() 61 | ? string.Join(string.Empty, postsCards) 62 | : "

No posts found!

"; 63 | 64 | if (this.IsAdmin) 65 | { 66 | this.ViewModel["authenticated"] = "none"; 67 | this.ViewModel["admin"] = "flex"; 68 | 69 | var logsHtml = this.logs 70 | .All() 71 | .Select(l => l.ToHtml()); 72 | 73 | this.ViewModel["logs"] = string.Join(string.Empty, logsHtml); 74 | } 75 | } 76 | 77 | return this.View(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ModPanel.App/Controllers/PostsController.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Controllers 2 | { 3 | using Data.Models; 4 | using Models.Posts; 5 | using Services.Contracts; 6 | using SimpleMvc.Framework.Attributes.Methods; 7 | using SimpleMvc.Framework.Contracts; 8 | 9 | public class PostsController : BaseController 10 | { 11 | private const string CreateError = "

Check your form for errors.

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

Content must be more than 10 symbols (inclusive).

"; 12 | 13 | private readonly IPostService posts; 14 | 15 | public PostsController(IPostService posts) 16 | { 17 | this.posts = posts; 18 | } 19 | 20 | public IActionResult Create() 21 | { 22 | if (!this.User.IsAuthenticated) 23 | { 24 | return this.RedirectToLogin(); 25 | } 26 | 27 | return this.View(); 28 | } 29 | 30 | [HttpPost] 31 | public IActionResult Create(PostModel model) 32 | { 33 | if (!this.User.IsAuthenticated) 34 | { 35 | return this.RedirectToLogin(); 36 | } 37 | 38 | if (!this.IsValidModel(model)) 39 | { 40 | this.ShowError(CreateError); 41 | return this.View(); 42 | } 43 | 44 | this.posts.Create( 45 | model.Title, 46 | model.Content, 47 | this.Profile.Id); 48 | 49 | if (this.IsAdmin) 50 | { 51 | this.Log(LogType.CreatePost, model.Title); 52 | } 53 | 54 | return this.RedirectToHome(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ModPanel.App/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Controllers 2 | { 3 | using Data.Models; 4 | using Models.Users; 5 | using Services.Contracts; 6 | using SimpleMvc.Framework.Attributes.Methods; 7 | using SimpleMvc.Framework.Contracts; 8 | 9 | public class UsersController : BaseController 10 | { 11 | 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.

"; 12 | private const string EmailExistsError = "E-mail is already taken."; 13 | private const string UserIsNotApprovedError = "You must wait for your registration to be approved!"; 14 | private const string LoginError = "

Invalid credentials.

"; 15 | 16 | private readonly IUserService users; 17 | 18 | public UsersController(IUserService users) 19 | { 20 | this.users = users; 21 | } 22 | 23 | public IActionResult Register() => this.View(); 24 | 25 | [HttpPost] 26 | public IActionResult Register(RegisterModel model) 27 | { 28 | if (model.Password != model.ConfirmPassword 29 | || !this.IsValidModel(model)) 30 | { 31 | this.ShowError(RegisterError); 32 | return this.View(); 33 | } 34 | 35 | var result = this.users.Create( 36 | model.Email, 37 | model.Password, 38 | (PositionType)model.Position); 39 | 40 | if (result) 41 | { 42 | return this.RedirectToLogin(); 43 | } 44 | else 45 | { 46 | this.ShowError(EmailExistsError); 47 | return this.View(); 48 | } 49 | } 50 | 51 | public IActionResult Login() => this.View(); 52 | 53 | [HttpPost] 54 | public IActionResult Login(LoginModel model) 55 | { 56 | if (!this.IsValidModel(model)) 57 | { 58 | this.ShowError(LoginError); 59 | return this.View(); 60 | } 61 | 62 | if (!this.users.UserIsApproved(model.Email)) 63 | { 64 | this.ShowError(UserIsNotApprovedError); 65 | return this.View(); 66 | } 67 | 68 | if (this.users.UserExists(model.Email, model.Password)) 69 | { 70 | this.SignIn(model.Email); 71 | return this.RedirectToHome(); 72 | } 73 | else 74 | { 75 | this.ShowError(LoginError); 76 | return this.View(); 77 | } 78 | } 79 | 80 | public IActionResult Logout() 81 | { 82 | this.SignOut(); 83 | return this.RedirectToHome(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026152454_UsersTable.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace ModPanel.App.Data.Migrations 3 | { 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | 9 | [DbContext(typeof(ModPanelDbContext))] 10 | [Migration("20171026152454_UsersTable")] 11 | partial class UsersTable 12 | { 13 | protected override void BuildTargetModel(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("ModPanel.App.Data.Models.User", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd(); 24 | 25 | b.Property("Email") 26 | .IsRequired() 27 | .HasMaxLength(50); 28 | 29 | b.Property("IsAdmin"); 30 | 31 | b.Property("IsApproved"); 32 | 33 | b.Property("Password") 34 | .IsRequired() 35 | .HasMaxLength(50); 36 | 37 | b.Property("Position"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.HasIndex("Email") 42 | .IsUnique(); 43 | 44 | b.ToTable("Users"); 45 | }); 46 | #pragma warning restore 612, 618 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026152454_UsersTable.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.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 | IsAdmin = table.Column(type: "bit", nullable: false), 18 | IsApproved = table.Column(type: "bit", nullable: false), 19 | Password = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), 20 | Position = table.Column(type: "int", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Users", x => x.Id); 25 | }); 26 | 27 | migrationBuilder.CreateIndex( 28 | name: "IX_Users_Email", 29 | table: "Users", 30 | column: "Email", 31 | unique: true); 32 | } 33 | 34 | protected override void Down(MigrationBuilder migrationBuilder) 35 | { 36 | migrationBuilder.DropTable( 37 | name: "Users"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026165138_PostsTable.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace ModPanel.App.Data.Migrations 3 | { 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using ModPanel.App.Data; 9 | 10 | [DbContext(typeof(ModPanelDbContext))] 11 | [Migration("20171026165138_PostsTable")] 12 | partial class PostsTable 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 19 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 20 | 21 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd(); 25 | 26 | b.Property("Content") 27 | .IsRequired(); 28 | 29 | b.Property("Title") 30 | .IsRequired() 31 | .HasMaxLength(100); 32 | 33 | b.Property("UserId"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.HasIndex("UserId"); 38 | 39 | b.ToTable("Posts"); 40 | }); 41 | 42 | modelBuilder.Entity("ModPanel.App.Data.Models.User", b => 43 | { 44 | b.Property("Id") 45 | .ValueGeneratedOnAdd(); 46 | 47 | b.Property("Email") 48 | .IsRequired() 49 | .HasMaxLength(50); 50 | 51 | b.Property("IsAdmin"); 52 | 53 | b.Property("IsApproved"); 54 | 55 | b.Property("Password") 56 | .IsRequired() 57 | .HasMaxLength(50); 58 | 59 | b.Property("Position"); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.HasIndex("Email") 64 | .IsUnique(); 65 | 66 | b.ToTable("Users"); 67 | }); 68 | 69 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 70 | { 71 | b.HasOne("ModPanel.App.Data.Models.User", "User") 72 | .WithMany("Posts") 73 | .HasForeignKey("UserId") 74 | .OnDelete(DeleteBehavior.Cascade); 75 | }); 76 | #pragma warning restore 612, 618 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026165138_PostsTable.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | public partial class PostsTable : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Posts", 14 | columns: table => new 15 | { 16 | Id = table.Column(type: "int", nullable: false) 17 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 18 | Content = table.Column(type: "nvarchar(max)", nullable: false), 19 | Title = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 20 | UserId = table.Column(type: "int", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Posts", x => x.Id); 25 | table.ForeignKey( 26 | name: "FK_Posts_Users_UserId", 27 | column: x => x.UserId, 28 | principalTable: "Users", 29 | principalColumn: "Id", 30 | onDelete: ReferentialAction.Cascade); 31 | }); 32 | 33 | migrationBuilder.CreateIndex( 34 | name: "IX_Posts_UserId", 35 | table: "Posts", 36 | column: "UserId"); 37 | } 38 | 39 | protected override void Down(MigrationBuilder migrationBuilder) 40 | { 41 | migrationBuilder.DropTable( 42 | name: "Posts"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026175957_LogsTable.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace ModPanel.App.Data.Migrations 3 | { 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | 9 | [DbContext(typeof(ModPanelDbContext))] 10 | [Migration("20171026175957_LogsTable")] 11 | partial class LogsTable 12 | { 13 | protected override void BuildTargetModel(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("ModPanel.App.Data.Models.Log", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd(); 24 | 25 | b.Property("AdditionalInformation"); 26 | 27 | b.Property("Admin"); 28 | 29 | b.Property("Type"); 30 | 31 | b.HasKey("Id"); 32 | 33 | b.ToTable("Logs"); 34 | }); 35 | 36 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 37 | { 38 | b.Property("Id") 39 | .ValueGeneratedOnAdd(); 40 | 41 | b.Property("Content") 42 | .IsRequired(); 43 | 44 | b.Property("Title") 45 | .IsRequired() 46 | .HasMaxLength(100); 47 | 48 | b.Property("UserId"); 49 | 50 | b.HasKey("Id"); 51 | 52 | b.HasIndex("UserId"); 53 | 54 | b.ToTable("Posts"); 55 | }); 56 | 57 | modelBuilder.Entity("ModPanel.App.Data.Models.User", b => 58 | { 59 | b.Property("Id") 60 | .ValueGeneratedOnAdd(); 61 | 62 | b.Property("Email") 63 | .IsRequired() 64 | .HasMaxLength(50); 65 | 66 | b.Property("IsAdmin"); 67 | 68 | b.Property("IsApproved"); 69 | 70 | b.Property("Password") 71 | .IsRequired() 72 | .HasMaxLength(50); 73 | 74 | b.Property("Position"); 75 | 76 | b.HasKey("Id"); 77 | 78 | b.HasIndex("Email") 79 | .IsUnique(); 80 | 81 | b.ToTable("Users"); 82 | }); 83 | 84 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 85 | { 86 | b.HasOne("ModPanel.App.Data.Models.User", "User") 87 | .WithMany("Posts") 88 | .HasForeignKey("UserId") 89 | .OnDelete(DeleteBehavior.Cascade); 90 | }); 91 | #pragma warning restore 612, 618 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026175957_LogsTable.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | public partial class LogsTable : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Logs", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "int", nullable: false) 15 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 16 | AdditionalInformation = table.Column(type: "nvarchar(max)", nullable: true), 17 | Admin = table.Column(type: "nvarchar(max)", nullable: true), 18 | Type = table.Column(type: "int", nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Logs", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "Logs"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026183416_PostsCreatedOnColumn.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace ModPanel.App.Data.Migrations 3 | { 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using ModPanel.App.Data; 9 | using System; 10 | 11 | [DbContext(typeof(ModPanelDbContext))] 12 | [Migration("20171026183416_PostsCreatedOnColumn")] 13 | partial class PostsCreatedOnColumn 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("ModPanel.App.Data.Models.Log", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("AdditionalInformation"); 28 | 29 | b.Property("Admin"); 30 | 31 | b.Property("Type"); 32 | 33 | b.HasKey("Id"); 34 | 35 | b.ToTable("Logs"); 36 | }); 37 | 38 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 39 | { 40 | b.Property("Id") 41 | .ValueGeneratedOnAdd(); 42 | 43 | b.Property("Content") 44 | .IsRequired(); 45 | 46 | b.Property("CreatedOn"); 47 | 48 | b.Property("Title") 49 | .IsRequired() 50 | .HasMaxLength(100); 51 | 52 | b.Property("UserId"); 53 | 54 | b.HasKey("Id"); 55 | 56 | b.HasIndex("UserId"); 57 | 58 | b.ToTable("Posts"); 59 | }); 60 | 61 | modelBuilder.Entity("ModPanel.App.Data.Models.User", b => 62 | { 63 | b.Property("Id") 64 | .ValueGeneratedOnAdd(); 65 | 66 | b.Property("Email") 67 | .IsRequired() 68 | .HasMaxLength(50); 69 | 70 | b.Property("IsAdmin"); 71 | 72 | b.Property("IsApproved"); 73 | 74 | b.Property("Password") 75 | .IsRequired() 76 | .HasMaxLength(50); 77 | 78 | b.Property("Position"); 79 | 80 | b.HasKey("Id"); 81 | 82 | b.HasIndex("Email") 83 | .IsUnique(); 84 | 85 | b.ToTable("Users"); 86 | }); 87 | 88 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 89 | { 90 | b.HasOne("ModPanel.App.Data.Models.User", "User") 91 | .WithMany("Posts") 92 | .HasForeignKey("UserId") 93 | .OnDelete(DeleteBehavior.Cascade); 94 | }); 95 | #pragma warning restore 612, 618 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/20171026183416_PostsCreatedOnColumn.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | using System; 5 | 6 | public partial class PostsCreatedOnColumn : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.AddColumn( 11 | name: "CreatedOn", 12 | table: "Posts", 13 | type: "datetime2", 14 | nullable: true); 15 | } 16 | 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | migrationBuilder.DropColumn( 20 | name: "CreatedOn", 21 | table: "Posts"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Migrations/ModPanelDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace ModPanel.App.Data.Migrations 3 | { 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using System; 8 | 9 | [DbContext(typeof(ModPanelDbContext))] 10 | partial class ModPanelDbContextModelSnapshot : ModelSnapshot 11 | { 12 | protected override void BuildModel(ModelBuilder modelBuilder) 13 | { 14 | #pragma warning disable 612, 618 15 | modelBuilder 16 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") 17 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 18 | 19 | modelBuilder.Entity("ModPanel.App.Data.Models.Log", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd(); 23 | 24 | b.Property("AdditionalInformation"); 25 | 26 | b.Property("Admin"); 27 | 28 | b.Property("Type"); 29 | 30 | b.HasKey("Id"); 31 | 32 | b.ToTable("Logs"); 33 | }); 34 | 35 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 36 | { 37 | b.Property("Id") 38 | .ValueGeneratedOnAdd(); 39 | 40 | b.Property("Content") 41 | .IsRequired(); 42 | 43 | b.Property("CreatedOn"); 44 | 45 | b.Property("Title") 46 | .IsRequired() 47 | .HasMaxLength(100); 48 | 49 | b.Property("UserId"); 50 | 51 | b.HasKey("Id"); 52 | 53 | b.HasIndex("UserId"); 54 | 55 | b.ToTable("Posts"); 56 | }); 57 | 58 | modelBuilder.Entity("ModPanel.App.Data.Models.User", b => 59 | { 60 | b.Property("Id") 61 | .ValueGeneratedOnAdd(); 62 | 63 | b.Property("Email") 64 | .IsRequired() 65 | .HasMaxLength(50); 66 | 67 | b.Property("IsAdmin"); 68 | 69 | b.Property("IsApproved"); 70 | 71 | b.Property("Password") 72 | .IsRequired() 73 | .HasMaxLength(50); 74 | 75 | b.Property("Position"); 76 | 77 | b.HasKey("Id"); 78 | 79 | b.HasIndex("Email") 80 | .IsUnique(); 81 | 82 | b.ToTable("Users"); 83 | }); 84 | 85 | modelBuilder.Entity("ModPanel.App.Data.Models.Post", b => 86 | { 87 | b.HasOne("ModPanel.App.Data.Models.User", "User") 88 | .WithMany("Posts") 89 | .HasForeignKey("UserId") 90 | .OnDelete(DeleteBehavior.Cascade); 91 | }); 92 | #pragma warning restore 612, 618 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ModPanel.App/Data/ModPanelDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data 2 | { 3 | using Microsoft.EntityFrameworkCore; 4 | using Models; 5 | 6 | public class ModPanelDbContext : DbContext 7 | { 8 | public DbSet Users { get; set; } 9 | 10 | public DbSet Posts { get; set; } 11 | 12 | public DbSet Logs { get; set; } 13 | 14 | protected override void OnConfiguring(DbContextOptionsBuilder builder) 15 | { 16 | builder 17 | .UseSqlServer($"Server=.;Database=ModPanelDbExam;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 | .HasMany(u => u.Posts) 30 | .WithOne(p => p.User) 31 | .HasForeignKey(p => p.UserId); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Models/Log.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Models 2 | { 3 | public class Log 4 | { 5 | public int Id { get; set; } 6 | 7 | public string Admin { get; set; } 8 | 9 | public LogType Type { get; set; } 10 | 11 | public string AdditionalInformation { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Models/LogType.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Models 2 | { 3 | public enum LogType 4 | { 5 | CreatePost = 0, 6 | EditPost = 1, 7 | DeletePost = 2, 8 | UserApproval = 3, 9 | OpenMenu = 4 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Models/PositionType.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Models 2 | { 3 | public enum PositionType 4 | { 5 | Developer = 0, 6 | Designer = 1, 7 | TechnicalSupport = 2, 8 | TechnicalTrainer = 3, 9 | HR = 4, 10 | MarketingSpecialist = 5 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Models/Post.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Data.Models 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | public class Post 7 | { 8 | public int Id { get; set; } 9 | 10 | [Required] 11 | [MinLength(3)] 12 | [MaxLength(100)] 13 | public string Title { get; set; } 14 | 15 | [Required] 16 | [MinLength(10)] 17 | public string Content { get; set; } 18 | 19 | public DateTime? CreatedOn { get; set; } 20 | 21 | public int UserId { get; set; } 22 | 23 | public User User { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ModPanel.App/Data/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | public PositionType Position { get; set; } 20 | 21 | public bool IsApproved { get; set; } 22 | 23 | public bool IsAdmin { get; set; } 24 | 25 | public List Posts { get; set; } = new List(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/DependencyControllerRouter.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure 2 | { 3 | using Data.Models; 4 | using System; 5 | 6 | public static class EnumExtensions 7 | { 8 | public static string ToFriendlyName(this PositionType position) 9 | { 10 | switch (position) 11 | { 12 | case PositionType.Developer: 13 | case PositionType.Designer: 14 | case PositionType.HR: 15 | return position.ToString(); 16 | case PositionType.TechnicalSupport: 17 | return "Technical Support"; 18 | case PositionType.TechnicalTrainer: 19 | return "Technical Trainer"; 20 | case PositionType.MarketingSpecialist: 21 | return "Marketing Specialist"; 22 | default: 23 | throw new InvalidOperationException($"Invalid position type {position}."); 24 | } 25 | } 26 | 27 | public static string ToViewClassName(this LogType type) 28 | { 29 | switch (type) 30 | { 31 | case LogType.CreatePost: 32 | return "success"; 33 | case LogType.EditPost: 34 | return "warning"; 35 | case LogType.DeletePost: 36 | return "danger"; 37 | case LogType.UserApproval: 38 | return "success"; 39 | case LogType.OpenMenu: 40 | return "primary"; 41 | default: 42 | throw new InvalidOperationException($"Invalid log type {type}."); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/HtmlHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure 2 | { 3 | using Models.Logs; 4 | 5 | public static class HtmlHelpers 6 | { 7 | public static string ToHtml(this LogModel log) 8 | => $@" 9 |
10 |
11 |

{log}

12 |
13 |
"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Mapping/AutoMapperConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Mapping/IHaveCustomMapping.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure.Mapping 2 | { 3 | using AutoMapper; 4 | 5 | public interface IHaveCustomMapping 6 | { 7 | void Configure(IMapperConfigurationExpression config); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Mapping/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure.Mapping 2 | { 3 | public interface IMapFrom 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure 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.Any()) 10 | { 11 | return input; 12 | } 13 | 14 | var first = input.First(); 15 | var rest = input.Substring(1); 16 | 17 | return $"{char.ToUpper(first)}{rest}"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Validation/Posts/ContentAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure.Validation.Posts 2 | { 3 | using SimpleMvc.Framework.Attributes.Validation; 4 | 5 | public class ContentAttribute : PropertyValidationAttribute 6 | { 7 | public override bool IsValid(object value) 8 | { 9 | var content = value as string; 10 | if (content == null) 11 | { 12 | return true; 13 | } 14 | 15 | return content.Length >= 10; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Validation/Posts/TitleAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Infrastructure.Validation.Posts 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 >= 3 && title.Length <= 100; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Validation/RequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Validation/Users/EmailAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Infrastructure/Validation/Users/PasswordAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Launcher.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App 2 | { 3 | using Infrastructure; 4 | using Infrastructure.Mapping; 5 | using Microsoft.EntityFrameworkCore; 6 | using ModPanel.App.Data; 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 ModPanelDbContext()) 16 | { 17 | db.Database.Migrate(); 18 | } 19 | 20 | AutoMapperConfiguration.Initialize(); 21 | } 22 | 23 | public static void Main() 24 | => MvcEngine.Run(new WebServer( 25 | 1337, 26 | DependencyControllerRouter.Get(), 27 | new ResourceRouter())); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ModPanel.App/ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Admin/AdminUserModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Models.Admin 2 | { 3 | using AutoMapper; 4 | using Data.Models; 5 | using Infrastructure.Mapping; 6 | 7 | public class AdminUserModel : IMapFrom, IHaveCustomMapping 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Email { get; set; } 12 | 13 | public PositionType Position { get; set; } 14 | 15 | public int Posts { get; set; } 16 | 17 | public bool IsApproved { get; set; } 18 | 19 | public void Configure(IMapperConfigurationExpression config) 20 | { 21 | config 22 | .CreateMap() 23 | .ForMember(au => au.Posts, cfg => cfg.MapFrom(u => u.Posts.Count)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Home/HomeListingModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Models.Home 2 | { 3 | using Data.Models; 4 | using Infrastructure.Mapping; 5 | using System; 6 | using AutoMapper; 7 | 8 | public class HomeListingModel : IMapFrom, IHaveCustomMapping 9 | { 10 | public string Title { get; set; } 11 | 12 | public string Content { get; set; } 13 | 14 | public string CreatedBy { get; set; } 15 | 16 | public DateTime? CreatedOn { get; set; } 17 | 18 | public void Configure(IMapperConfigurationExpression config) 19 | { 20 | config 21 | .CreateMap() 22 | .ForMember(hl => hl.CreatedBy, cfg => cfg.MapFrom(p => p.User.Email)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Logs/LogModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Models.Logs 2 | { 3 | using Data.Models; 4 | using Infrastructure.Mapping; 5 | using System; 6 | 7 | public class LogModel : IMapFrom 8 | { 9 | public string Admin { get; set; } 10 | 11 | public LogType Type { get; set; } 12 | 13 | public string AdditionalInformation { get; set; } 14 | 15 | public override string ToString() 16 | { 17 | string message = null; 18 | 19 | switch (this.Type) 20 | { 21 | case LogType.CreatePost: 22 | message = $"created the post {this.AdditionalInformation}"; 23 | break; 24 | case LogType.EditPost: 25 | message = $"edited the post {this.AdditionalInformation}"; 26 | break; 27 | case LogType.DeletePost: 28 | message = $"deleted the post {this.AdditionalInformation}"; 29 | break; 30 | case LogType.UserApproval: 31 | message = $"approved the registration of {this.AdditionalInformation}"; 32 | break; 33 | case LogType.OpenMenu: 34 | message = $"opened {this.AdditionalInformation} menu"; 35 | break; 36 | default: 37 | throw new InvalidOperationException($"Invalid log type: {this.Type}."); 38 | } 39 | 40 | return $"{this.Admin} {message}"; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Posts/PostListingModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Models.Posts 2 | { 3 | using Data.Models; 4 | using Infrastructure.Mapping; 5 | 6 | public class PostListingModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Title { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Posts/PostModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Models.Posts 2 | { 3 | using Data.Models; 4 | using Infrastructure.Mapping; 5 | using Infrastructure.Validation; 6 | using Infrastructure.Validation.Posts; 7 | 8 | public class PostModel : IMapFrom 9 | { 10 | [Required] 11 | [Title] 12 | public string Title { get; set; } 13 | 14 | [Required] 15 | [Content] 16 | public string Content { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Users/LoginModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | -------------------------------------------------------------------------------- /ModPanel.App/Models/Users/RegisterModel.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.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 | [Required] 13 | [Password] 14 | public string Password { get; set; } 15 | 16 | [Required] 17 | public string ConfirmPassword { get; set; } 18 | 19 | public int Position { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ModPanel.App/Services/Contracts/ILogService.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Services.Contracts 2 | { 3 | using Data.Models; 4 | using Models.Logs; 5 | using System.Collections.Generic; 6 | 7 | public interface ILogService 8 | { 9 | void Create(string admin, LogType type, string additionalInformation); 10 | 11 | IEnumerable All(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ModPanel.App/Services/Contracts/IPostService.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Services.Contracts 2 | { 3 | using Models.Home; 4 | using Models.Posts; 5 | using System.Collections.Generic; 6 | 7 | public interface IPostService 8 | { 9 | void Create(string title, string content, int userId); 10 | 11 | IEnumerable All(); 12 | 13 | IEnumerable AllWithDetails(string search = null); 14 | 15 | PostModel GetById(int id); 16 | 17 | void Update(int id, string title, string content); 18 | 19 | string Delete(int id); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ModPanel.App/Services/Contracts/IUserService.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Services.Contracts 2 | { 3 | using Data.Models; 4 | using Models.Admin; 5 | using System.Collections.Generic; 6 | 7 | public interface IUserService 8 | { 9 | bool Create(string email, string password, PositionType position); 10 | 11 | bool UserExists(string email, string password); 12 | 13 | bool UserIsApproved(string email); 14 | 15 | IEnumerable All(); 16 | 17 | string Approve(int id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ModPanel.App/Services/LogService.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Services 2 | { 3 | using AutoMapper.QueryableExtensions; 4 | using Contracts; 5 | using Data; 6 | using Data.Models; 7 | using Models.Logs; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | 11 | public class LogService : ILogService 12 | { 13 | private readonly ModPanelDbContext db; 14 | 15 | public LogService(ModPanelDbContext db) 16 | { 17 | this.db = db; 18 | } 19 | 20 | public void Create(string admin, LogType type, string additionalInformation) 21 | { 22 | var log = new Log 23 | { 24 | Admin = admin, 25 | Type = type, 26 | AdditionalInformation = additionalInformation 27 | }; 28 | 29 | this.db.Logs.Add(log); 30 | this.db.SaveChanges(); 31 | } 32 | 33 | public IEnumerable All() 34 | => this.db 35 | .Logs 36 | .OrderByDescending(l => l.Id) 37 | .ProjectTo() 38 | .ToList(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ModPanel.App/Services/PostService.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Services 2 | { 3 | using AutoMapper.QueryableExtensions; 4 | using Contracts; 5 | using Data; 6 | using Data.Models; 7 | using Infrastructure; 8 | using Models.Home; 9 | using Models.Posts; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | 14 | public class PostService : IPostService 15 | { 16 | private readonly ModPanelDbContext db; 17 | 18 | public PostService(ModPanelDbContext db) 19 | { 20 | this.db = db; 21 | } 22 | 23 | public void Create(string title, string content, int userId) 24 | { 25 | var post = new Post 26 | { 27 | Title = title.Capitalize(), 28 | Content = content, 29 | UserId = userId, 30 | CreatedOn = DateTime.UtcNow 31 | }; 32 | 33 | this.db.Posts.Add(post); 34 | this.db.SaveChanges(); 35 | } 36 | 37 | public IEnumerable All() 38 | => this.db 39 | .Posts 40 | .ProjectTo() 41 | .ToList(); 42 | 43 | public IEnumerable AllWithDetails(string search = null) 44 | { 45 | var query = this.db.Posts.AsQueryable(); 46 | 47 | if (!string.IsNullOrWhiteSpace(search)) 48 | { 49 | query = query.Where(p => p.Title.ToLower().Contains(search.ToLower())); 50 | } 51 | 52 | return query 53 | .OrderByDescending(p => p.Id) 54 | .ProjectTo() 55 | .ToList(); 56 | } 57 | 58 | public PostModel GetById(int id) 59 | => this.db 60 | .Posts 61 | .Where(p => p.Id == id) 62 | .ProjectTo() 63 | .FirstOrDefault(); 64 | 65 | public void Update(int id, string title, string content) 66 | { 67 | var post = db.Posts.Find(id); 68 | 69 | if (post == null) 70 | { 71 | return; 72 | } 73 | 74 | post.Title = title.Capitalize(); 75 | post.Content = content; 76 | 77 | this.db.SaveChanges(); 78 | } 79 | 80 | public string Delete(int id) 81 | { 82 | var post = db.Posts.Find(id); 83 | 84 | if (post == null) 85 | { 86 | return null; 87 | } 88 | 89 | this.db.Posts.Remove(post); 90 | this.db.SaveChanges(); 91 | 92 | return post.Title; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ModPanel.App/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace ModPanel.App.Services 2 | { 3 | using AutoMapper.QueryableExtensions; 4 | using Contracts; 5 | using Data; 6 | using Data.Models; 7 | using Models.Admin; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | 11 | public class UserService : IUserService 12 | { 13 | private readonly ModPanelDbContext db; 14 | 15 | public UserService(ModPanelDbContext db) 16 | { 17 | this.db = db; 18 | } 19 | 20 | public bool Create(string email, string password, PositionType position) 21 | { 22 | if (this.db.Users.Any(u => u.Email == email)) 23 | { 24 | return false; 25 | } 26 | 27 | var isAdmin = !this.db.Users.Any(); 28 | 29 | var user = new User 30 | { 31 | Email = email, 32 | Password = password, 33 | IsAdmin = isAdmin, 34 | Position = position, 35 | IsApproved = isAdmin 36 | }; 37 | 38 | this.db.Add(user); 39 | this.db.SaveChanges(); 40 | 41 | return true; 42 | } 43 | 44 | public bool UserExists(string email, string password) 45 | => this.db 46 | .Users 47 | .Any(u => u.Email == email && u.Password == password); 48 | 49 | public bool UserIsApproved(string email) 50 | => this.db 51 | .Users 52 | .Any(u => u.Email == email && u.IsApproved); 53 | 54 | public IEnumerable All() 55 | => this.db 56 | .Users 57 | .ProjectTo() 58 | .ToList(); 59 | 60 | public string Approve(int id) 61 | { 62 | var user = this.db.Users.Find(id); 63 | 64 | if (user != null) 65 | { 66 | user.IsApproved = true; 67 | 68 | this.db.SaveChanges(); 69 | } 70 | 71 | return user?.Email; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ModPanel.App/Views/Admin/Delete.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |

Edit Post

7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Admin/Edit.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |

Edit Post

7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Admin/Log.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |

Log

7 |
8 | 9 |
10 |
11 |
12 | 13 |
14 |
15 | {{{logs}}} 16 |
17 |
18 |
19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Admin/Posts.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{{posts}}} 15 | 16 |
#TitleAction
17 |
18 |
19 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Admin/Users.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{{users}}} 17 | 18 |
#E-mailPositionPostsAction
19 |
20 |
21 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Home/Index.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 | Welcome 6 |
7 |

8 | Welcome 9 |

10 |

11 | Please login. If you don't have a registration yet, you can register here 12 |

13 |
14 |
15 |
16 |
17 | {{{posts}}} 18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | {{{posts}}} 26 |
27 |
28 |
29 | {{{logs}}} 30 |
31 |
32 |
33 |
34 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Layout.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Mod Panel 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 |
17 | 64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 | {{{error}}} 72 |
73 |
74 |
75 | 76 | {{{content}}} 77 | 78 |
79 | 82 |
83 | 84 | 85 | 87 | 89 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /ModPanel.App/Views/Posts/Create.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |

New Post

7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /ModPanel.App/Views/Users/Login.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |
5 |
6 |

Login

7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /ModPanel.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 | 37 |
38 | 39 |

40 | Your submission needs to be approved by an administrator! 41 |

42 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModPanel 2 | Simple MVC application built from scratch as an exam preparation 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}") = "ModPanel.App", "ModPanel.App\ModPanel.App.csproj", "{06328E37-8012-4A15-B873-B97829672224}" 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 | {06328E37-8012-4A15-B873-B97829672224}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {06328E37-8012-4A15-B873-B97829672224}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {06328E37-8012-4A15-B873-B97829672224}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {06328E37-8012-4A15-B873-B97829672224}.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 | --------------------------------------------------------------------------------