* login/logout

* change administration password
This commit is contained in:
tmont 2011-02-17 20:55:47 +00:00
parent df137150ae
commit 99cb4defbe
18 changed files with 391 additions and 21 deletions

View File

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Linq;
using JetBrains.Annotations;
using NHibernate;
using Portoa.NHibernate;

View File

@ -0,0 +1,54 @@
using System.Web.Mvc;
using Portoa.Web;
using Portoa.Web.ErrorHandling;
using VideoGameQuotes.Api;
using VideoGameQuotes.Web.Models;
using VideoGameQuotes.Web.Security;
using VideoGameQuotes.Web.Services;
namespace VideoGameQuotes.Web.Controllers {
[IsValidUser(Group = UserGroup.Admin)]
public class AdminController : Controller {
private readonly ICurrentUserProvider userProvider;
private readonly IAdministrationService adminService;
public AdminController(ICurrentUserProvider userProvider, IAdministrationService adminService) {
this.userProvider = userProvider;
this.adminService = adminService;
}
public ActionResult Index() {
return View();
}
[HttpGet]
public ActionResult Password() {
return View(new ChangePasswordModel());
}
[HttpPost]
public ActionResult Password(ChangePasswordModel model) {
if (!ModelState.IsValid) {
return View(model);
}
var user = userProvider.CurrentUser;
if (user == null) {
return View("Unknown", new ErrorModel());
}
try {
user.ChangePassword(model.Password);
adminService.SaveUser(user);
return View("PasswordSuccessfullyChanged");
} catch {
ControllerContext.AddModelError("password", "Unable to change password");
return View(model);
}
}
}
}

View File

@ -1,12 +1,19 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;
using System.Web.Mvc;
using Portoa.Web.Controllers;
using Portoa.Web.Security;
using VideoGameQuotes.Api;
using VideoGameQuotes.Web.Models;
namespace VideoGameQuotes.Web.Controllers {
public class HomeController : Controller {
private readonly IAuthenticationService authenticationService;
private readonly ICurrentUserProvider userProvider;
private static readonly string[] answers = new[] {
"I AM ERROR.",
"shyron",
@ -19,6 +26,11 @@ namespace VideoGameQuotes.Web.Controllers {
"ryu huyabasa"
};
public HomeController(IAuthenticationService authenticationService, ICurrentUserProvider userProvider) {
this.authenticationService = authenticationService;
this.userProvider = userProvider;
}
public ActionResult Index() {
return View();
}
@ -27,6 +39,30 @@ namespace VideoGameQuotes.Web.Controllers {
return View();
}
[HttpPost]
public ActionResult Login([Required]string username, [Required]string password) {
if (!ModelState.IsValid) {
return Json(this.CreateJsonErrorResponse("Invalid request"));
}
if (!authenticationService.IsValid(username, password)) {
return Json(this.CreateJsonErrorResponse("Invalid username/password"));
}
authenticationService.Login(username);
return Json(this.CreateJsonResponse());
}
[ChildActionOnly]
public ActionResult MainMenu() {
var model = new MainMenuModel { User = userProvider.CurrentUser };
return PartialView("MainMenu", model);
}
public ActionResult Logout(string redirectUrl) {
authenticationService.Logout();
return Redirect(redirectUrl ?? "/");
}
public ActionResult Contact() {
var randomAnswer = GetRandomAnswer();
var model = new ContactModel {

View File

@ -4,9 +4,9 @@ using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.Practices.Unity;
using Portoa.Logging;
using Portoa.Persistence;
using Portoa.Web;
using Portoa.Web.Models;
using Portoa.Web.Security;
using Portoa.Web.Unity;
using UnityGenerics;
using VideoGameQuotes.Api;
@ -40,13 +40,18 @@ namespace VideoGameQuotes.Web {
.RegisterType<ICurrentUserProvider, SessionBasedUserProvider>()
.RegisterType<IsValidUserAttribute>(new InjectionProperty<IsValidUserAttribute>(attr => attr.UserProvider))
.RegisterType<IUserService, UserService>()
.RegisterType<IAdministrationService, AdministrationService>()
.RegisterType<IQuoteService, QuoteService>()
.RegisterType<IApiService, ApiService>()
.RegisterType<IAuthenticationService, FormsAuthenticationService>()
.RegisterType<IQuoteSearcher, LuceneQuoteSearcher>()
.RegisterType<ISearchIndexLocator, SearchIndexLocator>(
new ContainerControlledLifetimeManager(),
new InjectionFactory(container => new SearchIndexLocator(((NameValueCollection)ConfigurationManager.GetSection("vgquotes"))["luceneIndexDirectory"]))
).RegisterType<IUserRepository, UserRepository>();
.RegisterType<ISearchIndexLocator, SearchIndexLocator>(new ContainerControlledLifetimeManager(), new InjectionFactory(CreateIndexLocator))
.RegisterType<IUserRepository, UserRepository>();
}
private static SearchIndexLocator CreateIndexLocator(IUnityContainer container) {
var indexDirectory = ((NameValueCollection)ConfigurationManager.GetSection("vgquotes"))["luceneIndexDirectory"];
return new SearchIndexLocator(indexDirectory);
}
protected override void AfterStartUp() {
@ -60,8 +65,13 @@ namespace VideoGameQuotes.Web {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("media/{*anything}");
//bullshit route so that RenderAction works
routes.MapRoute("mainmenu", "home/mainmenu", new { controller = "Home", action = "MainMenu" });
routes.MapRoute("admin", "admin/{action}", new { controller = "Admin", action = "Index" });
routes.MapRoute("api", "api/{action}/{id}/{*criteria}", new { controller = "Api" }, new { action = "game|system|category|publisher|quote", id = @"\d+|all" });
routes.MapRoute("home", "{action}", new { controller = "Home", action = "Index" }, new { action = "about|contact" });
routes.MapRoute("home", "{action}", new { controller = "Home", action = "Index" }, new { action = "about|contact|login|logout" });
routes.MapRoute("best", "best/{start}-{end}/", new { controller = "Quote", action = "Best" }, new { start = @"\d+", end = @"\d+" });
routes.MapRoute("browse", "browse/{*qualifiers}", new { controller = "Quote", action = "Browse" });
routes.MapRoute("search", "search/{*searchQuery}", new { controller = "Quote", action = "Search" });

View File

@ -0,0 +1,9 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace VideoGameQuotes.Web.Models {
public class ChangePasswordModel {
[Required, DisplayName("New password")]
public string Password { get; set; }
}
}

View File

@ -0,0 +1,7 @@
using VideoGameQuotes.Api;
namespace VideoGameQuotes.Web.Models {
public class MainMenuModel {
public User User { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using Portoa.Persistence;
using VideoGameQuotes.Api;
using VideoGameQuotes.Api.Persistence;
namespace VideoGameQuotes.Web.Services {
public interface IAdministrationService {
User SaveUser(User user);
}
public class AdministrationService : IAdministrationService {
private readonly IUserRepository userRepository;
public AdministrationService(IUserRepository userRepository) {
this.userRepository = userRepository;
}
[UnitOfWork]
public User SaveUser(User user) {
return userRepository.Save(user);
}
}
}

View File

@ -0,0 +1,34 @@
using System.Web.Security;
using Portoa.Persistence;
using Portoa.Web.Security;
using Portoa.Web.Session;
using VideoGameQuotes.Api.Persistence;
namespace VideoGameQuotes.Web.Services {
public class FormsAuthenticationService : IAuthenticationService {
private readonly IUserRepository userRepository;
private readonly ISessionStore sessionStore;
public FormsAuthenticationService(IUserRepository userRepository, ISessionStore sessionStore) {
this.userRepository = userRepository;
this.sessionStore = sessionStore;
}
[UnitOfWork]
public void Login(string username) {
FormsAuthentication.SetAuthCookie(username, true);
sessionStore["user"] = userRepository.FindByUsername(username);
}
public void Logout() {
FormsAuthentication.SignOut();
sessionStore["user"] = null;
}
[UnitOfWork]
public bool IsValid(string username, string password) {
var user = userRepository.FindByUsername(username);
return user != null && user.VerifyPassword(password);
}
}
}

View File

@ -84,11 +84,14 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Controllers\ApiController.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Models\ApiModel.cs" />
<Compile Include="Models\BrowseModel.cs" />
<Compile Include="Models\BrowseModelBinder.cs" />
<Compile Include="Models\ChangePasswordModel.cs" />
<Compile Include="Models\MainMenuModel.cs" />
<Compile Include="Models\QualifiedBrowseModel.cs" />
<Compile Include="Models\PagedQuoteCollectionModel.cs" />
<Compile Include="Models\QuoteCollectionModel.cs" />
@ -96,6 +99,8 @@
<Compile Include="Models\ReportModel.cs" />
<Compile Include="Models\SearchModel.cs" />
<Compile Include="Models\VoteModel.cs" />
<Compile Include="Services\AdministrationService.cs" />
<Compile Include="Services\FormsAuthenticationService.cs" />
<Compile Include="Validation\NonEmptyText.cs" />
<Compile Include="Security\IsValidUserAttribute.cs" />
<Compile Include="Controllers\QuoteController.cs" />
@ -123,7 +128,11 @@
<Content Include="media\css\reset.css" />
<Content Include="media\images\favicon.png" />
<Content Include="media\images\search.png" />
<Content Include="media\js\jquery.cookie.js" />
<Content Include="media\js\vgquotes.js" />
<Content Include="Views\Admin\Index.aspx" />
<Content Include="Views\Admin\Password.aspx" />
<Content Include="Views\Admin\PasswordSuccessfullyChanged.aspx" />
<Content Include="Views\Home\About.aspx" />
<Content Include="Views\Home\Contact.aspx" />
<Content Include="Views\Home\ContactSuccess.aspx" />
@ -139,6 +148,7 @@
<Content Include="Views\Quote\Submit.aspx" />
<Content Include="Views\Shared\ExceptionView.ascx" />
<Content Include="Views\Shared\Forbidden.aspx" />
<Content Include="Views\Shared\MainMenu.ascx" />
<Content Include="Views\Shared\NotFound.aspx" />
<Content Include="Views\Shared\NotFoundContent.ascx" />
<Content Include="Views\Shared\RecursiveExceptionView.ascx" />

View File

@ -0,0 +1,12 @@
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Admin</asp:Content>
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
<h2>Site Administration</h2>
<ul>
<li><%= Html.ActionLink("Create admin", "create", "admin") %></li>
<li><%= Html.ActionLink("Change password", "password", "admin") %></li>
<li><%= Html.ActionLink("View reports", "reports", "admin") %></li>
<li><%= Html.ActionLink("Manage users", "users", "admin") %></li>
</ul>
</asp:Content>

View File

@ -0,0 +1,16 @@
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage<VideoGameQuotes.Web.Models.ChangePasswordModel>" MasterPageFile="~/Views/Shared/Site.Master" %>
<%@ Import Namespace="Portoa.Web.Util" %>
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Change Password</asp:Content>
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
<% using (Html.BeginForm()) { %>
<p>
<%= Html.LabelFor(model => model.Password) %>
<br />
<%= Html.PasswordFor(model => model.Password) %>
</p>
<p>
<%= Html.Submit("Change Password") %>
</p>
<% } %>
</asp:Content>

View File

@ -0,0 +1,11 @@
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Password Successfully Changed</asp:Content>
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
<p>
Your password has been successfully changed.
</p>
<p>
<%= Html.ActionLink("Administer more stuff", "index", "admin") %>
</p>
</asp:Content>

View File

@ -0,0 +1,16 @@
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<VideoGameQuotes.Web.Models.MainMenuModel>" %>
<%@ Import Namespace="VideoGameQuotes.Api" %>
<li><%= Html.ActionLink("Recent", "recent", "Quote", null, new { title = "View most recently submitted quotes" })%></li>
<li><%= Html.RouteLink("Best", "quote", new { action = "best" }, new { title = "View the top rated quotes" })%></li>
<li><%= Html.ActionLink("Browse", "browse", "Quote", new { qualifiers = "" }, new { title = "Browse the quote database" })%></li>
<li><%= Html.ActionLink("Random", "random", "Quote", null, new { title = "View a random quote" })%></li>
<li><%= Html.ActionLink("Submit", "submit", "Quote", null, new { title = "Submit a new quote" }) %></li>
<li><%= Html.ActionLink("About", "about", "Home", null, new { title = "About the site" })%></li>
<% if (Model.User != null && Model.User.Group >= UserGroup.Admin) { %>
<li><%= Html.ActionLink("Admin", "index", "admin", null, new { title = "Perform administrative tasks" }) %></li>
<% } %>
<li class="searchbox">
<%= Html.TextBox("searchQuery", null, new { id = "search-query" })%>
<img src="/media/images/search.png" alt="search" title="search quotes" id="search-submit" />
</li>

View File

@ -17,21 +17,12 @@
<div id="header">
<div class="content-container">
<div id="logo">
<h1>Video Game Quotes</h1>
<h1><%= Html.ActionLink("Video Game Quotes", "Index", "Home") %></h1>
</div>
<div id="main-menu">
<ul class="clearfix menu">
<li><%= Html.ActionLink("Recent", "recent", "Quote", null, new { title = "View most recently submitted quotes" })%></li>
<li><%= Html.RouteLink("Best", "quote", new { action = "best" }, new { title = "View the top rated quotes" })%></li>
<li><%= Html.ActionLink("Browse", "browse", "Quote", new { qualifiers = "" }, new { title = "Browse the quote database" })%></li>
<li><%= Html.ActionLink("Random", "random", "Quote", null, new { title = "View a random quote" })%></li>
<li><%= Html.ActionLink("Submit", "submit", "Quote", null, new { title = "Submit a new quote" }) %></li>
<li><%= Html.ActionLink("About", "about", "Home", null, new { title = "About the site" })%></li>
<li class="searchbox">
<%= Html.TextBox("searchQuery", null, new { id = "search-query" })%>
<img src="/media/images/search.png" alt="search" title="search quotes" id="search-submit" />
</li>
<% Html.RenderAction("MainMenu", "Home"); %>
</ul>
</div>
</div>
@ -46,12 +37,17 @@
<div id="footer">
<div class="content-container">
&copy; <%= DateTime.UtcNow.Year %> <a href="http://tommymontgomery.com/" title="Who is this man?">Tommy Montgomery</a><br />
If you steal something, I&rsquo;ll murder your family.
<% if (!Request.IsAuthenticated) { %>
<a href="#" id="login-link">login</a>
<% } else { %>
<%= Html.ActionLink("logout", "logout", "home", new { redirectUrl = Request.Path }, null)%>
<% } %>
</div>
</div>
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js"></script>
<script type="text/javascript" src="/media/js/jquery.cookie.js"></script>
<script type="text/javascript" src="/media/js/vgquotes.js"></script>
<asp:ContentPlaceHolder ID="DeferrableScripts" runat="server" />
</body>

View File

@ -41,6 +41,8 @@
<system.web>
<!-- disables input validation for requests -->
<httpRuntime requestValidationMode="2.0"/>
<authentication mode="Forms" />
<compilation debug="true" targetFramework="4.0">
<assemblies>

View File

@ -129,6 +129,15 @@ ul.menu li {
padding: 5px;
}
#logo a {
text-decoration: none;
color: inherit;
}
#login-dialog {
padding: 10px;
}
#search-submit {
cursor: pointer;
}

View File

@ -0,0 +1,50 @@
/**
* Cookie plugin
*
* Copyright (c) 2006 Klaus Hartl (stilbuero.de)
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
jQuery.cookie = function(name, value, options) {
if (typeof value != 'undefined') { // name and value given, set cookie
options = options || {};
if (value === null) {
value = '';
options.expires = -1;
}
var expires = '';
if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
var date;
if (typeof options.expires == 'number') {
date = new Date();
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
} else {
date = options.expires;
}
expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
}
// CAUTION: Needed to parenthesize options.path and options.domain
// in the following expressions, otherwise they evaluate to undefined
// in the packed version for some reason...
var path = options.path ? '; path=' + (options.path) : '';
var domain = options.domain ? '; domain=' + (options.domain) : '';
var secure = options.secure ? '; secure' : '';
document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
} else { // only name given, get cookie
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
};

View File

@ -4,7 +4,39 @@
this.css("top", ($(window).height() - this.height()) / 2 + $(window).scrollTop() + "px");
this.css("left", ($(window).width() - this.width()) / 2 + $(window).scrollLeft() + "px");
return this;
}
};
$.vgquotes = {
refresh: function() { }
};
(function(){
var refreshCookie = "vgquotes.refreshFragment";
var refresh = function() {
var url = window.location.href;
var fragmentPosition = url.lastIndexOf("#");
if (fragmentPosition >= 0) {
if (fragmentPosition !== url.length - 1) {
$.cookie(refreshCookie, url.substring(fragmentPosition + 1)); //store the fragment in a cookie
}
url = url.substring(0, fragmentPosition);
}
window.location.href = url;
};
var applyFragmentFromCookie = function() {
var fragment = $.cookie(refreshCookie);
if (fragment !== null) {
window.location.href += "#" + fragment;
$.cookie(refreshCookie, null); //delete cookie
}
};
$(document).ready(applyFragmentFromCookie);
$.vgquotes.refresh = refresh;
}());
$(document).ready(function() {
(function(){
@ -145,5 +177,50 @@
return false;
});
//login stuff
(function(){
var showLoginForm = function() {
var $dialog = $("#login-dialog");
if ($dialog.length > 0) {
$dialog.remove();
return false;
}
var $usernameInput = $("<input/>").attr({ type: "text", id: "login-username" });
var $passwordInput = $("<input/>").attr({ type: "password", id: "login-password" });
var $submit = $("<input/>").attr("type", "submit").css("display", "none");
var $form = $("<form/>").attr({ method: "post", action: "/login" }).submit(function() {
$.ajax("/login", {
type: "POST",
data: { username: $usernameInput.val(), password: $passwordInput.val() },
success: function(data, status, $xhr) {
if (data.Error !== null) {
alert(data.Error);
return;
}
$.vgquotes.refresh();
}
});
return false;
});
var $dialog = $("<div/>").addClass("dialog").attr("id", "login-dialog");
$form.append($usernameInput).append($passwordInput).append($submit);
$dialog.append($form);
$("body").append($dialog);
$dialog.center();
$usernameInput.focus();
return false;
};
$("#login-link").click(showLoginForm);
}());
});
}(jQuery, window));