#472 Create recovery code/expiration columns and methods in data source
This commit is contained in:
parent
ffc5b77f36
commit
0aac8928af
@ -235,6 +235,24 @@ public class CacheDataSource implements DataSource {
|
|||||||
return source.getAllAuths();
|
return source.getAllAuths();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecoveryCode(String name, String code, long expiration) {
|
||||||
|
source.setRecoveryCode(name, code, expiration);
|
||||||
|
cachedAuths.refresh(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRecoveryCode(String name) {
|
||||||
|
// TODO #472: can probably get it from the cached Auth?
|
||||||
|
return source.getRecoveryCode(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRecoveryCode(String name) {
|
||||||
|
source.removeRecoveryCode(name);
|
||||||
|
cachedAuths.refresh(name);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PlayerAuth> getLoggedPlayers() {
|
public List<PlayerAuth> getLoggedPlayers() {
|
||||||
return new ArrayList<>(PlayerCache.getInstance().getCache().values());
|
return new ArrayList<>(PlayerCache.getInstance().getCache().values());
|
||||||
|
|||||||
@ -22,6 +22,8 @@ public final class Columns {
|
|||||||
public final String EMAIL;
|
public final String EMAIL;
|
||||||
public final String ID;
|
public final String ID;
|
||||||
public final String IS_LOGGED;
|
public final String IS_LOGGED;
|
||||||
|
public final String RECOVERY_CODE;
|
||||||
|
public final String RECOVERY_EXPIRATION;
|
||||||
|
|
||||||
public Columns(Settings settings) {
|
public Columns(Settings settings) {
|
||||||
NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
|
NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
|
||||||
@ -38,6 +40,8 @@ public final class Columns {
|
|||||||
EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL);
|
EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL);
|
||||||
ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID);
|
ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID);
|
||||||
IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED);
|
IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED);
|
||||||
|
RECOVERY_CODE = settings.getProperty(DatabaseSettings.MYSQL_COL_RECOVERY_CODE);
|
||||||
|
RECOVERY_EXPIRATION = settings.getProperty(DatabaseSettings.MYSQL_COL_RECOVERY_EXPIRATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,6 +194,30 @@ public interface DataSource extends Reloadable {
|
|||||||
*/
|
*/
|
||||||
List<PlayerAuth> getAllAuths();
|
List<PlayerAuth> getAllAuths();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the password recovery code for a user.
|
||||||
|
*
|
||||||
|
* @param name The name of the user
|
||||||
|
* @param code The recovery code
|
||||||
|
* @param expiration Recovery code expiration (milliseconds timestamp)
|
||||||
|
*/
|
||||||
|
void setRecoveryCode(String name, String code, long expiration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the recovery code of a user if available and not yet expired.
|
||||||
|
*
|
||||||
|
* @param name The name of the user
|
||||||
|
* @return The recovery code, or null if no current code available
|
||||||
|
*/
|
||||||
|
String getRecoveryCode(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the recovery code of a given user.
|
||||||
|
*
|
||||||
|
* @param name The name of the user
|
||||||
|
*/
|
||||||
|
void removeRecoveryCode(String name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload the data source.
|
* Reload the data source.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -468,6 +468,21 @@ public class FlatFile implements DataSource {
|
|||||||
throw new UnsupportedOperationException("Flat file no longer supported");
|
throw new UnsupportedOperationException("Flat file no longer supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecoveryCode(String name, String code, long expiration) {
|
||||||
|
throw new UnsupportedOperationException("Flat file no longer supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRecoveryCode(String name) {
|
||||||
|
throw new UnsupportedOperationException("Flat file no longer supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRecoveryCode(String name) {
|
||||||
|
throw new UnsupportedOperationException("Flat file no longer supported");
|
||||||
|
}
|
||||||
|
|
||||||
private static PlayerAuth buildAuthFromArray(String[] args) {
|
private static PlayerAuth buildAuthFromArray(String[] args) {
|
||||||
// Format allows 2, 3, 4, 7, 8, 9 fields. Anything else is unknown
|
// Format allows 2, 3, 4, 7, 8, 9 fields. Anything else is unknown
|
||||||
if (args.length >= 2 && args.length <= 9 && args.length != 5 && args.length != 6) {
|
if (args.length >= 2 && args.length <= 9 && args.length != 5 && args.length != 6) {
|
||||||
|
|||||||
@ -208,6 +208,14 @@ public class MySQL implements DataSource {
|
|||||||
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
|
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
|
||||||
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL);
|
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isColumnMissing(md, col.RECOVERY_CODE)) {
|
||||||
|
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_CODE + " VARCHAR(20);");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isColumnMissing(md, col.RECOVERY_EXPIRATION)) {
|
||||||
|
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_EXPIRATION + " BIGINT;");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ConsoleLogger.info("MySQL setup finished");
|
ConsoleLogger.info("MySQL setup finished");
|
||||||
}
|
}
|
||||||
@ -856,6 +864,54 @@ public class MySQL implements DataSource {
|
|||||||
return auths;
|
return auths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecoveryCode(String name, String code, long expiration) {
|
||||||
|
String sql = "UPDATE " + tableName
|
||||||
|
+ " SET " + col.RECOVERY_CODE + " = ?, "
|
||||||
|
+ col.RECOVERY_EXPIRATION + " = ?"
|
||||||
|
+ " WHERE " + col.NAME + " = ?;";
|
||||||
|
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
|
||||||
|
pst.setString(1, code);
|
||||||
|
pst.setLong(2, expiration);
|
||||||
|
pst.setString(3, name.toLowerCase());
|
||||||
|
pst.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logSqlException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRecoveryCode(String name) {
|
||||||
|
String sql = "SELECT " + col.RECOVERY_CODE + " FROM " + tableName
|
||||||
|
+ " WHERE " + col.NAME + " = ? AND " + col.RECOVERY_EXPIRATION + " > ?;";
|
||||||
|
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
|
||||||
|
pst.setString(1, name.toLowerCase());
|
||||||
|
pst.setLong(2, System.currentTimeMillis());
|
||||||
|
try (ResultSet rs = pst.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getString(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logSqlException(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRecoveryCode(String name) {
|
||||||
|
String sql = "UPDATE " + tableName
|
||||||
|
+ " SET " + col.RECOVERY_CODE + " = NULL"
|
||||||
|
+ " AND " + col.RECOVERY_EXPIRATION + " = NULL"
|
||||||
|
+ " WHERE " + col.NAME + " = ?;";
|
||||||
|
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
|
||||||
|
pst.setString(1, name.toLowerCase());
|
||||||
|
pst.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logSqlException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
|
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
|
||||||
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
|
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
|
||||||
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
|
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
|
||||||
|
|||||||
@ -127,6 +127,14 @@ public class SQLite implements DataSource {
|
|||||||
if (isColumnMissing(md, col.IS_LOGGED)) {
|
if (isColumnMissing(md, col.IS_LOGGED)) {
|
||||||
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';");
|
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isColumnMissing(md, col.RECOVERY_CODE)) {
|
||||||
|
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_CODE + " VARCHAR(20);");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isColumnMissing(md, col.RECOVERY_EXPIRATION)) {
|
||||||
|
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_EXPIRATION + " BIGINT;");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ConsoleLogger.info("SQLite Setup finished");
|
ConsoleLogger.info("SQLite Setup finished");
|
||||||
}
|
}
|
||||||
@ -586,6 +594,54 @@ public class SQLite implements DataSource {
|
|||||||
return auths;
|
return auths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecoveryCode(String name, String code, long expiration) {
|
||||||
|
String sql = "UPDATE " + tableName
|
||||||
|
+ " SET " + col.RECOVERY_CODE + " = ?, "
|
||||||
|
+ col.RECOVERY_EXPIRATION + " = ?"
|
||||||
|
+ " WHERE " + col.NAME + " = ?;";
|
||||||
|
try (PreparedStatement pst = con.prepareStatement(sql)) {
|
||||||
|
pst.setString(1, code);
|
||||||
|
pst.setLong(2, expiration);
|
||||||
|
pst.setString(3, name.toLowerCase());
|
||||||
|
pst.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logSqlException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRecoveryCode(String name) {
|
||||||
|
String sql = "SELECT " + col.RECOVERY_CODE + " FROM " + tableName
|
||||||
|
+ " WHERE " + col.NAME + " = ? AND " + col.RECOVERY_EXPIRATION + " > ?;";
|
||||||
|
try (PreparedStatement pst = con.prepareStatement(sql)) {
|
||||||
|
pst.setString(1, name.toLowerCase());
|
||||||
|
pst.setLong(2, System.currentTimeMillis());
|
||||||
|
try (ResultSet rs = pst.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getString(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logSqlException(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRecoveryCode(String name) {
|
||||||
|
String sql = "UPDATE " + tableName
|
||||||
|
+ " SET " + col.RECOVERY_CODE + " = NULL"
|
||||||
|
+ " AND " + col.RECOVERY_EXPIRATION + " = NULL"
|
||||||
|
+ " WHERE " + col.NAME + " = ?;";
|
||||||
|
try (PreparedStatement pst = con.prepareStatement(sql)) {
|
||||||
|
pst.setString(1, name.toLowerCase());
|
||||||
|
pst.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logSqlException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
|
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
|
||||||
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
|
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,14 @@ public class DatabaseSettings implements SettingsHolder {
|
|||||||
public static final Property<String> MYSQL_COL_GROUP =
|
public static final Property<String> MYSQL_COL_GROUP =
|
||||||
newProperty("ExternalBoardOptions.mySQLColumnGroup", "");
|
newProperty("ExternalBoardOptions.mySQLColumnGroup", "");
|
||||||
|
|
||||||
|
@Comment("Column for storing recovery code (when password lost)")
|
||||||
|
public static final Property<String> MYSQL_COL_RECOVERY_CODE =
|
||||||
|
newProperty("DataSource.mySQLrecoveryCode", "recoverycode");
|
||||||
|
|
||||||
|
@Comment("Column for storing recovery code expiration")
|
||||||
|
public static final Property<String> MYSQL_COL_RECOVERY_EXPIRATION =
|
||||||
|
newProperty("DataSource.mySQLrecoveryExpiration", "recoveryexpiration");
|
||||||
|
|
||||||
private DatabaseSettings() {
|
private DatabaseSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,10 @@ DataSource:
|
|||||||
mySQLlastlocWorld: world
|
mySQLlastlocWorld: world
|
||||||
# Column for RealName
|
# Column for RealName
|
||||||
mySQLRealName: realname
|
mySQLRealName: realname
|
||||||
|
# Column for storing recovery code (when password lost)
|
||||||
|
mySQLrecoveryCode: recoverycode
|
||||||
|
# Column for storing recovery code expiration
|
||||||
|
mySQLrecoveryExpiration: recoveryexpiration
|
||||||
settings:
|
settings:
|
||||||
# The name shown in the help messages.
|
# The name shown in the help messages.
|
||||||
helpHeader: AuthMeReloaded
|
helpHeader: AuthMeReloaded
|
||||||
|
|||||||
@ -126,6 +126,17 @@ public final class TestHelper {
|
|||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set ConsoleLogger to use a new real logger.
|
||||||
|
*
|
||||||
|
* @return The real logger used by ConsoleLogger
|
||||||
|
*/
|
||||||
|
public static Logger setRealLogger() {
|
||||||
|
Logger logger = Logger.getAnonymousLogger();
|
||||||
|
ConsoleLogger.setLogger(logger);
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that a class only has a hidden, zero-argument constructor, preventing the
|
* Check that a class only has a hidden, zero-argument constructor, preventing the
|
||||||
* instantiation of such classes (utility classes). Invokes the hidden constructor
|
* instantiation of such classes (utility classes). Invokes the hidden constructor
|
||||||
|
|||||||
@ -382,4 +382,47 @@ public abstract class AbstractDataSourceIntegrationTest {
|
|||||||
assertThat(dataSource.getAllAuths(), empty());
|
assertThat(dataSource.getAllAuths(), empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSetRecoveryCode() {
|
||||||
|
// given
|
||||||
|
DataSource dataSource = getDataSource();
|
||||||
|
String name = "Bobby";
|
||||||
|
String code = "A123BC";
|
||||||
|
|
||||||
|
// when
|
||||||
|
dataSource.setRecoveryCode(name, code, System.currentTimeMillis() + 100_000L);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(dataSource.getRecoveryCode(name), equalTo(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRemoveRecoveryCode() {
|
||||||
|
// given
|
||||||
|
String name = "User";
|
||||||
|
DataSource dataSource = getDataSource();
|
||||||
|
dataSource.setRecoveryCode(name, "code", System.currentTimeMillis() + 20_000L);
|
||||||
|
|
||||||
|
// when
|
||||||
|
dataSource.removeRecoveryCode(name);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(dataSource.getRecoveryCode(name), nullValue());
|
||||||
|
assertThat(dataSource.getRecoveryCode("bobby"), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotReturnRecoveryCodeIfExpired() {
|
||||||
|
// given
|
||||||
|
String name = "user";
|
||||||
|
DataSource dataSource = getDataSource();
|
||||||
|
dataSource.setRecoveryCode(name, "123456", System.currentTimeMillis() - 2_000L);
|
||||||
|
|
||||||
|
// when
|
||||||
|
String code = dataSource.getRecoveryCode(name);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(code, nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest {
|
|||||||
});
|
});
|
||||||
set(DatabaseSettings.MYSQL_DATABASE, "h2_test");
|
set(DatabaseSettings.MYSQL_DATABASE, "h2_test");
|
||||||
set(DatabaseSettings.MYSQL_TABLE, "authme");
|
set(DatabaseSettings.MYSQL_TABLE, "authme");
|
||||||
TestHelper.setupLogger();
|
TestHelper.setRealLogger();
|
||||||
|
|
||||||
Path sqlInitFile = TestHelper.getJarPath(TestHelper.PROJECT_ROOT + "datasource/sql-initialize.sql");
|
Path sqlInitFile = TestHelper.getJarPath(TestHelper.PROJECT_ROOT + "datasource/sql-initialize.sql");
|
||||||
sqlInitialize = new String(Files.readAllBytes(sqlInitFile));
|
sqlInitialize = new String(Files.readAllBytes(sqlInitFile));
|
||||||
|
|||||||
@ -56,7 +56,7 @@ public class SQLiteIntegrationTest extends AbstractDataSourceIntegrationTest {
|
|||||||
});
|
});
|
||||||
set(DatabaseSettings.MYSQL_DATABASE, "sqlite-test");
|
set(DatabaseSettings.MYSQL_DATABASE, "sqlite-test");
|
||||||
set(DatabaseSettings.MYSQL_TABLE, "authme");
|
set(DatabaseSettings.MYSQL_TABLE, "authme");
|
||||||
TestHelper.setupLogger();
|
TestHelper.setRealLogger();
|
||||||
|
|
||||||
Path sqlInitFile = TestHelper.getJarPath(TestHelper.PROJECT_ROOT + "datasource/sql-initialize.sql");
|
Path sqlInitFile = TestHelper.getJarPath(TestHelper.PROJECT_ROOT + "datasource/sql-initialize.sql");
|
||||||
// Note ljacqu 20160221: It appears that we can only run one statement per Statement.execute() so we split
|
// Note ljacqu 20160221: It appears that we can only run one statement per Statement.execute() so we split
|
||||||
|
|||||||
@ -13,6 +13,8 @@ CREATE TABLE authme (
|
|||||||
email VARCHAR(255) DEFAULT 'your@email.com',
|
email VARCHAR(255) DEFAULT 'your@email.com',
|
||||||
isLogged INT DEFAULT '0', realname VARCHAR(255) NOT NULL DEFAULT 'Player',
|
isLogged INT DEFAULT '0', realname VARCHAR(255) NOT NULL DEFAULT 'Player',
|
||||||
salt varchar(255),
|
salt varchar(255),
|
||||||
|
recoverycode VARCHAR(20),
|
||||||
|
recoveryexpiration BIGINT,
|
||||||
CONSTRAINT table_const_prim PRIMARY KEY (id)
|
CONSTRAINT table_const_prim PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user