Skip to content
This repository was archived by the owner on May 5, 2026. It is now read-only.

Commit 5a11c6f

Browse files
committed
fix: admin force login commands not clean/skip Dialog correctly for the user
1 parent ffeda2d commit 5a11c6f

7 files changed

Lines changed: 230 additions & 3 deletions

File tree

authme-core/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import fr.xephi.authme.permission.PermissionsManager;
77
import fr.xephi.authme.process.Management;
88
import fr.xephi.authme.service.BukkitService;
9+
import fr.xephi.authme.service.PreJoinDialogService;
910
import org.bukkit.command.CommandSender;
1011
import org.bukkit.entity.Player;
1112

1213
import javax.inject.Inject;
1314
import java.util.List;
15+
import java.util.Locale;
1416

1517
import static fr.xephi.authme.permission.PlayerPermission.CAN_LOGIN_BE_FORCED;
1618

@@ -31,13 +33,23 @@ public class ForceLoginCommand implements ExecutableCommand {
3133
@Inject
3234
private Messages messages;
3335

36+
@Inject
37+
private PreJoinDialogService preJoinDialogService;
38+
3439
@Override
3540
public void executeCommand(CommandSender sender, List<String> arguments) {
3641
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
3742

3843
Player player = bukkitService.getPlayerExact(playerName);
3944
if (player == null || !player.isOnline()) {
40-
messages.send(sender, MessageKey.FORCE_LOGIN_PLAYER_OFFLINE);
45+
// Player may be blocked in the pre-join dialog (Paper/Folia configuration phase).
46+
// Approving the force-login completes the blocking future so the player proceeds to
47+
// PLAY state, where AsynchronousJoin will call forceLogin() on their behalf.
48+
if (preJoinDialogService.approvePreJoinForceLogin(playerName.toLowerCase(Locale.ROOT))) {
49+
messages.send(sender, MessageKey.FORCE_LOGIN_SUCCESS, playerName);
50+
} else {
51+
messages.send(sender, MessageKey.FORCE_LOGIN_PLAYER_OFFLINE);
52+
}
4153
} else if (!permissionsManager.hasPermission(player, CAN_LOGIN_BE_FORCED)) {
4254
messages.send(sender, MessageKey.FORCE_LOGIN_FORBIDDEN, playerName);
4355
} else {

authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public void processJoin(Player player) {
126126
PreJoinDialogService.PendingRegistration pendingRegistration =
127127
preJoinDialogService.consumePendingRegistration(playerId);
128128
boolean shouldSkipPostJoinDialog = preJoinDialogService.consumeSkipPostJoinDialog(playerId);
129+
boolean pendingForceLogin = preJoinDialogService.consumePendingForceLogin(playerId);
129130

130131
if (!validationService.fulfillsNameRestrictions(player)) {
131132
handlePlayerWithUnmetNameRestriction(player, ip);
@@ -200,7 +201,7 @@ public void processJoin(Player player) {
200201
return;
201202
}
202203

203-
processJoinSync(player, isAuthAvailable, pendingLoginPassword, pendingRegistration, shouldSkipPostJoinDialog);
204+
processJoinSync(player, isAuthAvailable, pendingLoginPassword, pendingRegistration, shouldSkipPostJoinDialog, pendingForceLogin);
204205
}
205206

206207
private void handlePlayerWithUnmetNameRestriction(Player player, String ip) {
@@ -220,7 +221,7 @@ private void handlePlayerWithUnmetNameRestriction(Player player, String ip) {
220221
*/
221222
private void processJoinSync(Player player, boolean isAuthAvailable, String pendingLoginPassword,
222223
PreJoinDialogService.PendingRegistration pendingRegistration,
223-
boolean shouldSkipPostJoinDialog) {
224+
boolean shouldSkipPostJoinDialog, boolean pendingForceLogin) {
224225
int registrationTimeout = service.getProperty(
225226
isAuthAvailable ? RestrictionSettings.LOGIN_TIMEOUT : RestrictionSettings.REGISTER_TIMEOUT
226227
) * TICKS_PER_SECOND;
@@ -246,6 +247,10 @@ private void processJoinSync(Player player, boolean isAuthAvailable, String pend
246247
bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.login(player, pendingLoginPassword));
247248
return;
248249
}
250+
if (pendingForceLogin) {
251+
bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player));
252+
return;
253+
}
249254
if (!shouldSkipPostJoinDialog
250255
&& !playerCache.isAuthenticated(player.getName())
251256
&& service.getProperty(RegistrationSettings.USE_DIALOG_UI)

authme-core/src/main/java/fr/xephi/authme/service/PreJoinDialogService.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Map;
44
import java.util.Set;
55
import java.util.UUID;
6+
import java.util.concurrent.CompletableFuture;
67
import java.util.concurrent.ConcurrentHashMap;
78

89
/**
@@ -14,6 +15,12 @@ public class PreJoinDialogService {
1415
private final Map<UUID, PendingRegistration> pendingRegistrations = new ConcurrentHashMap<>();
1516
private final Set<UUID> skipPostJoinDialogs = ConcurrentHashMap.newKeySet();
1617

18+
// Pre-join force-login: tracks players blocked in the pre-join login dialog so that
19+
// ForceLoginCommand can unblock them without requiring the player to be in PLAY state.
20+
private final Map<String, UUID> pendingPreJoinByName = new ConcurrentHashMap<>();
21+
private final Map<UUID, CompletableFuture<String>> pendingPreJoinFutures = new ConcurrentHashMap<>();
22+
private final Set<UUID> pendingForceLogins = ConcurrentHashMap.newKeySet();
23+
1724
public PreJoinDialogService() {
1825
}
1926

@@ -45,10 +52,68 @@ public boolean consumeSkipPostJoinDialog(UUID playerId) {
4552
return skipPostJoinDialogs.remove(playerId);
4653
}
4754

55+
/**
56+
* Registers the blocking {@link CompletableFuture} used by the pre-join login dialog so that
57+
* {@link #approvePreJoinForceLogin} can resolve it from outside the event handler thread.
58+
*
59+
* @param normalizedName the player name in lowercase
60+
* @param uuid the player's UUID
61+
* @param future the future that blocks the configuration-phase thread
62+
*/
63+
public void registerPreJoinFuture(String normalizedName, UUID uuid, CompletableFuture<String> future) {
64+
pendingPreJoinByName.put(normalizedName, uuid);
65+
pendingPreJoinFutures.put(uuid, future);
66+
}
67+
68+
/**
69+
* Removes the pre-join future registration once the blocking wait is over.
70+
*
71+
* @param uuid the player's UUID
72+
*/
73+
public void unregisterPreJoinFuture(UUID uuid) {
74+
pendingPreJoinFutures.remove(uuid);
75+
pendingPreJoinByName.values().remove(uuid);
76+
}
77+
78+
/**
79+
* Approves a force-login for a player currently blocked in the pre-join login dialog.
80+
* Completes the blocking future with {@code null} (no kick message), allowing the player to
81+
* proceed to PLAY state where {@link #consumePendingForceLogin} will trigger a force-login.
82+
*
83+
* @param normalizedName the player name in lowercase
84+
* @return {@code true} if the player was in the pre-join dialog and the approval was registered,
85+
* {@code false} if no such player was found (e.g. already joined or not in dialog)
86+
*/
87+
public boolean approvePreJoinForceLogin(String normalizedName) {
88+
UUID uuid = pendingPreJoinByName.get(normalizedName);
89+
if (uuid == null) {
90+
return false;
91+
}
92+
CompletableFuture<String> future = pendingPreJoinFutures.get(uuid);
93+
if (future == null) {
94+
return false;
95+
}
96+
pendingForceLogins.add(uuid);
97+
future.complete(null);
98+
return true;
99+
}
100+
101+
/**
102+
* Consumes the force-login flag for the given player.
103+
*
104+
* @param playerId the player's UUID
105+
* @return {@code true} if a force-login was approved for this player (flag is cleared)
106+
*/
107+
public boolean consumePendingForceLogin(UUID playerId) {
108+
return pendingForceLogins.remove(playerId);
109+
}
110+
48111
public void clear(UUID playerId) {
49112
pendingLoginPasswords.remove(playerId);
50113
pendingRegistrations.remove(playerId);
51114
skipPostJoinDialogs.remove(playerId);
115+
pendingForceLogins.remove(playerId);
116+
unregisterPreJoinFuture(playerId);
52117
}
53118

54119
public record PendingRegistration(String primaryValue, String secondaryValue, boolean isEmailRegistration) {

authme-core/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import fr.xephi.authme.permission.PlayerPermission;
77
import fr.xephi.authme.process.Management;
88
import fr.xephi.authme.service.BukkitService;
9+
import fr.xephi.authme.service.PreJoinDialogService;
910
import org.bukkit.command.CommandSender;
1011
import org.bukkit.entity.Player;
1112
import org.junit.jupiter.api.Test;
@@ -43,6 +44,9 @@ class ForceLoginCommandTest {
4344
@Mock
4445
private Messages messages;
4546

47+
@Mock
48+
private PreJoinDialogService preJoinDialogService;
49+
4650
@Test
4751
void shouldRejectOfflinePlayer() {
4852
// given
@@ -131,6 +135,40 @@ void shouldForceLoginSenderSelf() {
131135
verify(messages).send(eq(sender), eq(MessageKey.FORCE_LOGIN_SUCCESS), eq(senderName));
132136
}
133137

138+
@Test
139+
void shouldForceLoginPlayerBlockedInPreJoinDialog() {
140+
// given
141+
String playerName = "Connor";
142+
given(bukkitService.getPlayerExact(playerName)).willReturn(null);
143+
given(preJoinDialogService.approvePreJoinForceLogin("connor")).willReturn(true);
144+
CommandSender sender = mock(CommandSender.class);
145+
146+
// when
147+
command.executeCommand(sender, Collections.singletonList(playerName));
148+
149+
// then
150+
verify(preJoinDialogService).approvePreJoinForceLogin("connor");
151+
verify(messages).send(eq(sender), eq(MessageKey.FORCE_LOGIN_SUCCESS), eq(playerName));
152+
verifyNoInteractions(management);
153+
}
154+
155+
@Test
156+
void shouldSendOfflineMessageWhenPlayerNotFoundAndNoPreJoinDialog() {
157+
// given
158+
String playerName = "NotConnecting";
159+
given(bukkitService.getPlayerExact(playerName)).willReturn(null);
160+
given(preJoinDialogService.approvePreJoinForceLogin("notconnecting")).willReturn(false);
161+
CommandSender sender = mock(CommandSender.class);
162+
163+
// when
164+
command.executeCommand(sender, Collections.singletonList(playerName));
165+
166+
// then
167+
verify(preJoinDialogService).approvePreJoinForceLogin("notconnecting");
168+
verify(messages).send(sender, MessageKey.FORCE_LOGIN_PLAYER_OFFLINE);
169+
verifyNoInteractions(management);
170+
}
171+
134172
private static Player mockPlayer(boolean isOnline) {
135173
Player player = mock(Player.class);
136174
given(player.isOnline()).willReturn(isOnline);

authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,25 @@ public void shouldProcessPendingPreJoinLoginInsteadOfShowingDialog() {
222222
verify(dialogAdapter, never()).showLoginDialog(eq(player), any(DialogWindowSpec.class));
223223
}
224224

225+
@Test
226+
public void shouldForceLoginPlayerApprovedViaPreJoinDialog() {
227+
// given
228+
Player player = mockPlayer("Bobby");
229+
setUpRegisteredJoin(player);
230+
java.util.UUID playerId = java.util.UUID.randomUUID();
231+
given(player.getUniqueId()).willReturn(playerId);
232+
given(preJoinDialogService.consumePendingForceLogin(playerId)).willReturn(true);
233+
234+
// when
235+
asynchronousJoin.processJoin(player);
236+
237+
// then
238+
verify(limboService).createLimboPlayer(player, true);
239+
verify(asynchronousLogin).forceLogin(player);
240+
verify(asynchronousLogin, never()).login(eq(player), any());
241+
verify(dialogAdapter, never()).showLoginDialog(eq(player), any());
242+
}
243+
225244
@Test
226245
public void shouldSkipPostJoinDialogWhenPreJoinDialogWasDeferred() {
227246
// given
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package fr.xephi.authme.service;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.UUID;
6+
import java.util.concurrent.CompletableFuture;
7+
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.Matchers.is;
10+
import static org.hamcrest.Matchers.nullValue;
11+
12+
/**
13+
* Tests for {@link PreJoinDialogService}.
14+
*/
15+
class PreJoinDialogServiceTest {
16+
17+
@Test
18+
void shouldStoreAndConsumePendingLoginPassword() {
19+
PreJoinDialogService service = new PreJoinDialogService();
20+
UUID uuid = UUID.randomUUID();
21+
22+
service.storePendingLoginPassword(uuid, "s3cr3t");
23+
24+
assertThat(service.consumePendingLoginPassword(uuid), is("s3cr3t"));
25+
assertThat(service.consumePendingLoginPassword(uuid), nullValue());
26+
}
27+
28+
@Test
29+
void shouldApprovePreJoinForceLoginAndCompleteFuture() {
30+
PreJoinDialogService service = new PreJoinDialogService();
31+
UUID uuid = UUID.randomUUID();
32+
CompletableFuture<String> future = new CompletableFuture<>();
33+
service.registerPreJoinFuture("bobby", uuid, future);
34+
35+
boolean result = service.approvePreJoinForceLogin("bobby");
36+
37+
assertThat(result, is(true));
38+
assertThat(future.isDone(), is(true));
39+
assertThat(future.getNow("sentinel"), is(nullValue()));
40+
assertThat(service.consumePendingForceLogin(uuid), is(true));
41+
assertThat(service.consumePendingForceLogin(uuid), is(false));
42+
}
43+
44+
@Test
45+
void shouldReturnFalseForApproveWhenNoPreJoinDialogPending() {
46+
PreJoinDialogService service = new PreJoinDialogService();
47+
48+
assertThat(service.approvePreJoinForceLogin("nobody"), is(false));
49+
}
50+
51+
@Test
52+
void shouldReturnFalseForConsumeForceLoginWhenNotApproved() {
53+
PreJoinDialogService service = new PreJoinDialogService();
54+
55+
assertThat(service.consumePendingForceLogin(UUID.randomUUID()), is(false));
56+
}
57+
58+
@Test
59+
void shouldNotApproveAfterUnregister() {
60+
PreJoinDialogService service = new PreJoinDialogService();
61+
UUID uuid = UUID.randomUUID();
62+
CompletableFuture<String> future = new CompletableFuture<>();
63+
service.registerPreJoinFuture("alice", uuid, future);
64+
service.unregisterPreJoinFuture(uuid);
65+
66+
boolean result = service.approvePreJoinForceLogin("alice");
67+
68+
assertThat(result, is(false));
69+
assertThat(future.isDone(), is(false));
70+
}
71+
72+
@Test
73+
void shouldClearAllStateForPlayer() {
74+
PreJoinDialogService service = new PreJoinDialogService();
75+
UUID uuid = UUID.randomUUID();
76+
CompletableFuture<String> future = new CompletableFuture<>();
77+
service.storePendingLoginPassword(uuid, "pw");
78+
service.registerPreJoinFuture("charlie", uuid, future);
79+
80+
service.clear(uuid);
81+
82+
assertThat(service.consumePendingLoginPassword(uuid), is(nullValue()));
83+
assertThat(service.approvePreJoinForceLogin("charlie"), is(false));
84+
assertThat(service.consumePendingForceLogin(uuid), is(false));
85+
}
86+
}

authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,13 @@ private void handleBlockingLoginDialog(PlayerConfigurationConnection connection,
170170
loginResponse.completeOnTimeout(
171171
messages.retrieveSingle(playerName, MessageKey.LOGIN_TIMEOUT_ERROR), timeoutSeconds, TimeUnit.SECONDS);
172172
pendingLoginResponses.put(playerId, loginResponse);
173+
preJoinDialogService.registerPreJoinFuture(playerName.toLowerCase(java.util.Locale.ROOT), playerId, loginResponse);
173174

174175
connection.getAudience().showDialog(
175176
PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName)));
176177
String kickMessage = loginResponse.join();
177178
pendingLoginResponses.remove(playerId);
179+
preJoinDialogService.unregisterPreJoinFuture(playerId);
178180
connection.getAudience().closeDialog();
179181

180182
if (kickMessage != null) {

0 commit comments

Comments
 (0)