diff --git a/Plan/common/src/main/java/com/djrapitops/plan/commands/PlanCommand.java b/Plan/common/src/main/java/com/djrapitops/plan/commands/PlanCommand.java index bff59256c..ed1d5d207 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/commands/PlanCommand.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/commands/PlanCommand.java @@ -105,6 +105,7 @@ public class PlanCommand { .subcommand(registerCommand()) .subcommand(unregisterCommand()) + .subcommand(logoutCommand()) .subcommand(webUsersCommand()) .subcommand(acceptCommand()) @@ -239,6 +240,17 @@ public class PlanCommand { .build(); } + private Subcommand logoutCommand() { + return Subcommand.builder() + .requirePermission(Permissions.LOGOUT_OTHER) + .requiredArgument(locale.getString(HelpLang.ARG_USERNAME), locale.getString(HelpLang.DESC_ARG_USERNAME)) + .description(locale.getString(HelpLang.LOGOUT)) + .inDepthDescription(locale.getString(DeepHelpLang.LOGOUT)) + .onCommand(registrationCommands::onLogoutCommand) + .onTabComplete(this::webUserNames) + .build(); + } + private List webUserNames(CMDSender sender, Arguments arguments) { if (!sender.hasPermission(Permissions.UNREGISTER_OTHER)) { return Collections.emptyList(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/RegistrationCommands.java b/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/RegistrationCommands.java index d59d1b3cf..c6f0c3e3f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/RegistrationCommands.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/RegistrationCommands.java @@ -234,4 +234,20 @@ public class RegistrationCommands { } }); } + + + public void onLogoutCommand(CMDSender sender, Arguments arguments) { + Optional username = arguments.get(0); + if (!username.isPresent()) { + throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ONE_ARG, locale.getString(HelpLang.ARG_USERNAME) + "/*")); + } + + String loggingOut = username.get(); + + if ("*".equals(loggingOut)) { + activeCookieStore.removeAll(); + } else { + ActiveCookieStore.removeUserCookie(loggingOut); + } + } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java index 59cc44e34..16018966c 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java @@ -25,14 +25,13 @@ import com.djrapitops.plan.storage.database.DBSystem; import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; import com.djrapitops.plan.storage.database.transactions.events.CookieChangeTransaction; import net.playeranalytics.plugin.scheduling.RunnableFactory; +import net.playeranalytics.plugin.scheduling.Task; import net.playeranalytics.plugin.scheduling.TimeAmount; import org.apache.commons.codec.digest.DigestUtils; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -47,6 +46,8 @@ public class ActiveCookieStore implements SubSystem { private final RunnableFactory runnableFactory; private final Processing processing; + private final Collection expiryTasks; + @Inject public ActiveCookieStore( PlanConfig config, @@ -60,6 +61,8 @@ public class ActiveCookieStore implements SubSystem { this.dbSystem = dbSystem; this.processing = processing; this.runnableFactory = runnableFactory; + + expiryTasks = new ArrayList<>(); } private static void removeCookieStatic(String cookie) { @@ -84,14 +87,22 @@ public class ActiveCookieStore implements SubSystem { USERS_BY_COOKIE.putAll(dbSystem.getDatabase().query(WebUserQueries.fetchActiveCookies())); for (Map.Entry entry : dbSystem.getDatabase().query(WebUserQueries.getCookieExpiryTimes()).entrySet()) { long timeToExpiry = Math.max(entry.getValue() - System.currentTimeMillis(), 0L); - runnableFactory.create(() -> removeCookie(entry.getKey())) - .runTaskLaterAsynchronously(TimeAmount.toTicks(timeToExpiry, TimeUnit.MILLISECONDS)); + expiryTasks.add(runnableFactory.create(() -> removeCookie(entry.getKey())) + .runTaskLaterAsynchronously(TimeAmount.toTicks(timeToExpiry, TimeUnit.MILLISECONDS))); } } @Override public void disable() { USERS_BY_COOKIE.clear(); + expiryTasks.forEach(task -> { + try { + task.cancel(); + } catch (Exception e) { + // Ignore, task has already been cancelled + } + }); + expiryTasks.clear(); } public Optional checkCookie(String cookie) { @@ -102,6 +113,8 @@ public class ActiveCookieStore implements SubSystem { String cookie = DigestUtils.sha256Hex(user.getUsername() + UUID.randomUUID() + System.currentTimeMillis()); USERS_BY_COOKIE.put(cookie, user); saveNewCookie(user, cookie, System.currentTimeMillis()); + expiryTasks.add(runnableFactory.create(() -> removeCookie(cookie)) + .runTaskLaterAsynchronously(TimeAmount.toTicks(cookieExpiresAfter, TimeUnit.MILLISECONDS))); return cookie; } @@ -122,4 +135,9 @@ public class ActiveCookieStore implements SubSystem { private void deleteCookie(String username) { dbSystem.getDatabase().executeTransaction(CookieChangeTransaction.removeCookie(username)); } + + public void removeAll() { + disable(); + dbSystem.getDatabase().executeTransaction(CookieChangeTransaction.removeAll()); + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/Permissions.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/Permissions.java index 116aca7af..2f691da1e 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/Permissions.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/Permissions.java @@ -34,6 +34,7 @@ public enum Permissions { REGISTER_OTHER("plan.register.other"), UNREGISTER_SELF("plan.unregister.self"), UNREGISTER_OTHER("plan.unregister.other"), + LOGOUT_OTHER("plan.logout.other"), INFO("plan.info"), RELOAD("plan.reload"), DISABLE("plan.disable"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/DeepHelpLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/DeepHelpLang.java index 9971074b3..f3166235a 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/DeepHelpLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/DeepHelpLang.java @@ -31,6 +31,7 @@ public enum DeepHelpLang implements Lang { INGAME("In Depth Help - /plan ingame", "Displays some information about the player in game."), REGISTER("In Depth Help - /plan register", "Use without arguments to get link to register page. Use --code [code] after registration to get a user."), UNREGISTER("In Depth Help - /plan unregister", "Use without arguments to unregister player linked user, or with username argument to unregister another user."), + LOGOUT("In Depth Help - /plan logout", "Give username argument to log out another user from the panel, give * as argument to log out everyone."), INFO("In Depth Help - /plan info", "Display the current status of the plugin."), RELOAD("In Depth Help - /plan reload", "Disable and enable the plugin to reload any changes in config."), DISABLE("In Depth Help - /plan disable", "Disable the plugin or part of it until next reload/restart."), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HelpLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HelpLang.java index 07f909eb6..27789b269 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HelpLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HelpLang.java @@ -69,7 +69,8 @@ public enum HelpLang implements Lang { DB_UNINSTALLED("Command Help - /plan db uninstalled", "Set a server as uninstalled in the database."), EXPORT("Command Help - /plan export", "Export html or json files manually"), IMPORT("Command Help - /plan import", "Import data"), - JSON("Command Help - /plan json", "View json of Player's raw data."); + JSON("Command Help - /plan json", "View json of Player's raw data."), + LOGOUT("Command Help - /plan logout", "Log out other users from the panel."); private final String identifier; private final String defaultValue; diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java index 7bba64518..589a24973 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java @@ -46,6 +46,8 @@ public class CookieTable { public static final String DELETE_OLDER_STATEMENT = "DELETE FROM " + TABLE_NAME + WHERE + EXPIRES + "<=?"; + public static final String DELETE_ALL_STATEMENT = "DELETE FROM " + TABLE_NAME; + private CookieTable() { /* Static information class */ diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/RemoveEverythingTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/RemoveEverythingTransaction.java index 0e1d7e6f8..4fe81d757 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/RemoveEverythingTransaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/RemoveEverythingTransaction.java @@ -45,6 +45,7 @@ public class RemoveEverythingTransaction extends ThrowawayTransaction { clearTable(TPSTable.TABLE_NAME); clearTable(SecurityTable.TABLE_NAME); clearTable(ServerTable.TABLE_NAME); + clearTable(CookieTable.TABLE_NAME); clearTable(ExtensionPlayerValueTable.TABLE_NAME); clearTable(ExtensionServerValueTable.TABLE_NAME); clearTable(ExtensionGroupsTable.TABLE_NAME); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java index 800531f3c..827f55c53 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java @@ -43,9 +43,20 @@ public class CookieChangeTransaction extends Transaction { return new CookieChangeTransaction(username, null, null); } + public static CookieChangeTransaction removeAll() { + return new CookieChangeTransaction(null, null, null); + } + @Override protected void performOperations() { - if (cookie == null) { + if (username == null) { + execute(new ExecStatement(CookieTable.DELETE_ALL_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + // No parameters + } + }); + } else if (cookie == null) { execute(new ExecStatement(CookieTable.DELETE_BY_USER_STATEMENT) { @Override public void prepare(PreparedStatement statement) throws SQLException { diff --git a/Plan/common/src/main/resources/plugin.yml b/Plan/common/src/main/resources/plugin.yml index 9cd6ff1e5..c06c0b566 100644 --- a/Plan/common/src/main/resources/plugin.yml +++ b/Plan/common/src/main/resources/plugin.yml @@ -60,6 +60,8 @@ permissions: default: true plan.unregister.other: default: op + plan.logout.other: + default: op plan.info: default: op plan.reload: diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java index 3bf2244df..48cea6509 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java @@ -1,3 +1,19 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ package com.djrapitops.plan.delivery.webserver.auth; import com.djrapitops.plan.delivery.domain.WebUser; diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/WebUserQueriesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/WebUserQueriesTest.java index 602fa8a85..d47cd3f7a 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/WebUserQueriesTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/WebUserQueriesTest.java @@ -98,9 +98,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { cookieStore.removeCookie(cookie); - Map result = db().query(WebUserQueries.fetchActiveCookies()); - Map expected = Collections.emptyMap(); - assertEquals(expected, result); + assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty()); } @Test @@ -116,8 +114,13 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { assertFalse(cookieStore.checkCookie(cookie).isPresent()); - Map result = db().query(WebUserQueries.fetchActiveCookies()); - Map expected = Collections.emptyMap(); - assertEquals(expected, result); + assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty()); + } + + @Test + default void removeEverythingRemovesCookies() { + activeCookieStoreSavesCookies(); + db().executeTransaction(new RemoveEverythingTransaction()); + assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty()); } } \ No newline at end of file