initial implementation of user accounts. need password hashing and end-user signup

This commit is contained in:
Andy Janata 2014-04-10 11:57:06 -07:00
parent 4dcf39f5b7
commit a7e6bc6e0d
10 changed files with 404 additions and 14 deletions

134
WebContent/adduser.jsp Normal file
View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8" ?>
<%--
Copyright (c) 2014, Andy Janata
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions
and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--%>
<%--
Bootstrapper to add first user accounts. All accounts created this way will be root administrator
accounts by default.
@author Andy Janata (ajanata@socialgamer.net)
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="com.google.inject.Injector" %>
<%@ page import="net.socialgamer.cah.RequestWrapper" %>
<%@ page import="net.socialgamer.cah.StartupUtils" %>
<%@ page import="net.socialgamer.cah.Constants" %>
<%@ page import="net.socialgamer.cah.db.Account" %>
<%@ page import="java.util.Date" %>
<%@ page import="org.hibernate.Session" %>
<%@ page import="org.hibernate.Transaction" %>
<%
RequestWrapper wrapper = new RequestWrapper(request);
if (!Constants.ADMIN_IP_ADDRESSES.contains(wrapper.getRemoteAddr())) {
response.sendError(403, "Access is restricted to known hosts");
return;
}
ServletContext servletContext = pageContext.getServletContext();
Injector injector = (Injector) servletContext.getAttribute(StartupUtils.INJECTOR);
String error = "";
String status = "";
final String save = request.getParameter("save");
final String username = request.getParameter("username");
final String password1 = request.getParameter("password1");
final String password2 = request.getParameter("password2");
final String email = request.getParameter("email");
if ("save".equals(save)
&& null != username && !username.isEmpty()
&& null != password1 && !password1.isEmpty()
&& null != password2 && !password2.isEmpty()
&& password1.equals(password2)) {
final Session s = injector.getInstance(Session.class);
// check for existing account
final Account existingAccount = Account.getAccount(s, username);
if (null != existingAccount) {
error = "Username already exists.";
} else {
final Transaction t = s.beginTransaction();
t.begin();
final Account newAccount = new Account();
newAccount.setUsername(username);
// FIXME hash password
newAccount.setPassword(password1);
newAccount.setEmail(email);
final Date now = new Date();
newAccount.setCreated(now);
newAccount.setLastSeen(now);
newAccount.setVerifiedPerson(true);
newAccount.setRoot(true);
try {
s.save(newAccount);
t.commit();
s.close();
status = "Created user " + username;
} catch (Exception e) {
error = e.getMessage();
t.rollback();
s.close();
}
}
} else if ("save".equals(save)) {
error = "Username, password1, and password2 are required. Password must match.";
}
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>PYX - Add Admin Accounts</title>
</head>
<body>
<span style="color:red"><%= error %></span>
<span style="color:blue"><%= status %></span>
<p>Passwords are stored in plain-text right now!</p>
<form method="post" action="adduser.jsp">
<input type="hidden" name="save" value="save" />
<label for="username">Username</label>
<input type="text" name="username" id="username" />
<br/>
<label for="password1">Password</label>
<input type="password" name="password1" id="password1" />
<br/>
<label for="password2">Password (again)</label>
<input type="password" name="password2" id="password2" />
<br/>
<label for="email">Email</label>
<input type="text" name="email" id="email" />
<br/>
<input type="submit" />
</form>
</body>
</html>

View File

@ -85,7 +85,7 @@ h2,h3,h4 {
#nickbox { #nickbox {
border: 1px solid black; border: 1px solid black;
display: inline; display: inline-block;
padding: 5px; padding: 5px;
} }

View File

