Skip to content

Commit 53e0dfe

Browse files
committed
Move incomplete registrations to database
Affects issues: - Fixed #2286
1 parent 5c38143 commit 53e0dfe

13 files changed

Lines changed: 319 additions & 46 deletions

File tree

Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/RegistrationCommands.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class RegistrationCommands {
5656
private final ActiveCookieStore activeCookieStore;
5757
private final LinkCommands linkCommands;
5858
private final Confirmation confirmation;
59+
private final RegistrationBin registrationBin;
5960
private final PluginLogger logger;
6061
private final ErrorLogger errorLogger;
6162

@@ -66,7 +67,7 @@ public RegistrationCommands(
6667
DBSystem dbSystem,
6768
ActiveCookieStore activeCookieStore,
6869
LinkCommands linkCommands,
69-
Confirmation confirmation,
70+
Confirmation confirmation, RegistrationBin registrationBin,
7071
PluginLogger logger,
7172
ErrorLogger errorLogger
7273
) {
@@ -77,6 +78,7 @@ public RegistrationCommands(
7778
this.activeCookieStore = activeCookieStore;
7879
this.linkCommands = linkCommands;
7980
this.confirmation = confirmation;
81+
this.registrationBin = registrationBin;
8082
this.logger = logger;
8183
this.errorLogger = errorLogger;
8284
}
@@ -108,7 +110,7 @@ public void onRegister(CMDSender sender, @Untrusted Arguments arguments) {
108110

109111
public void registerUsingCode(CMDSender sender, @Untrusted String code, @Untrusted Arguments arguments) {
110112
UUID linkedToUUID = sender.getUUID().orElse(null);
111-
User user = RegistrationBin.register(code, linkedToUUID)
113+
User user = registrationBin.register(code, linkedToUUID)
112114
.orElseThrow(() -> new IllegalArgumentException(locale.getString(FailReason.USER_INFORMATION_NOT_FOUND)));
113115
String permissionGroup = getPermissionGroup(sender, arguments)
114116
.orElseThrow(() -> new IllegalArgumentException(locale.getString(FailReason.NO_PERMISSION_GROUP)));

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/WebServerSystem.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.djrapitops.plan.SubSystem;
2020
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
21+
import com.djrapitops.plan.delivery.webserver.auth.RegistrationBin;
2122
import com.djrapitops.plan.delivery.webserver.http.WebServer;
2223
import com.djrapitops.plan.storage.file.PublicHtmlFiles;
2324
import net.playeranalytics.plugin.server.PluginLogger;
@@ -35,19 +36,21 @@ public class WebServerSystem implements SubSystem {
3536

3637
private final Addresses addresses;
3738
private final ActiveCookieStore activeCookieStore;
39+
private final RegistrationBin registrationBin;
3840
private final PublicHtmlFiles publicHtmlFiles;
3941
private final WebServer webServer;
4042
private final PluginLogger logger;
4143

4244
@Inject
4345
public WebServerSystem(
4446
Addresses addresses,
45-
ActiveCookieStore activeCookieStore,
47+
ActiveCookieStore activeCookieStore, RegistrationBin registrationBin,
4648
PublicHtmlFiles publicHtmlFiles,
4749
WebServer webServer,
4850
PluginLogger logger) {
4951
this.addresses = addresses;
5052
this.activeCookieStore = activeCookieStore;
53+
this.registrationBin = registrationBin;
5154
this.publicHtmlFiles = publicHtmlFiles;
5255
this.webServer = webServer;
5356
this.logger = logger;
@@ -76,4 +79,16 @@ public WebServer getWebServer() {
7679
public Addresses getAddresses() {
7780
return addresses;
7881
}
82+
83+
public ActiveCookieStore getActiveCookieStore() {
84+
return activeCookieStore;
85+
}
86+
87+
public RegistrationBin getRegistrationBin() {
88+
return registrationBin;
89+
}
90+
91+
public PublicHtmlFiles getPublicHtmlFiles() {
92+
return publicHtmlFiles;
93+
}
7994
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* This file is part of Player Analytics (Plan).
3+
*
4+
* Plan is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Lesser General Public License v3 as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* Plan is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package com.djrapitops.plan.delivery.webserver.auth;
18+
19+
import com.djrapitops.plan.delivery.domain.auth.User;
20+
import com.djrapitops.plan.utilities.dev.Untrusted;
21+
22+
import java.util.Collections;
23+
import java.util.UUID;
24+
25+
/**
26+
* @author AuroraLS3
27+
*/
28+
public class IncompleteRegistration {
29+
@Untrusted
30+
private final String username;
31+
private final String passwordHash;
32+
private final String code;
33+
private final long expiresAfter;
34+
35+
public IncompleteRegistration(@Untrusted String username, String passwordHash, String code, long expiresAfter) {
36+
this.username = username;
37+
this.passwordHash = passwordHash;
38+
this.code = code;
39+
this.expiresAfter = expiresAfter;
40+
}
41+
42+
public User toUser(UUID linkedToUUID) {
43+
return new User(username, null, linkedToUUID, passwordHash, null, Collections.emptyList());
44+
}
45+
46+
public String getUsername() {
47+
return username;
48+
}
49+
50+
public String getPasswordHash() {
51+
return passwordHash;
52+
}
53+
54+
public String getCode() {
55+
return code;
56+
}
57+
58+
public long getExpiresAfter() {
59+
return expiresAfter;
60+
}
61+
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/RegistrationBin.java

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
package com.djrapitops.plan.delivery.webserver.auth;
1818

1919
import com.djrapitops.plan.delivery.domain.auth.User;
20+
import com.djrapitops.plan.storage.database.DBSystem;
21+
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
22+
import com.djrapitops.plan.storage.database.sql.tables.webuser.RegistrationTable;
23+
import com.djrapitops.plan.storage.database.transactions.webuser.StoreIncompleteRegistrationTransaction;
2024
import com.djrapitops.plan.utilities.PassEncryptUtil;
2125
import com.djrapitops.plan.utilities.dev.Untrusted;
22-
import com.github.benmanes.caffeine.cache.Cache;
23-
import com.github.benmanes.caffeine.cache.Caffeine;
2426
import org.apache.commons.codec.digest.DigestUtils;
2527

26-
import java.util.Collections;
28+
import javax.inject.Inject;
29+
import javax.inject.Singleton;
2730
import java.util.Optional;
2831
import java.util.UUID;
2932
import java.util.concurrent.TimeUnit;
@@ -33,46 +36,41 @@
3336
*
3437
* @author AuroraLS3
3538
*/
39+
@Singleton
3640
public class RegistrationBin {
3741

38-
private static final Cache<String, AwaitingForRegistration> REGISTRATION_BIN = Caffeine.newBuilder()
39-
.expireAfterAccess(15, TimeUnit.MINUTES)
40-
.build();
42+
private final DBSystem dbSystem;
4143

42-
private RegistrationBin() {
43-
// Hide static cache constructor
44+
@Inject
45+
public RegistrationBin(DBSystem dbSystem) {
46+
this.dbSystem = dbSystem;
4447
}
4548

46-
public static String addInfoForRegistration(@Untrusted String username, @Untrusted String password) {
49+
public String addInfoForRegistration(@Untrusted String username, @Untrusted String password) {
4750
String hash = PassEncryptUtil.createHash(password);
48-
String code = DigestUtils.sha256Hex(username + password + System.currentTimeMillis()).substring(0, 12);
49-
REGISTRATION_BIN.put(code, new AwaitingForRegistration(username, hash));
50-
return code;
51-
}
51+
String code = DigestUtils.sha256Hex(username + hash + System.currentTimeMillis()).substring(0, 12);
52+
long expiresAfter = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15L);
5253

53-
public static Optional<User> register(@Untrusted String code, UUID linkedToUUID) {
54-
AwaitingForRegistration found = REGISTRATION_BIN.getIfPresent(code);
55-
if (found == null) return Optional.empty();
56-
REGISTRATION_BIN.invalidate(code);
57-
return Optional.of(found.toUser(linkedToUUID));
54+
dbSystem.getDatabase().executeTransaction(new StoreIncompleteRegistrationTransaction(
55+
new IncompleteRegistration(username, hash, code, expiresAfter)
56+
));
57+
return code;
5858
}
5959

60-
public static boolean contains(@Untrusted String code) {
61-
return REGISTRATION_BIN.getIfPresent(code) != null;
60+
public Optional<User> register(@Untrusted String code, UUID linkedToUUID) {
61+
return dbSystem.getDatabase().query(WebUserQueries.fetchIncompleteRegistration(code))
62+
.map(registration -> {
63+
dbSystem.getDatabase().executeInTransaction(RegistrationTable.DELETE_BY_CODE, registration.getCode());
64+
return registration.toUser(linkedToUUID);
65+
});
6266
}
6367

64-
private static class AwaitingForRegistration {
65-
@Untrusted
66-
private final String username;
67-
private final String passwordHash;
68-
69-
public AwaitingForRegistration(@Untrusted String username, String passwordHash) {
70-
this.username = username;
71-
this.passwordHash = passwordHash;
72-
}
73-
74-
public User toUser(UUID linkedToUUID) {
75-
return new User(username, null, linkedToUUID, passwordHash, null, Collections.emptyList());
76-
}
68+
public boolean contains(@Untrusted String code) {
69+
// This method is used to check if registration was completed by the user.
70+
// There's a bug here where incomplete registration can be cleaned up before registration completes while register window is open
71+
// Which can lead to a confusing situation where user gets "Registration succeeded, you can now log in"-message
72+
// Even though the user was not actually registered.
73+
Optional<IncompleteRegistration> incompleteRegistration = dbSystem.getDatabase().query(WebUserQueries.fetchIncompleteRegistration(code));
74+
return incompleteRegistration.isPresent();
7775
}
7876
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/RegisterResolver.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@
4949
public class RegisterResolver implements NoAuthResolver {
5050

5151
private final DBSystem dbSystem;
52+
private final RegistrationBin registrationBin;
5253

5354
@Inject
54-
public RegisterResolver(DBSystem dbSystem) {
55+
public RegisterResolver(DBSystem dbSystem, RegistrationBin registrationBin) {
5556
this.dbSystem = dbSystem;
57+
this.registrationBin = registrationBin;
5658
}
5759

5860
@GET
@@ -85,7 +87,7 @@ public Response getResponse(Request request) {
8587
if (checkCode.isPresent()) {
8688
return Response.builder()
8789
.setStatus(200)
88-
.setJSONContent(Collections.singletonMap("success", !RegistrationBin.contains(checkCode.get())))
90+
.setJSONContent(Collections.singletonMap("success", !registrationBin.contains(checkCode.get())))
8991
.build();
9092
}
9193

@@ -97,7 +99,7 @@ public Response getResponse(Request request) {
9799

98100
@Untrusted String password = getPassword(form, query);
99101
try {
100-
String code = RegistrationBin.addInfoForRegistration(username, password);
102+
String code = registrationBin.addInfoForRegistration(username, password);
101103
return Response.builder()
102104
.setStatus(200)
103105
.setJSONContent(Maps.builder(String.class, Object.class)

Plan/common/src/main/java/com/djrapitops/plan/storage/database/Database.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,11 @@ protected void performOperations() {
138138
});
139139
}
140140

141-
default CompletableFuture<?> executeInTransaction(String sql) {
141+
default CompletableFuture<?> executeInTransaction(String sql, Object... parameters) {
142142
return executeInTransaction(new ExecStatement(sql) {
143143
@Override
144-
public void prepare(PreparedStatement statement) {
145-
// Nothing to prepare
144+
public void prepare(PreparedStatement statement) throws SQLException {
145+
QueryParameterSetter.setParameters(statement, parameters);
146146
}
147147
});
148148
}

Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.Preferences;
2121
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
2222
import com.djrapitops.plan.delivery.webserver.auth.CookieMetadata;
23+
import com.djrapitops.plan.delivery.webserver.auth.IncompleteRegistration;
2324
import com.djrapitops.plan.storage.database.queries.Query;
2425
import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
2526
import com.djrapitops.plan.storage.database.sql.building.Select;
@@ -169,8 +170,8 @@ private static User extractUser(ResultSet set) throws SQLException {
169170
String permissionGroup = set.getString(WebGroupTable.NAME);
170171
String userPermissions = set.getString("user_permissions");
171172
List<String> permissions = userPermissions != null ? Arrays.stream(userPermissions.split(","))
172-
.filter(permission -> !permission.isEmpty())
173-
.collect(Collectors.toList()) : List.of();
173+
.filter(permission -> !permission.isEmpty())
174+
.collect(Collectors.toList()) : List.of();
174175
return new User(username, linkedTo != null ? linkedTo : "console", linkedToUUID, passwordHash, permissionGroup, new HashSet<>(permissions));
175176
}
176177

@@ -288,4 +289,17 @@ public static Query<Set<Integer>> fetchPreferencesUserIds() {
288289
String sql = SELECT + WebUserPreferencesTable.WEB_USER_ID + FROM + WebUserPreferencesTable.TABLE_NAME;
289290
return db -> db.querySet(sql, row -> row.getInt(WebUserPreferencesTable.WEB_USER_ID));
290291
}
292+
293+
public static Query<Optional<IncompleteRegistration>> fetchIncompleteRegistration(@Untrusted String code) {
294+
return db -> db.queryOptional(RegistrationTable.SELECT_BY_CODE, WebUserQueries::extractIncompleteRegistration, code, System.currentTimeMillis());
295+
}
296+
297+
private static IncompleteRegistration extractIncompleteRegistration(ResultSet set) throws SQLException {
298+
return new IncompleteRegistration(
299+
set.getString(RegistrationTable.USERNAME),
300+
set.getString(RegistrationTable.SALT_PASSWORD_HASH),
301+
set.getString(RegistrationTable.CODE),
302+
set.getLong(RegistrationTable.EXPIRY_TIME)
303+
);
304+
}
291305
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* This file is part of Player Analytics (Plan).
3+
*
4+
* Plan is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Lesser General Public License v3 as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* Plan is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package com.djrapitops.plan.storage.database.sql.tables.webuser;
18+
19+
import com.djrapitops.plan.storage.database.DBType;
20+
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
21+
import com.djrapitops.plan.storage.database.sql.building.Insert;
22+
23+
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
24+
25+
/**
26+
* Represents plan_incomplete_registration table.
27+
*
28+
* @author AuroraLS3
29+
*/
30+
public class RegistrationTable {
31+
32+
public static final String TABLE_NAME = "plan_incomplete_registration";
33+
34+
public static final String ID = "id";
35+
public static final String USERNAME = "username";
36+
public static final String SALT_PASSWORD_HASH = "salted_password_hash";
37+
public static final String CODE = "code";
38+
public static final String EXPIRY_TIME = "expiry_time";
39+
40+
public static final String INSERT_STATEMENT = Insert.values(TABLE_NAME, USERNAME, SALT_PASSWORD_HASH, CODE, EXPIRY_TIME);
41+
42+
public static final String DELETE_EXPIRED = DELETE_FROM + TABLE_NAME + WHERE + EXPIRY_TIME + "<=?";
43+
public static final String DELETE_BY_CODE = DELETE_FROM + TABLE_NAME + WHERE + CODE + "=?";
44+
45+
public static final String SELECT_BY_CODE = SELECT + "*" + FROM + TABLE_NAME + WHERE + CODE + "=?" + AND + EXPIRY_TIME + ">?";
46+
47+
private RegistrationTable() {
48+
/* Static SQL utility class */
49+
}
50+
51+
public static String createTableSql(DBType dbType) {
52+
return CreateTableBuilder.create(TABLE_NAME, dbType)
53+
.column(ID, INT).primaryKey()
54+
.column(USERNAME, varchar(100)).notNull().unique()
55+
.column(SALT_PASSWORD_HASH, varchar(100)).notNull()
56+
.column(CODE, varchar(12)).notNull().unique()
57+
.column(EXPIRY_TIME, LONG).notNull()
58+
.toString();
59+
}
60+
}

Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public static String[] tableNames() {
5050
SecurityTable.TABLE_NAME,
5151
WebUserPreferencesTable.TABLE_NAME,
5252
PluginVersionTable.TABLE_NAME,
53-
AllowlistBounceTable.TABLE_NAME
53+
AllowlistBounceTable.TABLE_NAME,
54+
RegistrationTable.TABLE_NAME
5455
};
5556
}
5657

@@ -84,6 +85,7 @@ protected void performOperations() {
8485
execute(WebUserPreferencesTable.createTableSQL(dbType));
8586
execute(PluginVersionTable.createTableSQL(dbType));
8687
execute(AllowlistBounceTable.createTableSQL(dbType));
88+
execute(RegistrationTable.createTableSql(dbType));
8789

8890
// DataExtension tables
8991
execute(ExtensionIconTable.createTableSQL(dbType));

0 commit comments

Comments
 (0)