├── .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 | ";
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 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/ModPanel.App/Views/Admin/Edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Edit Post
7 |
8 |
9 |
10 |
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 | Title |
10 | Action |
11 |
12 |
13 |
14 | {{{posts}}}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/ModPanel.App/Views/Admin/Users.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # |
9 | E-mail |
10 | Position |
11 | Posts |
12 | Action |
13 |
14 |
15 |
16 | {{{users}}}
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/ModPanel.App/Views/Home/Index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

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 |
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 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/ModPanel.App/Views/Users/Login.html:
--------------------------------------------------------------------------------
1 |
2 |
28 |
--------------------------------------------------------------------------------
/ModPanel.App/Views/Users/Register.html:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------