@ -100,10 +100,18 @@ HttpSession hSession = request.getSession(true);
implementing a way for players to manage card sets in the game by themselves.</li> implementing a way for players to manage card sets in the game by themselves.</li>
</ul> </ul>
<div id="nickbox"> <div id="nickbox">
Nickname: <label for="nickname">Nickname:</label>
<input type="text" id="nickname" value="" maxlength="30" role="textbox" <input type="text" id="nickname" value="" maxlength="30" role="textbox"
aria-label="Enter your nickname." /> aria-label="Enter your nickname." />
<label for="password">Password (optional):</label>
<input type="password" id="password" value="" maxlength="128" role="textbox"
aria=label="Enter your password, if you have one." />
<input type="button" id="nicknameconfirm" value="Set" /> <input type="button" id="nicknameconfirm" value="Set" />
<br/>
Enter your password if you have registered your nickname, otherwise leave it blank.
<br/>
TODO account registration
<br/>
<span id="nickbox_error" class="error"></span> <span id="nickbox_error" class="error"></span>
</div> </div>
<p> <p>

View File

@ -41,7 +41,7 @@ $(document).ready(function() {
} }
$("#nicknameconfirm").click(nicknameconfirm_click); $("#nicknameconfirm").click(nicknameconfirm_click);
$("#nickbox").keyup(nickbox_keyup); $("#nickbox").keyup(nickbox_keyup);
$("#nickbox").focus(); $("#nickname").focus();
$(".chat", $("#tab-global")).keyup(chat_keyup($(".chat_submit", $("#tab-global")))); $(".chat", $("#tab-global")).keyup(chat_keyup($(".chat_submit", $("#tab-global"))));
$(".chat_submit", $("#tab-global")).click(chatsubmit_click(null, $("#tab-global"))); $(".chat_submit", $("#tab-global")).click(chatsubmit_click(null, $("#tab-global")));
@ -94,11 +94,12 @@ function nickbox_keyup(e) {
*/ */
function nicknameconfirm_click() { function nicknameconfirm_click() {
var nickname = $.trim($("#nickname").val()); var nickname = $.trim($("#nickname").val());
var password = $.trim($("#password").val());
$.cookie("nickname", nickname, { $.cookie("nickname", nickname, {
domain : cah.COOKIE_DOMAIN, domain : cah.COOKIE_DOMAIN,
expires : 365 expires : 365
}); });
cah.Ajax.build(cah.$.AjaxOperation.REGISTER).withNickname(nickname).run(); cah.Ajax.build(cah.$.AjaxOperation.REGISTER).withNickname(nickname).withPassword(password).run();
} }
/** /**

View File

@ -132,6 +132,7 @@ cah.$.ErrorCode.NO_NICK_SPECIFIED = "nns";
cah.$.ErrorCode.NOT_ADMIN = "na"; cah.$.ErrorCode.NOT_ADMIN = "na";
cah.$.ErrorCode.NOT_YOUR_TURN = "nyt"; cah.$.ErrorCode.NOT_YOUR_TURN = "nyt";
cah.$.ErrorCode.BANNED = "B&"; cah.$.ErrorCode.BANNED = "B&";
cah.$.ErrorCode.ACCOUNT_IS_REGISTERED = "air";
cah.$.ErrorCode.INVALID_NICK = "in"; cah.$.ErrorCode.INVALID_NICK = "in";
cah.$.ErrorCode.ALREADY_STARTED = "as"; cah.$.ErrorCode.ALREADY_STARTED = "as";
cah.$.ErrorCode.BAD_REQUEST = "br"; cah.$.ErrorCode.BAD_REQUEST = "br";
@ -142,6 +143,7 @@ cah.$.ErrorCode.ALREADY_STOPPED = "aS";
cah.$.ErrorCode.NOT_ENOUGH_PLAYERS = "nep"; cah.$.ErrorCode.NOT_ENOUGH_PLAYERS = "nep";
cah.$.ErrorCode.INVALID_GAME = "ig"; cah.$.ErrorCode.INVALID_GAME = "ig";
cah.$.ErrorCode.NO_MSG_SPECIFIED = "nms"; cah.$.ErrorCode.NO_MSG_SPECIFIED = "nms";
cah.$.ErrorCode.ACCOUNT_NOT_REGISTERED = "anr";
cah.$.ErrorCode.NOT_ENOUGH_CARDS = "nec"; cah.$.ErrorCode.NOT_ENOUGH_CARDS = "nec";
cah.$.ErrorCode_msg = {}; cah.$.ErrorCode_msg = {};
cah.$.ErrorCode_msg['tmg'] = "There are too many games already in progress. Either join an existing game, or wait for one to become available."; cah.$.ErrorCode_msg['tmg'] = "There are too many games already in progress. Either join an existing game, or wait for one to become available.";
@ -169,6 +171,7 @@ cah.$.ErrorCode_msg['nns'] = "No nickname specified.";
cah.$.ErrorCode_msg['ngh'] = "Only the game host can do that."; cah.$.ErrorCode_msg['ngh'] = "Only the game host can do that.";
cah.$.ErrorCode_msg['nec'] = "You must select at least one base card set."; cah.$.ErrorCode_msg['nec'] = "You must select at least one base card set.";
cah.$.ErrorCode_msg['serr'] = "An error occured on the server."; cah.$.ErrorCode_msg['serr'] = "An error occured on the server.";
cah.$.ErrorCode_msg['anr'] = "That nickname is not registered. Please register or do not user a password.";
cah.$.ErrorCode_msg['nsu'] = "No such user."; cah.$.ErrorCode_msg['nsu'] = "No such user.";
cah.$.ErrorCode_msg['wp'] = "That password is incorrect."; cah.$.ErrorCode_msg['wp'] = "That password is incorrect.";
cah.$.ErrorCode_msg['as'] = "The game has already started."; cah.$.ErrorCode_msg['as'] = "The game has already started.";
@ -179,6 +182,7 @@ cah.$.ErrorCode_msg['na'] = "You are not an administrator.";
cah.$.ErrorCode_msg['niu'] = "Nickname is already in use."; cah.$.ErrorCode_msg['niu'] = "Nickname is already in use.";
cah.$.ErrorCode_msg['B&'] = "Banned."; cah.$.ErrorCode_msg['B&'] = "Banned.";
cah.$.ErrorCode_msg['ad'] = "Access denied."; cah.$.ErrorCode_msg['ad'] = "Access denied.";
cah.$.ErrorCode_msg['air'] = "That nickname is registered. Please provide its password.";
cah.$.ErrorCode_msg['nj'] = "You are not the judge."; cah.$.ErrorCode_msg['nj'] = "You are not the judge.";
cah.$.GameInfo = function() { cah.$.GameInfo = function() {

View File

@ -14,6 +14,7 @@
<property name="show_sql">false</property> <property name="show_sql">false</property>
<property name="format_sql">false</property> <property name="format_sql">false</property>
<mapping class="net.socialgamer.cah.db.Account" />
<mapping class="net.socialgamer.cah.db.BlackCard" /> <mapping class="net.socialgamer.cah.db.BlackCard" />
<mapping class="net.socialgamer.cah.db.WhiteCard" /> <mapping class="net.socialgamer.cah.db.WhiteCard" />
<mapping class="net.socialgamer.cah.db.CardSet" /> <mapping class="net.socialgamer.cah.db.CardSet" />

View File

@ -289,6 +289,9 @@ public class Constants {
*/ */
public enum ErrorCode implements Localizable { public enum ErrorCode implements Localizable {
ACCESS_DENIED("ad", "Access denied."), ACCESS_DENIED("ad", "Access denied."),
ACCOUNT_IS_REGISTERED("air", "That nickname is registered. Please provide its password."),
ACCOUNT_NOT_REGISTERED("anr",
"That nickname is not registered. Please register or do not user a password."),
ALREADY_STARTED("as", "The game has already started."), ALREADY_STARTED("as", "The game has already started."),
ALREADY_STOPPED("aS", "The game has already stopped."), ALREADY_STOPPED("aS", "The game has already stopped."),
BAD_OP("bo", "Invalid operation."), BAD_OP("bo", "Invalid operation."),

View File

@ -30,6 +30,10 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.PriorityBlockingQueue;
import javax.annotation.Nullable;
import net.socialgamer.cah.db.Account;
/** /**
* A user connected to the server. * A user connected to the server.
@ -52,7 +56,8 @@ public class User {
private final String hostName; private final String hostName;
private final boolean isAdmin; @Nullable
private final Account account;
private final List<Long> lastMessageTimes = Collections.synchronizedList(new LinkedList<Long>()); private final List<Long> lastMessageTimes = Collections.synchronizedList(new LinkedList<Long>());
@ -68,13 +73,13 @@ public class User {
* The user's nickname. * The user's nickname.
* @param hostName * @param hostName
* The user's Internet hostname (which will likely just be their IP address). * The user's Internet hostname (which will likely just be their IP address).
* @param isAdmin * @param account
* Whether this user is an admin. * The user's account, if the nickname is registered.
*/ */
public User(final String nickname, final String hostName, final boolean isAdmin) { public User(final String nickname, final String hostName, @Nullable final Account account) {
this.nickname = nickname; this.nickname = nickname;
this.hostName = hostName; this.hostName = hostName;
this.isAdmin = isAdmin; this.account = account;
queuedMessages = new PriorityBlockingQueue<QueuedMessage>(); queuedMessages = new PriorityBlockingQueue<QueuedMessage>();
} }
@ -143,7 +148,7 @@ public class User {
} }
public boolean isAdmin() { public boolean isAdmin() {
return isAdmin; return null != account && account.isRoot();
} }
/** /**

View File

@ -0,0 +1,216 @@
/**
* Copyright (c) 2014, Andy Janata
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.socialgamer.cah.db;
import java.util.Date;
import javax.annotation.Nonnull;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.Session;
/**
* A user account.
*
* @author Andy Janata (ajanata@socialgamer.net)
*/
@Entity
@Table(name = "accounts")
public class Account {
@Id
@GeneratedValue
private int id;
// TODO constant this somewhere
@Column(length = 30, nullable = false)
private String username;
@Column(length = 100, nullable = true)
private String email;
private boolean emailVerified;
private boolean lockedOut;
// FIXME don't use plaintext once I have reference material
@Column(length = 30, nullable = false)
private String password;
// TODO tokens for email verification
/**
* Flag to indicate this well-known name is actually that person.
*/
private boolean verifiedPerson;
/**
* Super flag to say this user can do anything in the system.
*/
private boolean root;
private boolean canKickFromServer;
private boolean canBanFromServer;
private boolean canKickFromAnyGame;
private boolean canIgnoreGamePasswords;
private boolean canIgnoreGameLimits;
private boolean canIgnoreServerLimits;
@Column(nullable = false)
private Date created;
@Column(nullable = false)
private Date lastSeen;
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(final String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(final String email) {
this.email = email;
}
public boolean isEmailVerified() {
return emailVerified;
}
public void setEmailVerified(final boolean emailVerified) {
this.emailVerified = emailVerified;
}
public boolean isLockedOut() {
return lockedOut;
}
public void setLockedOut(final boolean lockedOut) {
this.lockedOut = lockedOut;
}
public String getPassword() {
return password;
}
public void setPassword(final String password) {
this.password = password;
}
public boolean isVerifiedPerson() {
return verifiedPerson;
}
public void setVerifiedPerson(final boolean verifiedPerson) {
this.verifiedPerson = verifiedPerson;
}
public boolean isRoot() {
return root;
}
public void setRoot(final boolean root) {
this.root = root;
}
public boolean isCanKickFromServer() {
return canKickFromServer;
}
public void setCanKickFromServer(final boolean canKickFromServer) {
this.canKickFromServer = canKickFromServer;
}
public boolean isCanBanFromServer() {
return canBanFromServer;
}
public void setCanBanFromServer(final boolean canBanFromServer) {
this.canBanFromServer = canBanFromServer;
}
public boolean isCanKickFromAnyGame() {
return canKickFromAnyGame;
}
public void setCanKickFromAnyGame(final boolean canKickFromAnyGame) {
this.canKickFromAnyGame = canKickFromAnyGame;
}
public boolean isCanIgnoreGamePasswords() {
return canIgnoreGamePasswords;
}
public void setCanIgnoreGamePasswords(final boolean canIgnoreGamePasswords) {
this.canIgnoreGamePasswords = canIgnoreGamePasswords;
}
public boolean isCanIgnoreGameLimits() {
return canIgnoreGameLimits;
}
public void setCanIgnoreGameLimits(final boolean canIgnoreGameLimits) {
this.canIgnoreGameLimits = canIgnoreGameLimits;
}
public boolean isCanIgnoreServerLimits() {
return canIgnoreServerLimits;
}
public void setCanIgnoreServerLimits(final boolean canIgnoreServerLimits) {
this.canIgnoreServerLimits = canIgnoreServerLimits;
}
public Date getCreated() {
return created;
}
public void setCreated(final Date created) {
this.created = created;
}
public Date getLastSeen() {
return lastSeen;
}
public void setLastSeen(final Date lastSeen) {
this.lastSeen = lastSeen;
}
public static Account getAccount(@Nonnull final Session session,
@Nonnull final String username) {
return (Account) session.createQuery("from Account where username = :username")
.setParameter("username", username).uniqueResult();
}
}

View File

@ -32,7 +32,6 @@ import javax.servlet.http.HttpSession;
import net.socialgamer.cah.CahModule.BanList; import net.socialgamer.cah.CahModule.BanList;
import net.socialgamer.cah.CahModule.MaxUsers; import net.socialgamer.cah.CahModule.MaxUsers;
import net.socialgamer.cah.Constants;
import net.socialgamer.cah.Constants.AjaxOperation; import net.socialgamer.cah.Constants.AjaxOperation;
import net.socialgamer.cah.Constants.AjaxRequest; import net.socialgamer.cah.Constants.AjaxRequest;
import net.socialgamer.cah.Constants.AjaxResponse; import net.socialgamer.cah.Constants.AjaxResponse;
@ -42,6 +41,9 @@ import net.socialgamer.cah.Constants.SessionAttribute;
import net.socialgamer.cah.RequestWrapper; import net.socialgamer.cah.RequestWrapper;
import net.socialgamer.cah.data.ConnectedUsers; import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.User; import net.socialgamer.cah.data.User;
import net.socialgamer.cah.db.Account;
import org.hibernate.Session;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -60,13 +62,15 @@ public class RegisterHandler extends Handler {
private final ConnectedUsers users; private final ConnectedUsers users;
private final Set<String> banList; private final Set<String> banList;
private final Integer maxUsers; private final Integer maxUsers;
private final Session hibernateSession;
@Inject @Inject
public RegisterHandler(final ConnectedUsers users, @BanList final Set<String> banList, public RegisterHandler(final ConnectedUsers users, @BanList final Set<String> banList,
@MaxUsers final Integer maxUsers) { @MaxUsers final Integer maxUsers, final Session session) {
this.users = users; this.users = users;
this.banList = banList; this.banList = banList;
this.maxUsers = maxUsers; this.maxUsers = maxUsers;
this.hibernateSession = session;
} }
@Override @Override
@ -87,8 +91,22 @@ public class RegisterHandler extends Handler {
} else if ("xyzzy".equalsIgnoreCase(nick)) { } else if ("xyzzy".equalsIgnoreCase(nick)) {
return error(ErrorCode.RESERVED_NICK); return error(ErrorCode.RESERVED_NICK);
} else { } else {
final User user = new User(nick, request.getRemoteAddr(), final String password = request.getParameter(AjaxRequest.PASSWORD);
Constants.ADMIN_IP_ADDRESSES.contains(request.getRemoteAddr())); final Account account = Account.getAccount(hibernateSession, nick);
if (null != account) {
if (null == password || password.isEmpty()) {
return error(ErrorCode.ACCOUNT_IS_REGISTERED);
} else if (!account.getPassword().equals(password)) {
// FIXME hash incoming password
// wrong password
return error(ErrorCode.WRONG_PASSWORD);
}
} else if (null != password && !password.isEmpty()) {
// no account found
return error(ErrorCode.ACCOUNT_NOT_REGISTERED);
}
final User user = new User(nick, request.getRemoteAddr(), account);
final ErrorCode errorCode = users.checkAndAdd(user, maxUsers); final ErrorCode errorCode = users.checkAndAdd(user, maxUsers);
if (null == errorCode) { if (null == errorCode) {
// There is a findbugs warning on this line: // There is a findbugs warning on this line: