From 5061439d14315d267c73d0703b3df7c013726950 Mon Sep 17 00:00:00 2001 From: Aurora Lahtela <24460436+AuroraLS3@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:56:13 +0300 Subject: [PATCH] 1623/access control (#3173) * Add web authorization permission based on groups * Access and parts of website are limited by permissions * Add group management in /manage page * Higher level permissions grant lower level permissions similar to Sponge * Add command /plan setgroup, which uses plan.setgroup.other permission * Add command /plan groups, which uses plan.setgroup.other permission * Add more navigation based on permissions * API modifications * User#hasPermission now returns true if user has parent permission in the tree * ResolverService#registerPermissions and ResolverService#registerPermission methods for adding new permissions * Update locale with new lines * Various unrelated fixes to CSS and code Affects issues: - Close #1623 --- Plan/api/build.gradle | 2 +- .../plan/capability/Capability.java | 6 +- .../plan/delivery/web/ResolverService.java | 22 + .../web/resolver/request/WebUser.java | 12 +- Plan/build.gradle | 4 +- .../java/com/djrapitops/plan/PlanSystem.java | 2 +- .../djrapitops/plan/commands/PlanCommand.java | 36 +- .../plan/commands/TabCompleteCache.java | 16 +- .../commands/subcommands/LinkCommands.java | 2 +- .../subcommands/RegistrationCommands.java | 104 ++-- .../plan/commands/use/SubcommandBuilder.java | 4 + .../plan/delivery/domain/WebUser.java | 93 ---- .../plan/delivery/domain/auth/Group.java | 59 +++ .../plan/delivery/domain/auth/GroupList.java | 58 +++ .../plan/delivery/domain/auth/User.java | 36 +- .../delivery/domain/auth/WebPermission.java | 155 ++++++ .../domain/auth/WebPermissionList.java | 58 +++ .../rendering/json/PlayerJSONCreator.java | 88 ++-- .../plan/delivery/web/ResolverSvc.java | 22 +- .../delivery/webserver/ResponseResolver.java | 9 +- .../webserver/auth/ActiveCookieStore.java | 9 +- .../delivery/webserver/auth/FailReason.java | 2 + .../webserver/auth/RegistrationBin.java | 2 +- .../resolver/ErrorsPageResolver.java | 3 +- .../resolver/ManagePageResolver.java | 48 ++ .../resolver/PlayerPageResolver.java | 6 +- .../resolver/PlayersPageResolver.java | 3 +- .../webserver/resolver/QueryPageResolver.java | 3 +- .../webserver/resolver/RootPageResolver.java | 25 +- .../resolver/ServerPageResolver.java | 15 +- .../resolver/json/ErrorsJSONResolver.java | 3 +- .../resolver/json/ExtensionJSONResolver.java | 5 +- .../resolver/json/FiltersJSONResolver.java | 3 +- .../resolver/json/GraphsJSONResolver.java | 77 ++- .../resolver/json/NetworkJSONResolver.java | 19 +- .../json/NetworkPerformanceJSONResolver.java | 3 +- .../resolver/json/NetworkTabJSONResolver.java | 7 +- .../resolver/json/PlayerJSONResolver.java | 19 +- .../json/PlayerJoinAddressJSONResolver.java | 7 +- .../json/PlayerKillsJSONResolver.java | 3 +- .../json/PlayersTableJSONResolver.java | 6 +- .../resolver/json/QueryJSONResolver.java | 3 +- .../resolver/json/RetentionJSONResolver.java | 7 +- .../resolver/json/RootJSONResolver.java | 67 ++- .../json/ServerIdentityJSONResolver.java | 12 +- .../resolver/json/ServerTabJSONResolver.java | 7 +- .../resolver/json/SessionsJSONResolver.java | 7 +- .../resolver/json/VersionJSONResolver.java | 9 +- .../json/WebGroupDeleteJSONResolver.java | 108 ++++ .../resolver/json/WebGroupJSONResolver.java | 90 ++++ .../json/WebGroupPermissionJSONResolver.java | 94 ++++ .../json/WebGroupSaveJSONResolver.java | 113 +++++ .../json/WebPermissionJSONResolver.java | 86 ++++ .../resolver/swagger/SwaggerJsonResolver.java | 3 +- .../resolver/swagger/SwaggerPageResolver.java | 3 +- .../plan/identification/Identifiers.java | 4 + .../djrapitops/plan/settings/Permissions.java | 1 + .../plan/settings/locale/LocaleSystem.java | 2 + .../settings/locale/lang/DeepHelpLang.java | 4 +- .../plan/settings/locale/lang/HelpLang.java | 4 + .../plan/settings/locale/lang/HtmlLang.java | 40 ++ .../plan/storage/database/SQLDB.java | 5 + .../database/queries/LargeStoreQueries.java | 57 ++- .../queries/objects/WebUserQueries.java | 206 ++++++-- .../database/sql/tables/SecurityTable.java | 8 +- .../database/sql/tables/WebGroupTable.java | 51 ++ .../sql/tables/WebGroupToPermissionTable.java | 51 ++ .../sql/tables/WebPermissionTable.java | 50 ++ .../transactions/BackupCopyTransaction.java | 7 + .../DeleteWebGroupTransaction.java | 87 ++++ ...sionToGroupsWithPermissionTransaction.java | 69 +++ ...StoreMissingWebPermissionsTransaction.java | 61 +++ .../StoreWebGroupTransaction.java | 100 ++++ .../database/transactions/Transaction.java | 9 + .../commands/RemoveEverythingTransaction.java | 3 + .../commands/StoreWebUserTransaction.java | 40 +- .../init/CreateTablesTransaction.java | 5 +- .../LegacyPermissionLevelGroupsPatch.java | 79 +++ .../patches/SecurityTableGroupPatch.java | 80 +++ .../patches/UpdateWebPermissionsPatch.java | 68 +++ .../WebGroupAddMissingAdminGroupPatch.java | 50 ++ .../patches/WebGroupDefaultGroupsPatch.java | 84 +++ .../assets/plan/locale/locale_CN.yml | 146 ++++++ .../assets/plan/locale/locale_CS.yml | 146 ++++++ .../assets/plan/locale/locale_DE.yml | 146 ++++++ .../assets/plan/locale/locale_EN.yml | 146 ++++++ .../assets/plan/locale/locale_ES.yml | 146 ++++++ .../assets/plan/locale/locale_FI.yml | 154 +++++- .../assets/plan/locale/locale_FR.yml | 146 ++++++ .../assets/plan/locale/locale_IT.yml | 146 ++++++ .../assets/plan/locale/locale_JA.yml | 140 +++++ .../assets/plan/locale/locale_KO.yml | 146 ++++++ .../assets/plan/locale/locale_NL.yml | 146 ++++++ .../assets/plan/locale/locale_PT_BR.yml | 146 ++++++ .../assets/plan/locale/locale_RU.yml | 146 ++++++ .../assets/plan/locale/locale_TR.yml | 146 ++++++ .../assets/plan/locale/locale_ZH_TW.yml | 146 ++++++ .../plan/commands/PlanCommandTest.java | 5 +- .../subcommands/RegistrationCommandsTest.java | 190 +++++++ .../delivery/webserver/AccessControlTest.java | 477 ++++++------------ .../AccessControlVisibilityTest.java | 405 +++++++++++++++ .../webserver/HttpAccessControlTest.java | 117 +++++ .../webserver/JSErrorRegressionTest.java | 6 +- .../webserver/JksHttpsServerTest.java | 2 +- .../webserver/OpenRedirectFuzzTest.java | 2 +- .../webserver/Pkcs12HttpsServerTest.java | 2 +- .../webserver/auth/ActiveCookieStoreTest.java | 5 +- .../settings/locale/LocaleSystemTest.java | 19 + .../database/queries/DatabaseBackupTest.java | 4 +- .../database/queries/PingQueriesTest.java | 10 +- .../database/queries/WebUserQueriesTest.java | 161 +++++- .../java/extension/FullSystemExtension.java | 2 + .../java/extension/SeleniumExtension.java | 21 +- .../test/java/utilities/TestErrorLogger.java | 6 + Plan/config/checkstyle/checkstyle.xml | 4 +- Plan/react/dashboard/src/App.js | 19 +- .../dashboard/src/components/CardTabs.js | 5 +- .../src/components/alert/AlertPopupArea.js | 29 ++ .../src/components/calendar/ServerCalendar.js | 38 +- .../src/components/cards/CardHeader.js | 2 +- .../cards/common/GeolocationsCard.js | 2 +- .../cards/common/InsightsFor30DaysCard.js | 4 +- .../components/cards/common/PingTableCard.js | 2 +- .../cards/common/PvpKillsTableCard.js | 2 +- .../cards/common/RecentSessionsCard.js | 4 +- .../cards/player/ConnectionsCard.js | 45 ++ .../components/cards/player/NicknamesCard.js | 13 +- .../server/graphs/CurrentPlayerbaseCard.js | 2 +- .../server/graphs/JoinAddressGroupCard.js | 2 +- .../graphs/NetworkOnlineActivityGraphsCard.js | 34 +- .../server/graphs/OnlineActivityGraphsCard.js | 43 +- .../server/graphs/PerformanceGraphsCard.js | 2 +- .../insights/OnlineActivityInsightsCard.js | 2 +- .../insights/PerformanceInsightsCard.js | 2 +- .../server/insights/PlayerbaseInsightsCard.js | 2 +- .../server/insights/PvpPveInsightsCard.js | 2 +- .../server/insights/SessionInsightsCard.js | 2 +- .../tables/OnlineActivityAsNumbersCard.js | 2 +- .../server/tables/PerformanceAsNumbersCard.js | 4 +- .../server/tables/PlayerbaseTrendsCard.js | 2 +- .../server/tables/PvpPveAsNumbersCard.js | 2 +- .../server/tables/ServerWeekComparisonCard.js | 2 +- .../server/values/ServerAsNumbersCard.js | 2 +- .../src/components/graphs/TimeByTimeGraph.js | 4 +- .../dashboard/src/components/input/Select.js | 24 + .../dashboard/src/components/input/Toggle.js | 4 +- .../src/components/layout/OpaqueText.js | 13 + .../src/components/layout/SideNavTabs.js | 61 +++ .../src/components/modal/HelpModal.js | 5 + .../modal/VersionInformationModal.js | 8 +- .../modal/help/GroupPermissionHelp.js | 55 ++ .../components/navigation/MainPageRedirect.js | 12 +- .../navigation/PageNavigationItem.js | 34 +- .../src/components/navigation/Sidebar.js | 11 +- .../dashboard/src/hooks/authenticationHook.js | 24 +- .../src/hooks/context/alertPopupContext.js | 32 ++ .../configurationStorageContextHook.js | 60 +++ .../context/dropdownStatusContextHook.js | 27 + .../src/hooks/context/groupEditContextHook.js | 247 +++++++++ .../dashboard/src/hooks/dataFetchHook.js | 8 +- .../src/hooks/serverExtensionDataContext.js | 11 +- .../src/service/backendConfiguration.js | 4 + .../dashboard/src/service/localeService.js | 3 +- .../dashboard/src/service/manageService.js | 31 ++ Plan/react/dashboard/src/style/main.sass | 19 +- Plan/react/dashboard/src/style/style.css | 29 ++ Plan/react/dashboard/src/util/colors.js | 6 +- Plan/react/dashboard/src/views/SwaggerView.js | 19 +- .../src/views/common/Geolocations.js | 14 +- .../dashboard/src/views/layout/ErrorsPage.js | 11 +- .../dashboard/src/views/layout/ManagePage.js | 65 +++ .../dashboard/src/views/layout/NetworkPage.js | 72 ++- .../dashboard/src/views/layout/PlayerPage.js | 28 +- .../dashboard/src/views/layout/PlayersPage.js | 2 +- .../dashboard/src/views/layout/QueryPage.js | 2 +- .../src/views/layout/RegisterPage.js | 14 +- .../dashboard/src/views/layout/ServerPage.js | 77 ++- .../dashboard/src/views/manage/GroupsView.js | 413 +++++++++++++++ .../src/views/network/NetworkGeolocations.js | 13 +- .../src/views/network/NetworkJoinAddresses.js | 13 +- .../src/views/network/NetworkOverview.js | 20 +- .../src/views/network/NetworkPerformance.js | 11 +- .../views/network/NetworkPlayerRetention.js | 8 +- .../network/NetworkPlayerbaseOverview.js | 15 +- .../src/views/network/NetworkServers.js | 9 +- .../src/views/network/NetworkSessions.js | 13 +- .../src/views/player/PlayerOverview.js | 45 +- .../src/views/player/PlayerPluginData.js | 10 +- .../src/views/player/PlayerPvpPve.js | 6 +- .../src/views/player/PlayerServers.js | 6 +- .../src/views/player/PlayerSessions.js | 6 +- .../dashboard/src/views/players/AllPlayers.js | 9 +- .../dashboard/src/views/query/NewQueryView.js | 6 +- .../src/views/server/OnlineActivity.js | 14 +- .../src/views/server/PlayerbaseOverview.js | 14 +- .../src/views/server/ServerGeolocations.js | 12 +- .../src/views/server/ServerJoinAddresses.js | 13 +- .../src/views/server/ServerOverview.js | 21 +- .../src/views/server/ServerPerformance.js | 14 +- .../src/views/server/ServerPlayerRetention.js | 8 +- .../src/views/server/ServerPlayers.js | 9 +- .../src/views/server/ServerPluginData.js | 11 +- .../src/views/server/ServerPvpPve.js | 19 +- .../src/views/server/ServerSessions.js | 14 +- 204 files changed, 7567 insertions(+), 1013 deletions(-) delete mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/WebUser.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/Group.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/GroupList.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionList.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ManagePageResolver.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupDeleteJSONResolver.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupJSONResolver.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupPermissionJSONResolver.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupSaveJSONResolver.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebPermissionJSONResolver.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupTable.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupToPermissionTable.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebPermissionTable.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/DeleteWebGroupTransaction.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/GrantWebPermissionToGroupsWithPermissionTransaction.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreMissingWebPermissionsTransaction.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreWebGroupTransaction.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/LegacyPermissionLevelGroupsPatch.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/SecurityTableGroupPatch.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/commands/subcommands/RegistrationCommandsTest.java create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java create mode 100644 Plan/react/dashboard/src/components/alert/AlertPopupArea.js create mode 100644 Plan/react/dashboard/src/components/cards/player/ConnectionsCard.js create mode 100644 Plan/react/dashboard/src/components/input/Select.js create mode 100644 Plan/react/dashboard/src/components/layout/OpaqueText.js create mode 100644 Plan/react/dashboard/src/components/layout/SideNavTabs.js create mode 100644 Plan/react/dashboard/src/components/modal/help/GroupPermissionHelp.js create mode 100644 Plan/react/dashboard/src/hooks/context/alertPopupContext.js create mode 100644 Plan/react/dashboard/src/hooks/context/configurationStorageContextHook.js create mode 100644 Plan/react/dashboard/src/hooks/context/dropdownStatusContextHook.js create mode 100644 Plan/react/dashboard/src/hooks/context/groupEditContextHook.js create mode 100644 Plan/react/dashboard/src/service/manageService.js create mode 100644 Plan/react/dashboard/src/views/layout/ManagePage.js create mode 100644 Plan/react/dashboard/src/views/manage/GroupsView.js diff --git a/Plan/api/build.gradle b/Plan/api/build.gradle index 2e90d75fd..ba7dd8788 100644 --- a/Plan/api/build.gradle +++ b/Plan/api/build.gradle @@ -8,7 +8,7 @@ compileJava { options.release = 8 } -ext.apiVersion = '5.5-R0.1' +ext.apiVersion = '5.6-R0.1' publishing { repositories { diff --git a/Plan/api/src/main/java/com/djrapitops/plan/capability/Capability.java b/Plan/api/src/main/java/com/djrapitops/plan/capability/Capability.java index 2f7629a47..42743f558 100644 --- a/Plan/api/src/main/java/com/djrapitops/plan/capability/Capability.java +++ b/Plan/api/src/main/java/com/djrapitops/plan/capability/Capability.java @@ -90,7 +90,11 @@ enum Capability { * {@link com.djrapitops.plan.delivery.web.ResourceService#addJavascriptToResource(String, String, ResourceService.Position, String, String)} * {@link com.djrapitops.plan.delivery.web.ResourceService#addStyleToResource(String, String, ResourceService.Position, String, String)} */ - PAGE_EXTENSION_RESOURCES_REGISTER_DIRECT_CUSTOMIZATION; + PAGE_EXTENSION_RESOURCES_REGISTER_DIRECT_CUSTOMIZATION, + /** + * {@link com.djrapitops.plan.delivery.web.ResolverService#registerPermissions(String...)} + */ + PAGE_EXTENSION_USER_PERMISSIONS; static Optional getByName(String name) { if (name == null) { diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java index b2aa3ee8a..124782289 100644 --- a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java +++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java @@ -17,9 +17,11 @@ package com.djrapitops.plan.delivery.web; import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.request.Request; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; @@ -63,6 +65,26 @@ public interface ResolverService { */ void registerResolverForMatches(String pluginName, Pattern pattern, Resolver resolver); + /** + * Register a new permission that you are using in your {@link Resolver#canAccess(Request)} method. + *

+ * The permissions are not given to any users by default, and need to be given by admin manually. + * + * @param webPermissions Permission strings, higher level permissions grant lower level automatically - eg. page.foo also grants page.foo.bar + * @return CompletableFuture that tells when the permissions have been stored. + */ + CompletableFuture registerPermissions(String... webPermissions); + + /** + * Register a new permission that you are using in your {@link Resolver#canAccess(Request)} method. + *

+ * The permission is granted to any groups with {@code whenHasPermission} parameter. + * + * @param webPermission Permission string, higher level permissions grant lower level automatically - eg. page.foo also grants page.foo.bar + * @param whenHasPermission Permission that a group already has that this permission should be granted to - eg. page.network.overview.numbers + */ + void registerPermission(String webPermission, String whenHasPermission); + /** * Obtain a {@link Resolver} for a target. *

diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/WebUser.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/WebUser.java index 743f833af..2d8d00e81 100644 --- a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/WebUser.java +++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/WebUser.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.delivery.web.resolver.request; import java.util.*; +import java.util.function.Supplier; public final class WebUser { @@ -57,7 +58,16 @@ public final class WebUser { } public boolean hasPermission(String permission) { - return permissions.contains(permission); + for (String grant : permissions) { + String substitute = permission.replace(grant, ""); + // Last character is . so it is a sub-permission of the parent, eg. page.player, page.player.thing -> .thing + if (substitute.isEmpty() || substitute.startsWith(".")) return true; + } + return false; + } + + public boolean hasPermission(Supplier permissionSupplier) { + return hasPermission(permissionSupplier.get()); } public String getName() { diff --git a/Plan/build.gradle b/Plan/build.gradle index 48db4fa22..0f33910c5 100644 --- a/Plan/build.gradle +++ b/Plan/build.gradle @@ -36,10 +36,10 @@ def buildVersion = determineBuildVersion() allprojects { group "com.djrapitops" - version "5.5-SNAPSHOT" + version "5.6-SNAPSHOT" ext.majorVersion = '5' - ext.minorVersion = '5' + ext.minorVersion = '6' ext.buildVersion = buildVersion ext.fullVersion = project.ext.majorVersion + '.' + project.ext.minorVersion + ' build ' + project.ext.buildVersion diff --git a/Plan/common/src/main/java/com/djrapitops/plan/PlanSystem.java b/Plan/common/src/main/java/com/djrapitops/plan/PlanSystem.java index 19ade844b..1c27659dd 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/PlanSystem.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/PlanSystem.java @@ -180,12 +180,12 @@ public class PlanSystem implements SubSystem { queryService.register(); enableSystems( + processing, files, localeSystem, versionChecker, databaseSystem, webServerSystem, - processing, serverInfo, importSystem, exportSystem, 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 65798b035..1df9aac0d 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 @@ -115,6 +115,8 @@ public class PlanCommand { .subcommand(unregisterCommand()) .subcommand(logoutCommand()) .subcommand(webUsersCommand()) + .subcommand(groups()) + .subcommand(setGroup()) .subcommand(acceptCommand()) .subcommand(cancelCommand()) @@ -261,7 +263,7 @@ public class PlanCommand { .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) + .onArgsOnlyCommand(registrationCommands::onLogoutCommand) .onTabComplete(this::webUserNames) .build(); } @@ -514,4 +516,36 @@ public class PlanCommand { .onTabComplete(this::playerNames) .build(); } + + private Subcommand setGroup() { + return Subcommand.builder() + .aliases("setgroup") + .requirePermission(Permissions.SET_GROUP) + .requiredArgument(locale.getString(HelpLang.ARG_USERNAME), locale.getString(HelpLang.DESC_ARG_USERNAME)) + .requiredArgument(locale.getString(HelpLang.ARG_GROUP), locale.getString(HelpLang.DESC_ARG_GROUP)) + .description(locale.getString(HelpLang.SET_GROUP)) + .inDepthDescription(locale.getString(DeepHelpLang.SET_GROUP)) + .onCommand(registrationCommands::onChangePermissionGroup) + .onTabComplete(this::webGroupTabComplete) + .build(); + } + + private List webGroupTabComplete(CMDSender sender, @Untrusted Arguments arguments) { + Optional groupArgument = arguments.get(1); + if (groupArgument.isPresent()) { + return tabCompleteCache.getMatchingWebGroupNames(groupArgument.get()); + } + String usernameArgument = arguments.get(0).orElse(null); + return tabCompleteCache.getMatchingUserIdentifiers(usernameArgument); + } + + private Subcommand groups() { + return Subcommand.builder() + .aliases("groups") + .requirePermission(Permissions.SET_GROUP) + .description(locale.getString(HelpLang.GROUPS)) + .inDepthDescription(locale.getString(DeepHelpLang.GROUPS)) + .onCommand(registrationCommands::onListWebGroups) + .build(); + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/commands/TabCompleteCache.java b/Plan/common/src/main/java/com/djrapitops/plan/commands/TabCompleteCache.java index 2572b77e4..36c8918e3 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/commands/TabCompleteCache.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/commands/TabCompleteCache.java @@ -17,7 +17,6 @@ package com.djrapitops.plan.commands; import com.djrapitops.plan.SubSystem; -import com.djrapitops.plan.delivery.domain.auth.User; import com.djrapitops.plan.gathering.ServerSensor; import com.djrapitops.plan.identification.Server; import com.djrapitops.plan.identification.ServerUUID; @@ -52,6 +51,7 @@ public class TabCompleteCache implements SubSystem { private final Set serverIdentifiers; private final Set userIdentifiers; private final Set backupFileNames; + private final Set webGroupIdentifiers; @Inject public TabCompleteCache( @@ -68,6 +68,7 @@ public class TabCompleteCache implements SubSystem { serverIdentifiers = new HashSet<>(); userIdentifiers = new HashSet<>(); backupFileNames = new HashSet<>(); + webGroupIdentifiers = new HashSet<>(); } @Override @@ -77,9 +78,14 @@ public class TabCompleteCache implements SubSystem { refreshServerIdentifiers(); refreshUserIdentifiers(); refreshBackupFileNames(); + refreshWebGroupIdentifiers(); }); } + private void refreshWebGroupIdentifiers() { + webGroupIdentifiers.addAll(dbSystem.getDatabase().query(WebUserQueries.fetchGroupNames())); + } + private void refreshServerIdentifiers() { Map serverNames = dbSystem.getDatabase().query(ServerQueries.fetchPlanServerInformation()); for (Map.Entry server : serverNames.entrySet()) { @@ -94,9 +100,7 @@ public class TabCompleteCache implements SubSystem { } private void refreshUserIdentifiers() { - dbSystem.getDatabase().query(WebUserQueries.fetchAllUsers()).stream() - .map(User::getUsername) - .forEach(userIdentifiers::add); + userIdentifiers.addAll(dbSystem.getDatabase().query(WebUserQueries.fetchAllUsernames())); } private void refreshBackupFileNames() { @@ -133,6 +137,10 @@ public class TabCompleteCache implements SubSystem { return findMatches(backupFileNames, searchFor); } + public List getMatchingWebGroupNames(@Untrusted String searchFor) { + return findMatches(webGroupIdentifiers, searchFor); + } + @NotNull List findMatches(Collection searchList, @Untrusted String searchFor) { List filtered = searchList.stream() diff --git a/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/LinkCommands.java b/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/LinkCommands.java index cbe2c010b..7f2f63a50 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/LinkCommands.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/commands/subcommands/LinkCommands.java @@ -217,7 +217,7 @@ public class LinkCommands { sender.send(t + locale.getString(CommandLang.HEADER_WEB_USERS, 0)); } else { String usersListed = users.stream().sorted() - .map(user -> m + user.getUsername() + "::" + t + user.getLinkedTo() + "::" + s + user.getPermissionLevel() + "\n") + .map(user -> m + user.getUsername() + "::" + t + user.getLinkedTo() + "::" + s + user.getPermissionGroup() + "\n") .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) .toString(); sender.buildMessage() 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 d0a995805..c2a6dea23 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 @@ -20,6 +20,7 @@ import com.djrapitops.plan.commands.use.Arguments; import com.djrapitops.plan.commands.use.CMDSender; import com.djrapitops.plan.commands.use.ColorScheme; import com.djrapitops.plan.delivery.domain.auth.User; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore; import com.djrapitops.plan.delivery.webserver.auth.FailReason; import com.djrapitops.plan.delivery.webserver.auth.RegistrationBin; @@ -33,7 +34,6 @@ import com.djrapitops.plan.storage.database.Database; import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; import com.djrapitops.plan.storage.database.transactions.commands.RemoveWebUserTransaction; import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction; -import com.djrapitops.plan.utilities.PassEncryptUtil; import com.djrapitops.plan.utilities.dev.Untrusted; import com.djrapitops.plan.utilities.logging.ErrorContext; import com.djrapitops.plan.utilities.logging.ErrorLogger; @@ -41,7 +41,7 @@ import net.playeranalytics.plugin.server.PluginLogger; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -99,61 +99,41 @@ public class RegistrationCommands { } else { @Untrusted Optional code = arguments.getAfter("--code"); if (code.isPresent()) { - registerUsingCode(sender, code.get()); + registerUsingCode(sender, code.get(), arguments); } else { - registerUsingLegacy(sender, arguments); + sender.send(locale.getString(CommandLang.FAIL_REQ_ARGS, "--code", "/plan register --code 81cc5b17")); } } } - public void registerUsingCode(CMDSender sender, @Untrusted String code) { + public void registerUsingCode(CMDSender sender, @Untrusted String code, @Untrusted Arguments arguments) { UUID linkedToUUID = sender.getUUID().orElse(null); - Optional user = RegistrationBin.register(code, linkedToUUID); - if (user.isPresent()) { - registerUser(user.get(), sender, getPermissionLevel(sender)); - } else { - throw new IllegalArgumentException(locale.getString(FailReason.USER_INFORMATION_NOT_FOUND)); - } + User user = RegistrationBin.register(code, linkedToUUID) + .orElseThrow(() -> new IllegalArgumentException(locale.getString(FailReason.USER_INFORMATION_NOT_FOUND))); + String permissionGroup = getPermissionGroup(sender, arguments) + .orElseThrow(() -> new IllegalArgumentException(locale.getString(FailReason.NO_PERMISSION_GROUP))); + user.setPermissionGroup(permissionGroup); + registerUser(user, sender); } - public void registerUsingLegacy(CMDSender sender, @Untrusted Arguments arguments) { - @Untrusted String password = arguments.get(0) - .orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 1, ""))); - String passwordHash = PassEncryptUtil.createHash(password); - int permissionLevel = arguments.getInteger(2) - .filter(arg -> sender.hasPermission(Permissions.REGISTER_OTHER)) // argument only allowed with register other permission - .orElseGet(() -> getPermissionLevel(sender)); - - Optional senderUUID = sender.getUUID(); - Optional senderName = sender.getPlayerName(); - if (senderUUID.isPresent() && senderName.isPresent()) { - String playerName = senderName.get(); - UUID linkedToUUID = senderUUID.get(); - @Untrusted String username = arguments.get(1).orElse(playerName); - registerUser(new User(username, playerName, linkedToUUID, passwordHash, permissionLevel, Collections.emptyList()), sender, permissionLevel); - } else { - @Untrusted String username = arguments.get(1) - .orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 3, " "))); - registerUser(new User(username, "console", null, passwordHash, permissionLevel, Collections.emptyList()), sender, permissionLevel); + private Optional getPermissionGroup(CMDSender sender, @Untrusted Arguments arguments) { + List groups = dbSystem.getDatabase().query(WebUserQueries.fetchGroupNames()); + if (sender.isPlayer()) { + for (String group : groups) { + if (sender.hasPermission("plan.webgroup." + group)) { + return Optional.of(group); + } + } + } else if (arguments.contains("superuser")) { + return dbSystem.getDatabase().query(WebUserQueries.fetchGroupNamesWithPermission(WebPermission.MANAGE_GROUPS.getPermission())) + .stream().findFirst(); } + return Optional.empty(); } - private int getPermissionLevel(CMDSender sender) { - if (sender.hasPermission(Permissions.SERVER)) { - return 0; - } - if (sender.hasPermission(Permissions.PLAYER_OTHER)) { - return 1; - } - if (sender.hasPermission(Permissions.PLAYER_SELF)) { - return 2; - } - return 100; - } - - private void registerUser(User user, CMDSender sender, int permissionLevel) { + private void registerUser(User user, CMDSender sender) { String username = user.getUsername(); - user.setPermissionLevel(permissionLevel); + try { Database database = dbSystem.getDatabase(); boolean userExists = database.query(WebUserQueries.fetchUser(username)).isPresent(); @@ -163,11 +143,11 @@ public class RegistrationCommands { .get(); // Wait for completion sender.send(locale.getString(CommandLang.WEB_USER_REGISTER_SUCCESS, username)); - logger.info(locale.getString(CommandLang.WEB_USER_REGISTER_NOTIFY, username, permissionLevel)); + logger.info(locale.getString(CommandLang.WEB_USER_REGISTER_NOTIFY, username, user.getPermissionGroup())); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (DBOpException | ExecutionException e) { - errorLogger.warn(e, ErrorContext.builder().related(sender, user, permissionLevel).build()); + errorLogger.warn(e, ErrorContext.builder().related(sender, user).build()); } } @@ -217,7 +197,7 @@ public class RegistrationCommands { } - public void onLogoutCommand(CMDSender sender, @Untrusted Arguments arguments) { + public void onLogoutCommand(@Untrusted Arguments arguments) { @Untrusted String loggingOut = arguments.get(0) .orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ONE_ARG, locale.getString(HelpLang.ARG_USERNAME) + "/*"))); @@ -227,4 +207,32 @@ public class RegistrationCommands { ActiveCookieStore.removeUserCookie(loggingOut); } } + + public void onChangePermissionGroup(CMDSender sender, @Untrusted Arguments arguments) { + String username = arguments.get(0) + .orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, locale.getString(HelpLang.ARG_USERNAME)))); + String group = arguments.get(1) + .orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, locale.getString(HelpLang.ARG_GROUP)))); + + Database database = dbSystem.getDatabase(); + User user = database.query(WebUserQueries.fetchUser(username)) + .orElseThrow(() -> new IllegalArgumentException(locale.getString(FailReason.USER_DOES_NOT_EXIST))); + + Optional groupId = database.query(WebUserQueries.fetchGroupId(group)); + if (groupId.isEmpty()) { + throw new IllegalArgumentException(locale.getString(FailReason.GROUP_DOES_NOT_EXIST)); + } + + user.setPermissionGroup(group); + + database.executeTransaction(new StoreWebUserTransaction(user)) + .thenRun(() -> sender.send(locale.getString(CommandLang.PROGRESS_SUCCESS))); + } + + public void onListWebGroups(CMDSender sender) { + Database database = dbSystem.getDatabase(); + List groupNames = database.query(WebUserQueries.fetchGroupNames()); + + sender.send(String.join(", ", groupNames)); + } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/commands/use/SubcommandBuilder.java b/Plan/common/src/main/java/com/djrapitops/plan/commands/use/SubcommandBuilder.java index 7388e1978..1dcff380e 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/commands/use/SubcommandBuilder.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/commands/use/SubcommandBuilder.java @@ -54,6 +54,10 @@ public interface SubcommandBuilder { return onCommand((sender, arguments) -> executor.accept(sender)); } + default SubcommandBuilder onArgsOnlyCommand(Consumer executor) { + return onCommand((sender, arguments) -> executor.accept(arguments)); + } + SubcommandBuilder onTabComplete(BiFunction> resolver); Subcommand build(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/WebUser.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/WebUser.java deleted file mode 100644 index f41ab67ac..000000000 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/WebUser.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.domain; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Object containing webserver security user information. - * - * @author AuroraLS3 - * @deprecated Use {@link com.djrapitops.plan.delivery.domain.auth.User} instead - * TODO Rewrite Authentication stuff - */ -@Deprecated(since = "2022-02-12, User.java") -public class WebUser { - - private final String username; - private final String saltedPassHash; - private final int permLevel; - - public WebUser(String username, String saltedPassHash, int permLevel) { - this.username = username; - this.saltedPassHash = saltedPassHash; - this.permLevel = permLevel; - } - - public static List getPermissionsForLevel(int level) { - List permissions = new ArrayList<>(); - if (level <= 0) { - permissions.add("page.network"); - permissions.add("page.server"); - permissions.add("page.debug"); - // TODO Add JSON Permissions - } - if (level <= 1) { - permissions.add("page.players"); - permissions.add("page.player.other"); - } - if (level <= 2) { - permissions.add("page.player.self"); - } - return permissions; - } - - public String getSaltedPassHash() { - return saltedPassHash; - } - - public int getPermLevel() { - return permLevel; - } - - public String getName() { - return username; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - WebUser webUser = (WebUser) o; - return permLevel == webUser.permLevel && - Objects.equals(username, webUser.username) && - Objects.equals(saltedPassHash, webUser.saltedPassHash); - } - - @Override - public int hashCode() { - return Objects.hash(username, saltedPassHash, permLevel); - } - - public com.djrapitops.plan.delivery.web.resolver.request.WebUser toNewWebUser() { - return new com.djrapitops.plan.delivery.web.resolver.request.WebUser( - username, getPermissionsForLevel(permLevel).toArray(new String[0]) - ); - } -} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/Group.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/Group.java new file mode 100644 index 000000000..2a1719aad --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/Group.java @@ -0,0 +1,59 @@ +/* + * 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.domain.auth; + +import java.util.Objects; + +/** + * Represents Plan web permission group without permissions or users. + *

+ * This object is used instead of a String in case more attributes are added in the future. + * + * @author AuroraLS3 + */ +public class Group { + + private final String name; + + public Group(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Group group = (Group) o; + return Objects.equals(getName(), group.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + + @Override + public String toString() { + return "WebGroup{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/GroupList.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/GroupList.java new file mode 100644 index 000000000..85ed6ceeb --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/GroupList.java @@ -0,0 +1,58 @@ +/* + * 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.domain.auth; + +import java.util.List; +import java.util.Objects; + +/** + * Represents Plan web permission group listing without associated permissions. + * + * @author AuroraLS3 + */ +public class GroupList { + + private final List groups; + + public GroupList(List groups) { + this.groups = groups; + } + + public List getGroups() { + return groups; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GroupList that = (GroupList) o; + return Objects.equals(getGroups(), that.getGroups()); + } + + @Override + public int hashCode() { + return Objects.hash(getGroups()); + } + + @Override + public String toString() { + return "WebGroupList{" + + "groups=" + groups + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/User.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/User.java index 0a99f1d27..211b4989f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/User.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/User.java @@ -36,15 +36,15 @@ public class User implements Comparable { private final String linkedTo; private final UUID linkedToUUID; // null for 'console' private final String passwordHash; - private int permissionLevel; + private String permissionGroup; private final Collection permissions; - public User(@Untrusted String username, String linkedTo, UUID linkedToUUID, String passwordHash, int permissionLevel, Collection permissions) { + public User(@Untrusted String username, String linkedTo, UUID linkedToUUID, String passwordHash, String permissionGroup, Collection permissions) { this.username = username; this.linkedTo = linkedTo; this.linkedToUUID = linkedToUUID; this.passwordHash = passwordHash; - this.permissionLevel = permissionLevel; + this.permissionGroup = permissionGroup; this.permissions = permissions; } @@ -73,20 +73,16 @@ public class User implements Comparable { return passwordHash; } - /** - * @deprecated Permission list should be used instead. - */ - @Deprecated(since = "2022-05-04", forRemoval = true) - public int getPermissionLevel() { - return permissionLevel; + public String getPermissionGroup() { + return permissionGroup; } - /** - * @deprecated Permission list should be used instead. - */ - @Deprecated(since = "2022-05-04", forRemoval = true) - public void setPermissionLevel(int permissionLevel) { - this.permissionLevel = permissionLevel; + public void setPermissionGroup(String permissionGroup) { + this.permissionGroup = permissionGroup; + } + + public Collection getPermissions() { + return permissions; } @Override @@ -96,7 +92,7 @@ public class User implements Comparable { ", linkedTo='" + linkedTo + '\'' + ", linkedToUUID=" + linkedToUUID + ", passwordHash='" + passwordHash + '\'' + - ", permissionLevel=" + permissionLevel + + ", permissionGroup=" + permissionGroup + ", permissions=" + permissions + '}'; } @@ -106,22 +102,22 @@ public class User implements Comparable { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; - return permissionLevel == user.permissionLevel && - Objects.equals(username, user.username) && + return Objects.equals(username, user.username) && Objects.equals(linkedTo, user.linkedTo) && Objects.equals(linkedToUUID, user.linkedToUUID) && Objects.equals(passwordHash, user.passwordHash) && + Objects.equals(permissionGroup, user.permissionGroup) && Objects.equals(permissions, user.permissions); } @Override public int hashCode() { - return Objects.hash(username, linkedTo, linkedToUUID, passwordHash, permissionLevel, permissions); + return Objects.hash(username, linkedTo, linkedToUUID, passwordHash, permissionGroup, permissions); } @Override public int compareTo(User other) { - int comparison = Integer.compare(this.permissionLevel, other.permissionLevel); + int comparison = String.CASE_INSENSITIVE_ORDER.compare(this.permissionGroup, other.permissionGroup); if (comparison == 0) comparison = String.CASE_INSENSITIVE_ORDER.compare(this.username, other.username); return comparison; } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java new file mode 100644 index 000000000..58458cc2b --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java @@ -0,0 +1,155 @@ +/* + * 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.domain.auth; + +import com.djrapitops.plan.settings.locale.lang.Lang; +import org.apache.commons.lang3.StringUtils; + +import java.util.function.Supplier; + +/** + * List of web permissions. + * + * @author AuroraLS3 + */ +public enum WebPermission implements Supplier, Lang { + PAGE("Controls what is visible on pages"), + PAGE_NETWORK("See all of network page"), + PAGE_NETWORK_OVERVIEW("See Network Overview -tab"), + PAGE_NETWORK_OVERVIEW_NUMBERS("See Network Overview numbers"), + PAGE_NETWORK_OVERVIEW_GRAPHS("See Network Overview graphs"), + PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE("See Players Online graph"), + PAGE_NETWORK_OVERVIEW_GRAPHS_DAY_BY_DAY("See Day by Day graph"), + PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR("See Hour by Hour graph"), + PAGE_NETWORK_SERVER_LIST("See list of servers"), + PAGE_NETWORK_PLAYERBASE("See Playerbase Overview -tab"), + PAGE_NETWORK_PLAYERBASE_OVERVIEW("See Playerbase Overview numbers"), + PAGE_NETWORK_PLAYERBASE_GRAPHS("See Playerbase Overview graphs"), + PAGE_NETWORK_SESSIONS("See Sessions tab"), + PAGE_NETWORK_SESSIONS_OVERVIEW("See Session insights"), + PAGE_NETWORK_SESSIONS_WORLD_PIE("See World Pie graph"), + PAGE_NETWORK_SESSIONS_SERVER_PIE("See Server Pie graph"), + PAGE_NETWORK_SESSIONS_LIST("See list of sessions"), + PAGE_NETWORK_JOIN_ADDRESSES("See Join Addresses -tab"), + PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS("See Join Address graphs"), + PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph"), + PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME("See Join Addresses over time graph"), + PAGE_NETWORK_RETENTION("See Player Retention -tab"), + PAGE_NETWORK_GEOLOCATIONS("See Geolocations tab"), + PAGE_NETWORK_GEOLOCATIONS_MAP("See Geolocations Map"), + PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY("See Ping Per Country table"), + PAGE_NETWORK_PLAYERS("See Player list -tab"), + PAGE_NETWORK_PERFORMANCE("See network Performance tab"), + PAGE_NETWORK_PLUGINS("See Plugins tab of Proxy"), + + PAGE_SERVER("See all of server page"), + PAGE_SERVER_OVERVIEW("See Server Overview -tab"), + PAGE_SERVER_OVERVIEW_NUMBERS("See Server Overview numbers"), + PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH("See Players Online graph"), + PAGE_SERVER_ONLINE_ACTIVITY("See Online Activity -tab"), + PAGE_SERVER_ONLINE_ACTIVITY_OVERVIEW("See Online Activity numbers"), + PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS("See Online Activity graphs"), + PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY("See Day by Day graph"), + PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_HOUR_BY_HOUR("See Hour by Hour graph"), + PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_PUNCHCARD("See Punchcard graph"), + PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR("See Server calendar"), + PAGE_SERVER_PLAYERBASE("See Playerbase Overview -tab"), + PAGE_SERVER_PLAYERBASE_OVERVIEW("See Playerbase Overview numbers"), + PAGE_SERVER_PLAYERBASE_GRAPHS("See Playerbase Overview graphs"), + PAGE_SERVER_PLAYER_VERSUS("See PvP & PvE -tab"), + PAGE_SERVER_PLAYER_VERSUS_OVERVIEW("See PvP & PvE numbers"), + PAGE_SERVER_PLAYER_VERSUS_KILL_LIST("See Player kill and death lists"), + PAGE_SERVER_PLAYERS("See Player list -tab"), + PAGE_SERVER_SESSIONS("See Sessions tab"), + PAGE_SERVER_SESSIONS_OVERVIEW("See Session insights"), + PAGE_SERVER_SESSIONS_WORLD_PIE("See World Pie graph"), + PAGE_SERVER_SESSIONS_LIST("See list of sessions"), + PAGE_SERVER_JOIN_ADDRESSES("See Join Addresses -tab"), + PAGE_SERVER_JOIN_ADDRESSES_GRAPHS("See Join Address graphs"), + PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph"), + PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME("See Join Addresses over time graph"), + PAGE_SERVER_RETENTION("See Player Retention -tab"), + PAGE_SERVER_GEOLOCATIONS("See Geolocations tab"), + PAGE_SERVER_GEOLOCATIONS_MAP("See Geolocations Map"), + PAGE_SERVER_GEOLOCATIONS_PING_PER_COUNTRY("See Ping Per Country table"), + PAGE_SERVER_PERFORMANCE("See Performance tab"), + PAGE_SERVER_PERFORMANCE_GRAPHS("See Performance graphs"), + PAGE_SERVER_PERFORMANCE_OVERVIEW("See Performance numbers"), + PAGE_SERVER_PLUGINS("See Plugins -tabs of servers"), + + PAGE_PLAYER("See all of player page"), + PAGE_PLAYER_OVERVIEW("See Player Overview -tab"), + PAGE_PLAYER_SESSIONS("See Player Sessions -tab"), + PAGE_PLAYER_VERSUS("See PvP & PvE -tab"), + PAGE_PLAYER_SERVERS("See Servers -tab"), + PAGE_PLAYER_PLUGINS("See Plugins -tabs"), + + ACCESS("Controls access to pages"), + ACCESS_PLAYER("Allows accessing any /player pages"), + ACCESS_PLAYER_SELF("Allows accessing own /player page"), + ACCESS_RAW_PLAYER_DATA("Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."), + // Restricting to specific servers: access.server.uuid + ACCESS_SERVER("Allows accessing all /server pages"), + ACCESS_NETWORK("Allows accessing /network page"), + ACCESS_PLAYERS("Allows accessing /players page"), + ACCESS_QUERY("Allows accessing /query and Query results pages"), + ACCESS_ERRORS("Allows accessing /errors page"), + ACCESS_DOCS("Allows accessing /docs page"), + + MANAGE_GROUPS("Allows modifying group permissions & Access to /manage/groups page"), + MANAGE_USERS("Allows modifying what users belong to what group"); + + private final String description; + private final boolean deprecated; + + WebPermission(String description) { + this(description, false); + } + + WebPermission(String description, boolean deprecated) { + this.description = description; + this.deprecated = deprecated; + } + + public String getPermission() { + return StringUtils.lowerCase(name()).replace('_', '.'); + } + + public boolean isDeprecated() { + return deprecated; + } + + @Override + public String get() { + return getPermission(); + } + + @Override + public String getIdentifier() { + return "HTML - Permission " + name(); + } + + @Override + public String getKey() { + return "html.manage.permission.description." + name().toLowerCase(); + } + + @Override + public String getDefault() { + return description; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionList.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionList.java new file mode 100644 index 000000000..77afb6796 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionList.java @@ -0,0 +1,58 @@ +/* + * 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.domain.auth; + +import java.util.List; +import java.util.Objects; + +/** + * Represents a list of web permissions. + * + * @author AuroraLS3 + */ +public class WebPermissionList { + + private final List permissions; + + public WebPermissionList(List permissions) { + this.permissions = permissions; + } + + public List getPermissions() { + return permissions; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WebPermissionList that = (WebPermissionList) o; + return Objects.equals(getPermissions(), that.getPermissions()); + } + + @Override + public int hashCode() { + return Objects.hash(getPermissions()); + } + + @Override + public String toString() { + return "PermissionList{" + + "permissions=" + permissions + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java index 0c7c37e4f..c0dda905b 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.rendering.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.domain.container.PlayerContainer; import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionsDto; import com.djrapitops.plan.delivery.domain.keys.PlayerKeys; @@ -56,6 +57,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; @Singleton public class PlayerJSONCreator { @@ -96,18 +98,12 @@ public class PlayerJSONCreator { return dbSystem.getDatabase().query(SessionQueries.lastSeen(playerUUID)); } - public Map createJSONAsMap(UUID playerUUID) { + public Map createJSONAsMap(UUID playerUUID, Predicate hasPermission) { Database db = dbSystem.getDatabase(); Map serverNames = db.query(ServerQueries.fetchServerNames()); - String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); - PlayerContainer player = db.query(new PlayerContainerQuery(playerUUID)); SessionsMutator sessionsMutator = SessionsMutator.forContainer(player); - Map worldTimesPerServer = PerServerMutator.forContainer(player).worldTimesPerServer(); - List> serverAccordion = new ServerAccordion(player, serverNames, graphs, year, timeAmount, locale.getString(GenericLang.UNKNOWN)).asMaps(); - List kills = player.getValue(PlayerKeys.PLAYER_KILLS).orElse(Collections.emptyList()); - List deaths = player.getValue(PlayerKeys.PLAYER_DEATHS_KILLS).orElse(Collections.emptyList()); PingMutator.forContainer(player).addPingToSessions(sessionsMutator.all()); @@ -117,31 +113,52 @@ public class PlayerJSONCreator { data.put("timestamp", now); data.put("timestamp_f", year.apply(now)); - data.put("info", createInfoJSONMap(player, serverNames)); - data.put("online_activity", createOnlineActivityJSONMap(sessionsMutator)); - data.put("kill_data", createPvPPvEMap(player)); + if (hasPermission.test(WebPermission.PAGE_PLAYER_OVERVIEW)) { + data.put("info", createInfoJSONMap(player, serverNames)); + data.put("online_activity", createOnlineActivityJSONMap(sessionsMutator)); + data.put("nicknames", player.getValue(PlayerKeys.NICKNAMES) + .map(nicks -> Nickname.fromDataNicknames(nicks, serverNames, year)) + .orElse(Collections.emptyList())); + data.put("connections", player.getValue(PlayerKeys.GEO_INFO) + .map(geoInfo -> ConnectionInfo.fromGeoInfo(geoInfo, year)) + .orElse(Collections.emptyList())); + data.put("punchcard_series", graphs.special().punchCard(sessionsMutator).getDots()); + } else { + data.put("info", createLimitedInfoMap(player)); + } + if (hasPermission.test(WebPermission.PAGE_PLAYER_SESSIONS)) { + data.put("sessions", sessionsMutator.sort(new DateHolderRecentComparator()).toServerNameJSONMaps(graphs, config.getWorldAliasSettings(), formatters)); + data.put("sessions_per_page", config.get(DisplaySettings.SESSIONS_PER_PAGE)); + WorldPie worldPie = graphs.pie().worldPie(player.getValue(PlayerKeys.WORLD_TIMES).orElse(new WorldTimes())); + data.put("world_pie_series", worldPie.getSlices()); + data.put("gm_series", worldPie.toHighChartsDrillDownMaps()); + data.put("first_day", 1); // Monday + data.put("calendar_series", graphs.calendar().playerCalendar(player).getEntries()); + } + if (hasPermission.test(WebPermission.PAGE_PLAYER_VERSUS)) { + List kills = player.getValue(PlayerKeys.PLAYER_KILLS).orElse(Collections.emptyList()); + List deaths = player.getValue(PlayerKeys.PLAYER_DEATHS_KILLS).orElse(Collections.emptyList()); + + data.put("kill_data", createPvPPvEMap(player)); + data.put("player_kills", new PlayerKillMutator(kills).filterNonSelfKills().toJSONAsMap(formatters)); + data.put("player_deaths", new PlayerKillMutator(deaths).toJSONAsMap(formatters)); + } + if (hasPermission.test(WebPermission.PAGE_PLAYER_SERVERS)) { + List> serverAccordion = new ServerAccordion(player, serverNames, graphs, year, timeAmount, locale.getString(GenericLang.UNKNOWN)).asMaps(); + Map worldTimesPerServer = PerServerMutator.forContainer(player).worldTimesPerServer(); + String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); + + data.put("ping_graph", createPingGraphJson(player)); + data.put("servers", serverAccordion); + data.put("server_pie_series", graphs.pie().serverPreferencePie(serverNames, worldTimesPerServer).getSlices()); + data.put("server_pie_colors", pieColors); + } + if (hasPermission.test(WebPermission.PAGE_PLAYER_PLUGINS)) { + data.put("extensions", playerExtensionData(playerUUID)); + } else { + data.put("extensions", List.of()); + } - data.put("nicknames", player.getValue(PlayerKeys.NICKNAMES) - .map(nicks -> Nickname.fromDataNicknames(nicks, serverNames, year)) - .orElse(Collections.emptyList())); - data.put("connections", player.getValue(PlayerKeys.GEO_INFO) - .map(geoInfo -> ConnectionInfo.fromGeoInfo(geoInfo, year)) - .orElse(Collections.emptyList())); - data.put("player_kills", new PlayerKillMutator(kills).filterNonSelfKills().toJSONAsMap(formatters)); - data.put("player_deaths", new PlayerKillMutator(deaths).toJSONAsMap(formatters)); - data.put("sessions", sessionsMutator.sort(new DateHolderRecentComparator()).toServerNameJSONMaps(graphs, config.getWorldAliasSettings(), formatters)); - data.put("sessions_per_page", config.get(DisplaySettings.SESSIONS_PER_PAGE)); - data.put("servers", serverAccordion); - data.put("punchcard_series", graphs.special().punchCard(sessionsMutator).getDots()); - WorldPie worldPie = graphs.pie().worldPie(player.getValue(PlayerKeys.WORLD_TIMES).orElse(new WorldTimes())); - data.put("world_pie_series", worldPie.getSlices()); - data.put("gm_series", worldPie.toHighChartsDrillDownMaps()); - data.put("calendar_series", graphs.calendar().playerCalendar(player).getEntries()); - data.put("server_pie_series", graphs.pie().serverPreferencePie(serverNames, worldTimesPerServer).getSlices()); - data.put("server_pie_colors", pieColors); - data.put("ping_graph", createPingGraphJson(player)); - data.put("first_day", 1); // Monday - data.put("extensions", playerExtensionData(playerUUID)); return data; } @@ -236,6 +253,15 @@ public class PlayerJSONCreator { return info; } + private Map createLimitedInfoMap(PlayerContainer player) { + Map info = new HashMap<>(); + + info.put("name", player.getValue(PlayerKeys.NAME).orElse(player.getUnsafe(PlayerKeys.UUID).toString())); + info.put("uuid", player.getUnsafe(PlayerKeys.UUID).toString()); + + return info; + } + private Map createPvPPvEMap(PlayerContainer playerContainer) { long now = System.currentTimeMillis(); long weekAgo = now - TimeUnit.DAYS.toMillis(7L); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java index 5461171ae..8ea83c0a8 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java @@ -19,12 +19,16 @@ package com.djrapitops.plan.delivery.web; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.paths.PluginSettings; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.transactions.GrantWebPermissionToGroupsWithPermissionTransaction; +import com.djrapitops.plan.storage.database.transactions.StoreMissingWebPermissionsTransaction; import com.djrapitops.plan.utilities.dev.Untrusted; import net.playeranalytics.plugin.server.PluginLogger; import javax.inject.Inject; import javax.inject.Singleton; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -38,14 +42,16 @@ public class ResolverSvc implements ResolverService { private final PlanConfig config; private final PluginLogger logger; + private final DBSystem dbSystem; private final List basicResolvers; private final List regexResolvers; @Inject - public ResolverSvc(PlanConfig config, PluginLogger logger) { + public ResolverSvc(PlanConfig config, PluginLogger logger, DBSystem dbSystem) { this.config = config; this.logger = logger; + this.dbSystem = dbSystem; basicResolvers = new ArrayList<>(); regexResolvers = new ArrayList<>(); } @@ -72,6 +78,20 @@ public class ResolverSvc implements ResolverService { } } + @Override + public CompletableFuture registerPermissions(String... webPermissions) { + return dbSystem.getDatabase().executeTransaction(new StoreMissingWebPermissionsTransaction(Arrays.asList(webPermissions))) + .thenRun(() -> {}); + } + + @Override + public void registerPermission(String webPermission, String whenHasPermission) { + registerPermissions(webPermission) + .thenRun(() -> dbSystem.getDatabase().executeTransaction( + new GrantWebPermissionToGroupsWithPermissionTransaction(webPermission, whenHasPermission) + )); + } + @Override public Optional getResolver(String target) { for (Container container : basicResolvers) { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java index 0f6575261..c04eca061 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java @@ -60,7 +60,7 @@ import java.util.regex.Pattern; */ @Singleton @OpenAPIDefinition(info = @Info( - title = "Plan API endpoints", + title = "Swagger Docs", description = "If authentication is enabled (see response of /v1/whoami) logging in is required for endpoints (/auth/login). Pass 'Cookie' header in the requests after login.", contact = @Contact(name = "Github Discussions", url = "https://github.com/plan-player-analytics/Plan/discussions/categories/apis-and-development"), license = @License(name = "GNU Lesser General Public License v3.0 (LGPLv3.0)", url = "https://github.com/plan-player-analytics/Plan/blob/master/LICENSE") @@ -82,6 +82,7 @@ public class ResponseResolver { private final ErrorsPageResolver errorsPageResolver; private final SwaggerJsonResolver swaggerJsonResolver; private final SwaggerPageResolver swaggerPageResolver; + private final ManagePageResolver managePageResolver; private final ErrorLogger errorLogger; private final ResolverService resolverService; @@ -116,7 +117,7 @@ public class ResponseResolver { SwaggerJsonResolver swaggerJsonResolver, SwaggerPageResolver swaggerPageResolver, - ErrorLogger errorLogger + ManagePageResolver managePageResolver, ErrorLogger errorLogger ) { this.resolverService = resolverService; this.responseFactory = responseFactory; @@ -138,6 +139,7 @@ public class ResponseResolver { this.errorsPageResolver = errorsPageResolver; this.swaggerJsonResolver = swaggerJsonResolver; this.swaggerPageResolver = swaggerPageResolver; + this.managePageResolver = managePageResolver; this.errorLogger = errorLogger; } @@ -154,7 +156,7 @@ public class ResponseResolver { resolverService.registerResolver(plugin, "/player", playerPageResolver); resolverService.registerResolver(plugin, "/network", serverPageResolver); resolverService.registerResolver(plugin, "/server", serverPageResolver); - if (webserverConfiguration.isAuthenticationEnabled()) { + if (webServer.get().isAuthRequired()) { resolverService.registerResolver(plugin, "/login", loginPageResolver); resolverService.registerResolver(plugin, "/register", registerPageResolver); resolverService.registerResolver(plugin, "/auth/login", loginResolver); @@ -162,6 +164,7 @@ public class ResponseResolver { if (webserverConfiguration.isRegistrationEnabled()) { resolverService.registerResolver(plugin, "/auth/register", registerResolver); } + resolverService.registerResolver(plugin, "/manage", managePageResolver); } resolverService.registerResolver(plugin, "/errors", errorsPageResolver); 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 017630331..565daa3cb 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 @@ -89,13 +89,14 @@ public class ActiveCookieStore implements SubSystem { @Override public void enable() { ActiveCookieStore.setCookiesExpireAfter(config.get(WebserverSettings.COOKIES_EXPIRE_AFTER)); - processing.submitNonCritical(this::loadActiveCookies); + processing.submitNonCritical(this::reloadActiveCookies); } - private void loadActiveCookies() { - USERS_BY_COOKIE.clear(); + public void reloadActiveCookies() { try { - USERS_BY_COOKIE.putAll(dbSystem.getDatabase().query(WebUserQueries.fetchActiveCookies())); + Map cookies = dbSystem.getDatabase().query(WebUserQueries.fetchActiveCookies()); + USERS_BY_COOKIE.clear(); + USERS_BY_COOKIE.putAll(cookies); for (Map.Entry entry : dbSystem.getDatabase().query(WebUserQueries.getCookieExpiryTimes()).entrySet()) { long timeToExpiry = Math.max(entry.getValue() - System.currentTimeMillis(), 0L); activeCookieExpiryCleanupTask.addExpiry(entry.getKey(), System.currentTimeMillis() + timeToExpiry); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/FailReason.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/FailReason.java index b801121e4..ebf63809b 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/FailReason.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/FailReason.java @@ -29,7 +29,9 @@ public enum FailReason implements Lang { EXPIRED_COOKIE("html.error.auth.expiredCookie", "User cookie has expired"), USER_AND_PASS_NOT_SPECIFIED("html.error.auth.emptyForm", "User and Password not specified"), USER_DOES_NOT_EXIST("html.error.auth.userNotFound", "User does not exist"), + GROUP_DOES_NOT_EXIST("html.error.auth.groupNotFound", "Web Permission Group does not exist"), USER_INFORMATION_NOT_FOUND("html.error.auth.registrationFailed", "Registration failed, try again (The code expires after 15 minutes)"), + NO_PERMISSION_GROUP("html.error.auth.noPermissionGroup", "Registration failed, player did not have any 'plan.webgroup.{name}' permission"), USER_PASS_MISMATCH("html.error.auth.loginFailed", "User and Password did not match"), DATABASE_NOT_OPEN("html.error.auth.dbClosed", "Database is not open, check db status with /plan info"), ERROR("html.error.auth.generic", "Authentication failed due to error"); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/RegistrationBin.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/RegistrationBin.java index 615f246d4..f77dcf6a0 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/RegistrationBin.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/RegistrationBin.java @@ -72,7 +72,7 @@ public class RegistrationBin { } public User toUser(UUID linkedToUUID) { - return new User(username, null, linkedToUUID, passwordHash, 100, Collections.emptyList()); + return new User(username, null, linkedToUUID, passwordHash, null, Collections.emptyList()); } } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java index 8b048ae76..549c4e0b8 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; @@ -35,7 +36,7 @@ public class ErrorsPageResolver implements Resolver { @Override public boolean canAccess(Request request) { - return request.getUser().map(user -> user.hasPermission("page.server")).orElse(false); + return request.getUser().map(user -> user.hasPermission(WebPermission.ACCESS_ERRORS)).orElse(false); } @Override diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ManagePageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ManagePageResolver.java new file mode 100644 index 000000000..96aa64592 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ManagePageResolver.java @@ -0,0 +1,48 @@ +/* + * 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.resolver; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.Response; +import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.delivery.webserver.ResponseFactory; + +import javax.inject.Inject; +import java.util.Optional; + +public class ManagePageResolver implements Resolver { + + private final ResponseFactory responseFactory; + + @Inject + public ManagePageResolver( + ResponseFactory responseFactory + ) { + this.responseFactory = responseFactory; + } + + @Override + public boolean canAccess(Request request) { + return request.getUser().map(user -> user.hasPermission(WebPermission.MANAGE_GROUPS) || user.hasPermission(WebPermission.MANAGE_USERS)).orElse(false); + } + + @Override + public Optional resolve(Request request) { + return Optional.of(responseFactory.reactPageResponse(request)); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayerPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayerPageResolver.java index 1d5c40a31..2999a76cf 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayerPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayerPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.rendering.html.Html; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -63,7 +64,10 @@ public class PlayerPageResolver implements Resolver { URIPath path = request.getPath(); WebUser user = request.getUser().orElse(new WebUser("")); boolean isOwnPage = isOwnPage(path, user); - return user.hasPermission("page.player.other") || user.hasPermission("page.player.self") && isOwnPage; + boolean raw = path.getPart(2).map("raw"::equalsIgnoreCase).orElse(false); + boolean canSeeNormalPage = user.hasPermission(WebPermission.ACCESS_PLAYER) + || user.hasPermission(WebPermission.ACCESS_PLAYER_SELF) && isOwnPage; + return canSeeNormalPage && !raw || user.hasPermission(WebPermission.ACCESS_RAW_PLAYER_DATA); } @NotNull diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java index f951e55ca..7af5f3c63 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; @@ -44,7 +45,7 @@ public class PlayersPageResolver implements Resolver { @Override public boolean canAccess(Request request) { - return request.getUser().map(user -> user.hasPermission("page.players")).orElse(false); + return request.getUser().map(user -> user.hasPermission(WebPermission.ACCESS_PLAYERS)).orElse(false); } @Override diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java index 7fb52f452..e6651b585 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; @@ -37,7 +38,7 @@ public class QueryPageResolver implements Resolver { @Override public boolean canAccess(Request request) { - return request.getUser().map(user -> user.hasPermission("page.players")).orElse(false); + return request.getUser().map(user -> user.hasPermission(WebPermission.ACCESS_QUERY)).orElse(false); } @Override diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/RootPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/RootPageResolver.java index 16cbcea88..fe553adbb 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/RootPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/RootPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.rendering.html.Html; import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -42,6 +43,8 @@ import java.util.UUID; @Singleton public class RootPageResolver implements NoAuthResolver { + private static final String NETWORK_PAGE = "network"; + private final ResponseFactory responseFactory; private final Lazy webServer; private final ServerInfo serverInfo; @@ -61,21 +64,31 @@ public class RootPageResolver implements NoAuthResolver { private Response getResponse(Request request) { Server server = serverInfo.getServer(); if (!webServer.get().isAuthRequired()) { - String redirectTo = server.isProxy() ? "network" : "server/" + Html.encodeToURL(server.getIdentifiableName()); + String redirectTo = server.isProxy() ? NETWORK_PAGE : "server/" + Html.encodeToURL(server.getIdentifiableName()); return responseFactory.redirectResponse(redirectTo); } WebUser user = request.getUser() .orElseThrow(() -> new WebUserAuthException(FailReason.EXPIRED_COOKIE)); - if (user.hasPermission("page.server")) { - return responseFactory.redirectResponse(server.isProxy() ? "network" : "server/" + Html.encodeToURL(server.getIdentifiableName())); - } else if (user.hasPermission("page.players")) { + if (server.isProxy() && user.hasPermission(WebPermission.ACCESS_NETWORK)) { + return responseFactory.redirectResponse(NETWORK_PAGE); + } else if (user.hasPermission(WebPermission.ACCESS_SERVER)) { + return responseFactory.redirectResponse(server.isProxy() ? NETWORK_PAGE : "server/" + Html.encodeToURL(server.getIdentifiableName())); + } else if (user.hasPermission(WebPermission.ACCESS_PLAYERS)) { return responseFactory.redirectResponse("players"); - } else if (user.hasPermission("page.player.self")) { + } else if (user.hasPermission(WebPermission.ACCESS_PLAYER_SELF)) { return responseFactory.redirectResponse("player/" + user.getUUID().map(UUID::toString).orElseGet(user::getName)); + } else if (user.hasPermission(WebPermission.ACCESS_QUERY)) { + return responseFactory.redirectResponse("query"); + } else if (user.hasPermission(WebPermission.MANAGE_GROUPS)) { + return responseFactory.redirectResponse("manage"); + } else if (user.hasPermission(WebPermission.ACCESS_DOCS)) { + return responseFactory.redirectResponse("docs"); + } else if (user.hasPermission(WebPermission.ACCESS_ERRORS)) { + return responseFactory.redirectResponse("errors"); } else { - return responseFactory.forbidden403(user.getName() + " has insufficient permissions to be redirected to any page. Needs one of: 'page.server', 'page.players' or 'page.player.self'"); + return responseFactory.forbidden403("User has insufficient permissions to be redirected to any page."); } } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java index 1cb8cde37..ee4477fc9 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.rendering.html.Html; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -42,6 +43,8 @@ import java.util.Optional; @Singleton public class ServerPageResolver implements Resolver { + private static final String NETWORK_PAGE = "network"; + private final ResponseFactory responseFactory; private final DBSystem dbSystem; private final ServerInfo serverInfo; @@ -60,9 +63,9 @@ public class ServerPageResolver implements Resolver { @Override public boolean canAccess(Request request) { @Untrusted String firstPart = request.getPath().getPart(0).orElse(""); - WebUser permissions = request.getUser().orElse(new WebUser("")); - boolean forServerPage = "server".equalsIgnoreCase(firstPart) && permissions.hasPermission("page.server"); - boolean forNetworkPage = "network".equalsIgnoreCase(firstPart) && permissions.hasPermission("page.network"); + WebUser user = request.getUser().orElse(new WebUser("")); + boolean forServerPage = "server".equalsIgnoreCase(firstPart) && user.hasPermission(WebPermission.ACCESS_SERVER); + boolean forNetworkPage = NETWORK_PAGE.equalsIgnoreCase(firstPart) && user.hasPermission(WebPermission.ACCESS_NETWORK); return forServerPage || forNetworkPage; } @@ -75,7 +78,7 @@ public class ServerPageResolver implements Resolver { private Optional redirectToCurrentServer() { String directTo = serverInfo.getServer().isProxy() - ? "/network" + ? "/" + NETWORK_PAGE : "/server/" + Html.encodeToURL(serverInfo.getServer().getIdentifiableName()); return Optional.of(responseFactory.redirectResponse(directTo)); } @@ -83,7 +86,7 @@ public class ServerPageResolver implements Resolver { private Optional getServerPage(ServerUUID serverUUID, @Untrusted Request request) { boolean toNetworkPage = serverInfo.getServer().isProxy() && serverInfo.getServerUUID().equals(serverUUID); if (toNetworkPage) { - if (request.getPath().getPart(0).map("network"::equals).orElse(false)) { + if (request.getPath().getPart(0).map(NETWORK_PAGE::equals).orElse(false)) { return Optional.of(responseFactory.networkPageResponse(request)); } else { // Accessing /server/Server which should be redirected to /network @@ -95,7 +98,7 @@ public class ServerPageResolver implements Resolver { private Optional getServerUUID(@Untrusted URIPath path) { if (serverInfo.getServer().isProxy() - && path.getPart(0).map("network"::equals).orElse(false) + && path.getPart(0).map(NETWORK_PAGE::equals).orElse(false) ) { return Optional.of(serverInfo.getServerUUID()); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ErrorsJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ErrorsJSONResolver.java index 6695151cd..3807ec5b8 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ErrorsJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ErrorsJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.resolver.MimeType; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -57,7 +58,7 @@ public class ErrorsJSONResolver implements Resolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + return request.getUser().orElse(new WebUser("")).hasPermission(WebPermission.ACCESS_ERRORS); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ExtensionJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ExtensionJSONResolver.java index ef666b980..f76ac9ac9 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ExtensionJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ExtensionJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionDataDto; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -71,8 +72,8 @@ public class ExtensionJSONResolver extends JSONResolver { @Override public boolean canAccess(Request request) { - WebUser permissions = request.getUser().orElse(new WebUser("")); - return permissions.hasPermission("page.server") || permissions.hasPermission("page.network"); + WebUser user = request.getUser().orElse(new WebUser("")); + return user.hasPermission(WebPermission.PAGE_NETWORK_PLUGINS) || user.hasPermission(WebPermission.PAGE_SERVER_PLUGINS); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java index b3a8a8c90..78d5f205e 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.delivery.webserver.resolver.json; import com.djrapitops.plan.delivery.domain.DateObj; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.domain.datatransfer.FilterDto; import com.djrapitops.plan.delivery.domain.datatransfer.ViewDto; import com.djrapitops.plan.delivery.formatting.Formatters; @@ -87,7 +88,7 @@ public class FiltersJSONResolver implements Resolver { @Override public boolean canAccess(Request request) { WebUser user = request.getUser().orElse(new WebUser("")); - return user.hasPermission("page.players"); + return user.hasPermission(WebPermission.ACCESS_QUERY); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java index 3802a9488..5cf5c0405 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.graphs.GraphJSONCreator; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -41,6 +42,7 @@ import jakarta.ws.rs.Path; import javax.inject.Inject; import javax.inject.Singleton; +import java.util.List; import java.util.Optional; /** @@ -72,7 +74,21 @@ public class GraphsJSONResolver extends JSONResolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + @Untrusted String type = request.getQuery().get("type") + .orElseThrow(() -> new BadRequestException("'type' parameter was not defined.")); + DataID dataID = getDataID(type); + boolean forServer = request.getQuery().get("server").isPresent(); + + List requiredPermissionOptions = forServer + ? getRequiredPermission(dataID) + : getRequiredNetworkPermission(dataID); + + if (requiredPermissionOptions.isEmpty()) return true; + WebUser user = request.getUser().orElse(new WebUser("")); + for (WebPermission permissionOption : requiredPermissionOptions) { + if (user.hasPermission(permissionOption)) return true; + } + return false; } /** @@ -190,6 +206,65 @@ public class GraphsJSONResolver extends JSONResolver { } } + private List getRequiredPermission(DataID dataID) { + switch (dataID) { + case GRAPH_PERFORMANCE: + return List.of(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS); + case GRAPH_PING: + case GRAPH_OPTIMIZED_PERFORMANCE: + return List.of(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, WebPermission.PAGE_NETWORK_PERFORMANCE); + case GRAPH_ONLINE: + return List.of(WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH); + case GRAPH_UNIQUE_NEW: + return List.of(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY); + case GRAPH_HOURLY_UNIQUE_NEW: + return List.of(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_HOUR_BY_HOUR); + case GRAPH_CALENDAR: + return List.of(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR); + case GRAPH_PUNCHCARD: + return List.of(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_PUNCHCARD); + case GRAPH_WORLD_PIE: + return List.of(WebPermission.PAGE_SERVER_SESSIONS_WORLD_PIE); + case GRAPH_ACTIVITY: + return List.of(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS); + case GRAPH_WORLD_MAP: + return List.of(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP); + case GRAPH_HOSTNAME_PIE: + return List.of(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE); + case JOIN_ADDRESSES_BY_DAY: + return List.of(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME); + default: + return List.of(); + } + } + + private List getRequiredNetworkPermission(DataID dataID) { + switch (dataID) { + case GRAPH_PERFORMANCE: + case GRAPH_OPTIMIZED_PERFORMANCE: + case GRAPH_PING: + return List.of(WebPermission.PAGE_NETWORK_PERFORMANCE); + case GRAPH_ACTIVITY: + return List.of(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS); + case GRAPH_UNIQUE_NEW: + return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_DAY_BY_DAY); + case GRAPH_HOURLY_UNIQUE_NEW: + return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR); + case GRAPH_SERVER_PIE: + return List.of(WebPermission.PAGE_NETWORK_SESSIONS_SERVER_PIE); + case GRAPH_WORLD_MAP: + return List.of(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP); + case GRAPH_ONLINE_PROXIES: + return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE); + case GRAPH_HOSTNAME_PIE: + return List.of(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE); + case JOIN_ADDRESSES_BY_DAY: + return List.of(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME); + default: + return List.of(); + } + } + private Object generateGraphDataJSONOfType(DataID id, ServerUUID serverUUID, @Untrusted URIQuery query) { switch (id) { case GRAPH_PERFORMANCE: diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkJSONResolver.java index 203c9c156..b09d9c8bd 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.rendering.json.network.NetworkOverviewJSONCreator; import com.djrapitops.plan.delivery.rendering.json.network.NetworkPlayerBaseOverviewJSONCreator; @@ -49,19 +50,19 @@ public class NetworkJSONResolver { ) { this.asyncJSONResolverService = asyncJSONResolverService; resolver = CompositeResolver.builder() - .add("overview", forJSON(DataID.SERVER_OVERVIEW, networkOverviewJSONCreator)) - .add("playerbaseOverview", forJSON(DataID.PLAYERBASE_OVERVIEW, networkPlayerBaseOverviewJSONCreator)) - .add("sessionsOverview", forJSON(DataID.SESSIONS_OVERVIEW, networkSessionsOverviewJSONCreator)) - .add("servers", forJSON(DataID.SERVERS, jsonFactory::serversAsJSONMaps)) - .add("pingTable", forJSON(DataID.PING_TABLE, jsonFactory::pingPerGeolocation)) - .add("listServers", forJSON(DataID.LIST_SERVERS, jsonFactory::listServers)) - .add("serverOptions", forJSON(DataID.LIST_SERVERS, jsonFactory::listServers)) + .add("overview", forJSON(DataID.SERVER_OVERVIEW, networkOverviewJSONCreator, WebPermission.PAGE_NETWORK_OVERVIEW_NUMBERS)) + .add("playerbaseOverview", forJSON(DataID.PLAYERBASE_OVERVIEW, networkPlayerBaseOverviewJSONCreator, WebPermission.PAGE_NETWORK_PLAYERBASE_OVERVIEW)) + .add("sessionsOverview", forJSON(DataID.SESSIONS_OVERVIEW, networkSessionsOverviewJSONCreator, WebPermission.PAGE_NETWORK_SESSIONS_OVERVIEW)) + .add("servers", forJSON(DataID.SERVERS, jsonFactory::serversAsJSONMaps, WebPermission.PAGE_NETWORK_SERVER_LIST)) + .add("pingTable", forJSON(DataID.PING_TABLE, jsonFactory::pingPerGeolocation, WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY)) + .add("listServers", forJSON(DataID.LIST_SERVERS, jsonFactory::listServers, WebPermission.PAGE_NETWORK_PERFORMANCE)) + .add("serverOptions", forJSON(DataID.LIST_SERVERS, jsonFactory::listServers, WebPermission.PAGE_NETWORK_PERFORMANCE)) .add("performanceOverview", networkPerformanceJSONResolver) .build(); } - private NetworkTabJSONResolver forJSON(DataID dataID, NetworkTabJSONCreator tabJSONCreator) { - return new NetworkTabJSONResolver<>(dataID, tabJSONCreator, asyncJSONResolverService); + private NetworkTabJSONResolver forJSON(DataID dataID, NetworkTabJSONCreator tabJSONCreator, WebPermission permission) { + return new NetworkTabJSONResolver<>(dataID, permission, tabJSONCreator, asyncJSONResolverService); } public CompositeResolver getResolver() { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java index 52aae6452..1121bb137 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.domain.mutators.TPSMutator; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.formatting.Formatters; @@ -92,7 +93,7 @@ public class NetworkPerformanceJSONResolver implements Resolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.network"); + return request.getUser().orElse(new WebUser("")).hasPermission(WebPermission.PAGE_NETWORK_PERFORMANCE); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkTabJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkTabJSONResolver.java index a709542c4..0d10608d9 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkTabJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkTabJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.network.NetworkTabJSONCreator; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -37,14 +38,16 @@ import java.util.function.Supplier; public class NetworkTabJSONResolver extends JSONResolver { private final DataID dataID; + private final WebPermission permission; private final Supplier jsonCreator; private final AsyncJSONResolverService asyncJSONResolverService; public NetworkTabJSONResolver( - DataID dataID, NetworkTabJSONCreator jsonCreator, + DataID dataID, WebPermission permission, NetworkTabJSONCreator jsonCreator, AsyncJSONResolverService asyncJSONResolverService ) { this.dataID = dataID; + this.permission = permission; this.jsonCreator = jsonCreator; this.asyncJSONResolverService = asyncJSONResolverService; } @@ -54,7 +57,7 @@ public class NetworkTabJSONResolver extends JSONResolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.network"); + return request.getUser().orElse(new WebUser("")).hasPermission(permission); } @Override diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJSONResolver.java index c1412a96f..e644a9c76 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.formatting.Formatters; import com.djrapitops.plan.delivery.rendering.json.PlayerJSONCreator; @@ -43,6 +44,7 @@ import javax.inject.Singleton; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Predicate; @Singleton @Path("/v1/player") @@ -63,8 +65,8 @@ public class PlayerJSONResolver implements Resolver { @Override public boolean canAccess(Request request) { WebUser user = request.getUser().orElse(new WebUser("")); - if (user.hasPermission("page.player.other")) return true; - if (user.hasPermission("page.player.self")) { + if (user.hasPermission(WebPermission.ACCESS_PLAYER)) return true; + if (user.hasPermission(WebPermission.ACCESS_PLAYER_SELF)) { try { UUID webUserUUID = identifiers.getPlayerUUID(user.getName()); UUID playerUUID = identifiers.getPlayerUUID(request); @@ -97,10 +99,12 @@ public class PlayerJSONResolver implements Resolver { private Response getResponse(Request request) { UUID playerUUID = identifiers.getPlayerUUID(request); // Can throw BadRequestException - Optional etag = Identifiers.getEtag(request); + // User needs to be taken into account due to permissions. + String userSpecific = request.getUser().map(WebUser::getUsername).orElse(""); + Optional etag = Identifiers.getStringEtag(request); if (etag.isPresent()) { long lastSeen = jsonCreator.getLastSeen(playerUUID); - if (etag.get() == lastSeen) { + if (etag.get().equals(lastSeen + userSpecific)) { return Response.builder() .setStatus(304) .setContent(new byte[0]) @@ -108,7 +112,10 @@ public class PlayerJSONResolver implements Resolver { } } - Map jsonAsMap = jsonCreator.createJSONAsMap(playerUUID); + Predicate hasPermission = request.getUser() + .map(user -> (Predicate) user::hasPermission) + .orElse(permission -> true); // No user means auth disabled inside resolve + Map jsonAsMap = jsonCreator.createJSONAsMap(playerUUID, hasPermission); long lastSeenRawValue = Optional.ofNullable(jsonAsMap.get("info")) .map(Map.class::cast) .map(info -> info.get("last_seen_raw_value")) @@ -119,7 +126,7 @@ public class PlayerJSONResolver implements Resolver { .setJSONContent(jsonAsMap) .setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG_USER_SPECIFIC) .setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastSeenRawValue)) - .setHeader(HttpHeader.ETAG.asString(), lastSeenRawValue) + .setHeader(HttpHeader.ETAG.asString(), lastSeenRawValue + userSpecific) .build(); } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java index fe84605ff..5299d545f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -67,7 +68,11 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver { @Override public boolean canAccess(@Untrusted Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + WebUser user = request.getUser().orElse(new WebUser("")); + if (request.getQuery().get("server").isPresent()) { + return user.hasPermission(WebPermission.PAGE_SERVER_RETENTION); + } + return user.hasPermission(WebPermission.PAGE_NETWORK_RETENTION); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerKillsJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerKillsJSONResolver.java index 3ba66a62f..79dffb304 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerKillsJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerKillsJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -71,7 +72,7 @@ public class PlayerKillsJSONResolver extends JSONResolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + return request.getUser().orElse(new WebUser("")).hasPermission(WebPermission.PAGE_SERVER_PLAYER_VERSUS_KILL_LIST); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayersTableJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayersTableJSONResolver.java index 33dea3755..ef009157f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayersTableJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayersTableJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -73,10 +74,11 @@ public class PlayersTableJSONResolver extends JSONResolver { public boolean canAccess(Request request) { WebUser user = request.getUser().orElse(new WebUser("")); if (request.getQuery().get("server").isPresent()) { - return user.hasPermission("page.server"); + return user.hasPermission(WebPermission.PAGE_SERVER_PLAYERS); } // Assume players page - return user.hasPermission("page.players"); + return user.hasPermission(WebPermission.ACCESS_PLAYERS) + || user.hasPermission(WebPermission.ACCESS_NETWORK) && user.hasPermission(WebPermission.PAGE_NETWORK_PLAYERS); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java index afe3edcf9..2280d395f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.delivery.webserver.resolver.json; import com.djrapitops.plan.delivery.domain.DateMap; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.domain.datatransfer.InputFilterDto; import com.djrapitops.plan.delivery.domain.datatransfer.InputQueryDto; import com.djrapitops.plan.delivery.domain.datatransfer.ViewDto; @@ -109,7 +110,7 @@ public class QueryJSONResolver implements Resolver { @Override public boolean canAccess(Request request) { WebUser user = request.getUser().orElse(new WebUser("")); - return user.hasPermission("page.players"); + return user.hasPermission(WebPermission.ACCESS_QUERY); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RetentionJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RetentionJSONResolver.java index be2bc51a3..9706b39c0 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RetentionJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RetentionJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -67,7 +68,11 @@ public class RetentionJSONResolver extends JSONResolver { @Override public boolean canAccess(@Untrusted Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + WebUser user = request.getUser().orElse(new WebUser("")); + if (request.getQuery().get("server").isPresent()) { + return user.hasPermission(WebPermission.PAGE_SERVER_RETENTION); + } + return user.hasPermission(WebPermission.PAGE_NETWORK_RETENTION); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java index 7fb1aeef2..3c94bc035 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java @@ -16,11 +16,14 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.rendering.json.*; import com.djrapitops.plan.delivery.web.resolver.CompositeResolver; import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService; import com.djrapitops.plan.delivery.webserver.cache.DataID; +import com.djrapitops.plan.delivery.webserver.http.WebServer; import com.djrapitops.plan.identification.Identifiers; +import dagger.Lazy; import javax.inject.Inject; import javax.inject.Singleton; @@ -35,13 +38,22 @@ public class RootJSONResolver { private final Identifiers identifiers; private final AsyncJSONResolverService asyncJSONResolverService; - private final CompositeResolver resolver; + private final Lazy webServer; + private final WebGroupJSONResolver webGroupJSONResolver; + private final WebGroupPermissionJSONResolver webGroupPermissionJSONResolver; + private final WebPermissionJSONResolver webPermissionJSONResolver; + private final WebGroupSaveJSONResolver webGroupSaveJSONResolver; + private final WebGroupDeleteJSONResolver webGroupDeleteJSONResolver; + + private final CompositeResolver.Builder readOnlyResourcesBuilder; + private CompositeResolver resolver; @Inject public RootJSONResolver( Identifiers identifiers, AsyncJSONResolverService asyncJSONResolverService, JSONFactory jsonFactory, + Lazy webServer, GraphsJSONResolver graphsJSONResolver, SessionsJSONResolver sessionsJSONResolver, @@ -67,23 +79,29 @@ public class RootJSONResolver { ServerIdentityJSONResolver serverIdentityJSONResolver, ExtensionJSONResolver extensionJSONResolver, RetentionJSONResolver retentionJSONResolver, - PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver + PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver, + + WebGroupJSONResolver webGroupJSONResolver, + WebGroupPermissionJSONResolver webGroupPermissionJSONResolver, + WebPermissionJSONResolver webPermissionJSONResolver, + WebGroupSaveJSONResolver webGroupSaveJSONResolver, + WebGroupDeleteJSONResolver webGroupDeleteJSONResolver ) { this.identifiers = identifiers; this.asyncJSONResolverService = asyncJSONResolverService; - resolver = CompositeResolver.builder() + readOnlyResourcesBuilder = CompositeResolver.builder() .add("players", playersTableJSONResolver) .add("sessions", sessionsJSONResolver) .add("kills", playerKillsJSONResolver) .add("graph", graphsJSONResolver) - .add("pingTable", forJSON(DataID.PING_TABLE, jsonFactory::pingPerGeolocation)) - .add("serverOverview", forJSON(DataID.SERVER_OVERVIEW, serverOverviewJSONCreator)) - .add("onlineOverview", forJSON(DataID.ONLINE_OVERVIEW, onlineActivityOverviewJSONCreator)) - .add("sessionsOverview", forJSON(DataID.SESSIONS_OVERVIEW, sessionsOverviewJSONCreator)) - .add("playerVersus", forJSON(DataID.PVP_PVE, pvPPvEJSONCreator)) - .add("playerbaseOverview", forJSON(DataID.PLAYERBASE_OVERVIEW, playerBaseOverviewJSONCreator)) - .add("performanceOverview", forJSON(DataID.PERFORMANCE_OVERVIEW, performanceJSONCreator)) + .add("pingTable", forJSON(DataID.PING_TABLE, jsonFactory::pingPerGeolocation, WebPermission.PAGE_SERVER_GEOLOCATIONS_PING_PER_COUNTRY)) + .add("serverOverview", forJSON(DataID.SERVER_OVERVIEW, serverOverviewJSONCreator, WebPermission.PAGE_SERVER_OVERVIEW_NUMBERS)) + .add("onlineOverview", forJSON(DataID.ONLINE_OVERVIEW, onlineActivityOverviewJSONCreator, WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_OVERVIEW)) + .add("sessionsOverview", forJSON(DataID.SESSIONS_OVERVIEW, sessionsOverviewJSONCreator, WebPermission.PAGE_SERVER_SESSIONS_OVERVIEW)) + .add("playerVersus", forJSON(DataID.PVP_PVE, pvPPvEJSONCreator, WebPermission.PAGE_SERVER_PLAYER_VERSUS_OVERVIEW)) + .add("playerbaseOverview", forJSON(DataID.PLAYERBASE_OVERVIEW, playerBaseOverviewJSONCreator, WebPermission.PAGE_SERVER_PLAYERBASE_OVERVIEW)) + .add("performanceOverview", forJSON(DataID.PERFORMANCE_OVERVIEW, performanceJSONCreator, WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW)) .add("player", playerJSONResolver) .add("network", networkJSONResolver.getResolver()) .add("filters", filtersJSONResolver) @@ -97,15 +115,36 @@ public class RootJSONResolver { .add("whoami", whoAmIJSONResolver) .add("extensionData", extensionJSONResolver) .add("retention", retentionJSONResolver) - .add("joinAddresses", playerJoinAddressJSONResolver) - .build(); + .add("joinAddresses", playerJoinAddressJSONResolver); + + this.webServer = webServer; + // These endpoints require authentication to be enabled. + this.webGroupJSONResolver = webGroupJSONResolver; + this.webGroupPermissionJSONResolver = webGroupPermissionJSONResolver; + this.webPermissionJSONResolver = webPermissionJSONResolver; + this.webGroupSaveJSONResolver = webGroupSaveJSONResolver; + this.webGroupDeleteJSONResolver = webGroupDeleteJSONResolver; } - private ServerTabJSONResolver forJSON(DataID dataID, ServerTabJSONCreator tabJSONCreator) { - return new ServerTabJSONResolver<>(dataID, identifiers, tabJSONCreator, asyncJSONResolverService); + private ServerTabJSONResolver forJSON(DataID dataID, ServerTabJSONCreator tabJSONCreator, WebPermission permission) { + return new ServerTabJSONResolver<>(dataID, permission, identifiers, tabJSONCreator, asyncJSONResolverService); } public CompositeResolver getResolver() { + if (resolver == null) { + if (webServer.get().isAuthRequired()) { + resolver = readOnlyResourcesBuilder + .add("webGroups", webGroupJSONResolver) + .add("groupPermissions", webGroupPermissionJSONResolver) + .add("permissions", webPermissionJSONResolver) + .add("saveGroupPermissions", webGroupSaveJSONResolver) + .add("deleteGroup", webGroupDeleteJSONResolver) + .build(); + } else { + resolver = readOnlyResourcesBuilder.build(); + } + } + return resolver; } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerIdentityJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerIdentityJSONResolver.java index f929a1044..4b8dbdcdd 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerIdentityJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerIdentityJSONResolver.java @@ -16,14 +16,16 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; -import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver; +import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException; import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException; import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.utilities.dev.Untrusted; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -43,7 +45,7 @@ import java.util.Optional; * @author AuroraLS3 */ @Singleton -public class ServerIdentityJSONResolver implements NoAuthResolver { +public class ServerIdentityJSONResolver implements Resolver { private final JSONFactory jsonFactory; @@ -52,6 +54,12 @@ public class ServerIdentityJSONResolver implements NoAuthResolver { this.jsonFactory = jsonFactory; } + @Override + public boolean canAccess(Request request) { + WebUser user = request.getUser().orElse(new WebUser("")); + return user.hasPermission(WebPermission.ACCESS_SERVER); + } + @GET @Operation( description = "Get server identity for an identifier", diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerTabJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerTabJSONResolver.java index e0fb693b7..da82b532f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerTabJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/ServerTabJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.ServerTabJSONCreator; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -39,15 +40,17 @@ import java.util.function.Function; public class ServerTabJSONResolver extends JSONResolver { private final DataID dataID; + private final WebPermission permission; private final Identifiers identifiers; private final Function jsonCreator; private final AsyncJSONResolverService asyncJSONResolverService; public ServerTabJSONResolver( - DataID dataID, Identifiers identifiers, ServerTabJSONCreator jsonCreator, + DataID dataID, WebPermission permission, Identifiers identifiers, ServerTabJSONCreator jsonCreator, AsyncJSONResolverService asyncJSONResolverService ) { this.dataID = dataID; + this.permission = permission; this.identifiers = identifiers; this.jsonCreator = jsonCreator; this.asyncJSONResolverService = asyncJSONResolverService; @@ -58,7 +61,7 @@ public class ServerTabJSONResolver extends JSONResolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + return request.getUser().orElse(new WebUser("")).hasPermission(permission); } @Override diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/SessionsJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/SessionsJSONResolver.java index d8e906793..5d1cf79fd 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/SessionsJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/SessionsJSONResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -72,7 +73,11 @@ public class SessionsJSONResolver extends JSONResolver { @Override public boolean canAccess(Request request) { - return request.getUser().orElse(new WebUser("")).hasPermission("page.server"); + WebUser user = request.getUser().orElse(new WebUser("")); + if (request.getQuery().get("server").isPresent()) { + return user.hasPermission(WebPermission.PAGE_SERVER_SESSIONS_LIST); + } + return user.hasPermission(WebPermission.PAGE_NETWORK_SESSIONS_LIST); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/VersionJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/VersionJSONResolver.java index e9b5521fa..f6a6357c3 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/VersionJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/VersionJSONResolver.java @@ -17,7 +17,7 @@ package com.djrapitops.plan.delivery.webserver.resolver.json; import com.djrapitops.plan.delivery.web.resolver.MimeType; -import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; import com.djrapitops.plan.version.VersionChecker; @@ -42,7 +42,7 @@ import java.util.Optional; * @author Kopo942 */ @Path("/v1/version") -public class VersionJSONResolver implements Resolver { +public class VersionJSONResolver implements NoAuthResolver { private final VersionChecker versionChecker; private final String currentVersion; @@ -56,11 +56,6 @@ public class VersionJSONResolver implements Resolver { this.versionChecker = versionChecker; } - @Override - public boolean canAccess(Request request) { - return true; - } - @GET @Operation( description = "Get Plan version and update information", diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupDeleteJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupDeleteJSONResolver.java new file mode 100644 index 000000000..c174f2d66 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupDeleteJSONResolver.java @@ -0,0 +1,108 @@ +/* + * 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.resolver.json; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.web.resolver.MimeType; +import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.Response; +import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException; +import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.transactions.DeleteWebGroupTransaction; +import com.djrapitops.plan.utilities.dev.Untrusted; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * Endpoint for adding a group. + * + * @author AuroraLS3 + */ +@Singleton +@Path("/v1/deleteGroup") +public class WebGroupDeleteJSONResolver implements Resolver { + + private final DBSystem dbSystem; + private final ActiveCookieStore activeCookieStore; + + @Inject + public WebGroupDeleteJSONResolver(DBSystem dbSystem, ActiveCookieStore activeCookieStore) { + this.dbSystem = dbSystem; + this.activeCookieStore = activeCookieStore; + } + + @Override + public boolean canAccess(Request request) { + return request.getUser().map(user -> user.hasPermission(WebPermission.MANAGE_GROUPS)).orElse(false); + } + + @DELETE + @Operation( + description = "Delete a group", + parameters = { + @Parameter(name = "group", description = "Name of the group to delete", required = true), + @Parameter(name = "moveTo", description = "Name of the group to move users of deleted group to", required = true) + }, + responses = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, + examples = @ExampleObject("{\"success\": true}"))), + }, + requestBody = @RequestBody(content = @Content(examples = @ExampleObject())) + ) + @Override + public Optional resolve(Request request) { + if (!request.getMethod().equals("DELETE")) { + throw new BadRequestException("Endpoint needs to be sent a DELETE request."); + } + @Untrusted String groupName = request.getQuery().get("group") + .orElseThrow(() -> new BadRequestException("'group' parameter not given.")); + @Untrusted String moveTo = request.getQuery().get("moveTo") + .orElseThrow(() -> new BadRequestException("'moveTo' parameter not given.")); + + return Optional.of(getResponse(groupName, moveTo)); + } + + private Response getResponse(@Untrusted String groupName, @Untrusted String moveTo) { + try { + dbSystem.getDatabase().executeTransaction(new DeleteWebGroupTransaction(groupName, moveTo)) + .get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new IllegalStateException(e); + } + activeCookieStore.reloadActiveCookies(); + + return Response.builder() + .setStatus(200) + .setJSONContent("{\"success\": true}") + .build(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupJSONResolver.java new file mode 100644 index 000000000..5d6e933d5 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupJSONResolver.java @@ -0,0 +1,90 @@ +/* + * 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.resolver.json; + +import com.djrapitops.plan.delivery.domain.auth.Group; +import com.djrapitops.plan.delivery.domain.auth.GroupList; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.web.resolver.MimeType; +import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.Response; +import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Endpoint for getting list of Plan web permission groups. + * + * @author AuroraLS3 + */ +@Singleton +@Path("/v1/webGroups") +public class WebGroupJSONResolver implements Resolver { + + private final DBSystem dbSystem; + + @Inject + public WebGroupJSONResolver(DBSystem dbSystem) { + this.dbSystem = dbSystem; + } + + @Override + public boolean canAccess(Request request) { + return request.getUser().map(user -> user.hasPermission(WebPermission.MANAGE_GROUPS)).orElse(false); + } + + @GET + @Operation( + description = "Get list of web permission groups", + responses = { + @ApiResponse(responseCode = "200", content = @Content( + mediaType = MimeType.JSON, + schema = @Schema(implementation = GroupList.class))), + }, + requestBody = @RequestBody(content = @Content(examples = @ExampleObject())) + ) + @Override + public Optional resolve(Request request) { + return Optional.of(getResponse()); + } + + private Response getResponse() { + List groupNames = dbSystem.getDatabase().query(WebUserQueries.fetchGroupNames()); + + GroupList groupList = new GroupList(groupNames.stream() + .map(Group::new) + .collect(Collectors.toList())); + return Response.builder() + .setMimeType(MimeType.JSON) + .setJSONContent(groupList) + .build(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupPermissionJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupPermissionJSONResolver.java new file mode 100644 index 000000000..84f91108d --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupPermissionJSONResolver.java @@ -0,0 +1,94 @@ +/* + * 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.resolver.json; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.domain.auth.WebPermissionList; +import com.djrapitops.plan.delivery.web.resolver.MimeType; +import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.Response; +import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException; +import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.utilities.dev.Untrusted; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.Optional; + +/** + * Endpoint for getting list of Plan web permissions of specific group. + * + * @author AuroraLS3 + */ +@Singleton +@Path("/v1/groupPermissions") +public class WebGroupPermissionJSONResolver implements Resolver { + + private final DBSystem dbSystem; + + @Inject + public WebGroupPermissionJSONResolver(DBSystem dbSystem) { + this.dbSystem = dbSystem; + } + + @Override + public boolean canAccess(Request request) { + return request.getUser().map(user -> user.hasPermission(WebPermission.MANAGE_GROUPS)).orElse(false); + } + + @GET + @Operation( + description = "Get list of web permissions that have been granted to specific group", + parameters = { + @Parameter(name = "group", description = "Name of the group", required = true), + }, + responses = { + @ApiResponse(responseCode = "200", content = @Content( + mediaType = MimeType.JSON, + schema = @Schema(implementation = WebPermissionList.class))), + }, + requestBody = @RequestBody(content = @Content(examples = @ExampleObject())) + ) + @Override + public Optional resolve(Request request) { + return Optional.of(getResponse(request)); + } + + private Response getResponse(@Untrusted Request request) { + @Untrusted String group = request.getQuery().get("group") + .orElseThrow(() -> new BadRequestException("Parameter 'group' not given.")); + List permissions = dbSystem.getDatabase().query(WebUserQueries.fetchGroupPermissions(group)); + + WebPermissionList permissionList = new WebPermissionList(permissions); + return Response.builder() + .setMimeType(MimeType.JSON) + .setJSONContent(permissionList) + .build(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupSaveJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupSaveJSONResolver.java new file mode 100644 index 000000000..a196acb8c --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebGroupSaveJSONResolver.java @@ -0,0 +1,113 @@ +/* + * 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.resolver.json; + +import com.djrapitops.plan.delivery.domain.auth.GroupList; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.web.resolver.MimeType; +import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.Response; +import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException; +import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; +import com.google.gson.Gson; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * Endpoint for storing new permissions for a group. + * + * @author AuroraLS3 + */ +@Singleton +@Path("/v1/saveGroupPermissions") +public class WebGroupSaveJSONResolver implements Resolver { + + private final DBSystem dbSystem; + private final ActiveCookieStore activeCookieStore; + + @Inject + public WebGroupSaveJSONResolver(DBSystem dbSystem, ActiveCookieStore activeCookieStore) { + this.dbSystem = dbSystem; + this.activeCookieStore = activeCookieStore; + } + + @Override + public boolean canAccess(Request request) { + return request.getUser().map(user -> user.hasPermission(WebPermission.MANAGE_GROUPS)).orElse(false); + } + + @POST + @Operation( + description = "Update list of permissions for a group", + parameters = { + @Parameter(name = "group", description = "Name of the group", required = true), + }, + responses = { + @ApiResponse(responseCode = "200", content = @Content( + mediaType = MimeType.JSON, + schema = @Schema(implementation = GroupList.class))), + }, + requestBody = @RequestBody(content = @Content(examples = @ExampleObject("[\"page\"]"))) + ) + @Override + public Optional resolve(Request request) { + if (!request.getMethod().equals("POST")) { + throw new BadRequestException("Endpoint needs to be sent a POST request."); + } + + String groupName = request.getQuery().get("group") + .orElseThrow(() -> new BadRequestException("'group' parameter not given.")); + String requestBody = new String(request.getRequestBody(), StandardCharsets.UTF_8); + List permissions = Arrays.asList(new Gson().fromJson(requestBody, String[].class)); + return Optional.of(getResponse(groupName, permissions)); + } + + private Response getResponse(String groupName, List permissions) { + try { + dbSystem.getDatabase().executeTransaction(new StoreWebGroupTransaction(groupName, permissions)) + .get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new IllegalStateException(e); + } + activeCookieStore.reloadActiveCookies(); + + return Response.builder() + .setStatus(200) + .setJSONContent("{\"success\": true}") + .build(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebPermissionJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebPermissionJSONResolver.java new file mode 100644 index 000000000..2aa83de10 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/WebPermissionJSONResolver.java @@ -0,0 +1,86 @@ +/* + * 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.resolver.json; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.domain.auth.WebPermissionList; +import com.djrapitops.plan.delivery.web.resolver.MimeType; +import com.djrapitops.plan.delivery.web.resolver.Resolver; +import com.djrapitops.plan.delivery.web.resolver.Response; +import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.Optional; + +/** + * Endpoint for getting list of available Plan web permissions. + * + * @author AuroraLS3 + */ +@Singleton +@Path("/v1/permissions") +public class WebPermissionJSONResolver implements Resolver { + + private final DBSystem dbSystem; + + @Inject + public WebPermissionJSONResolver(DBSystem dbSystem) { + this.dbSystem = dbSystem; + } + + @Override + public boolean canAccess(Request request) { + return request.getUser().map(user -> user.hasPermission(WebPermission.MANAGE_GROUPS)).orElse(false); + } + + @GET + @Operation( + description = "Get list of web permissions that can be granted", + responses = { + @ApiResponse(responseCode = "200", content = @Content( + mediaType = MimeType.JSON, + schema = @Schema(implementation = WebPermissionList.class))), + }, + requestBody = @RequestBody(content = @Content(examples = @ExampleObject())) + ) + @Override + public Optional resolve(Request request) { + return Optional.of(getResponse()); + } + + private Response getResponse() { + List permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions()); + + WebPermissionList permissionList = new WebPermissionList(permissions); + return Response.builder() + .setMimeType(MimeType.JSON) + .setJSONContent(permissionList) + .build(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerJsonResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerJsonResolver.java index 21e649147..70cf2184b 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerJsonResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerJsonResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.swagger; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; @@ -38,7 +39,7 @@ public class SwaggerJsonResolver implements Resolver { @Override public boolean canAccess(Request request) { return request.getUser() - .filter(user -> user.hasPermission("page.server")) + .filter(user -> user.hasPermission(WebPermission.ACCESS_DOCS)) .isPresent(); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerPageResolver.java index b2eacfe2d..83a7e04ef 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerPageResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/swagger/SwaggerPageResolver.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.swagger; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; @@ -38,7 +39,7 @@ public class SwaggerPageResolver implements Resolver { @Override public boolean canAccess(Request request) { return request.getUser() - .filter(user -> user.hasPermission("page.server")) + .filter(user -> user.hasPermission(WebPermission.ACCESS_DOCS)) .isPresent(); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/identification/Identifiers.java b/Plan/common/src/main/java/com/djrapitops/plan/identification/Identifiers.java index 4efcfa8aa..2c12038d1 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/identification/Identifiers.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/identification/Identifiers.java @@ -93,6 +93,10 @@ public class Identifiers { }); } + public static Optional getStringEtag(Request request) { + return request.getHeader(HttpHeader.IF_NONE_MATCH.asString()); + } + /** * Obtain UUID of the server. * 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 2f691da1e..1fb78423a 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"), + SET_GROUP("plan.setgroup.other"), LOGOUT_OTHER("plan.logout.other"), INFO("plan.info"), RELOAD("plan.reload"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java index daa5e8a91..bb5aee4d1 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.settings.locale; import com.djrapitops.plan.SubSystem; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.web.AssetVersions; import com.djrapitops.plan.delivery.webserver.auth.FailReason; import com.djrapitops.plan.settings.config.PlanConfig; @@ -104,6 +105,7 @@ public class LocaleSystem implements SubSystem { HtmlLang.values(), JSLang.values(), PluginLang.values(), + WebPermission.values(), }; } 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 66cde63de..1c308293a 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 @@ -46,7 +46,9 @@ public enum DeepHelpLang implements Lang { DB_UNINSTALLED("command.help.dbUninstalled.inDepth", "In Depth Help - /plan db uninstalled", "Marks a server in Plan database as uninstalled so that it will not show up in server queries."), EXPORT("command.help.export.inDepth", "In Depth Help - /plan export", "Performs an export to export location defined in the config."), IMPORT("command.help.import.inDepth", "In Depth Help - /plan import", "Performs an import to load data into the database."), - JSON("command.help.json.inDepth", "In Depth Help - /plan json", "Allows you to download a player's data in json format. All of it."); + JSON("command.help.json.inDepth", "In Depth Help - /plan json", "Allows you to download a player's data in json format. All of it."), + SET_GROUP("command.help.setgroup.inDepth", "In Depth Help - /plan setgroup", "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups."), + GROUPS("command.help.groups.inDepth", "In Depth Help - /plan groups", "List available web permission groups that are managed on the web interface."); private final String key; private final String identifier; 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 5a002eb96..4ea6b53da 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 @@ -26,6 +26,7 @@ public enum HelpLang implements Lang { ARG_NAME_UUID("command.argument.nameOrUUID.name", "CMD Arg Name - name or uuid", "name/uuid"), ARG_CODE("command.argument.code.name", "CMD Arg Name - code", "${code}"), ARG_USERNAME("command.argument.username.name", "CMD Arg Name - username", "username"), + ARG_GROUP("command.argument.group.name", "CMD Arg Name - group", "group"), ARG_FEATURE("command.argument.feature.name", "CMD Arg Name - feature", "feature"), ARG_SUBCOMMAND("command.argument.subcommand.name", "CMD Arg Name - subcommand", "subcommand"), ARG_BACKUP_FILE("command.argument.backupFile.name", "CMD Arg Name - backup-file", "backup-file"), @@ -36,6 +37,7 @@ public enum HelpLang implements Lang { DESC_ARG_PLAYER_IDENTIFIER_REMOVE("command.argument.nameOrUUID.removeDescription", "CMD Arg - player identifier remove", "Identifier for a player that will be removed from current database."), DESC_ARG_CODE("command.argument.code.description", "CMD Arg - code", "Code used to finalize registration."), DESC_ARG_USERNAME("command.argument.username.description", "CMD Arg - username", "Username of another user. If not specified player linked user is used."), + DESC_ARG_GROUP("command.argument.group.description", "CMD Arg - group", "Web Permission Group, case sensitive."), DESC_ARG_FEATURE("command.argument.feature.description", "CMD Arg - feature", "Name of the feature to disable: ${0}"), DESC_ARG_SUBCOMMAND("command.argument.subcommand.description", "CMD Arg - subcommand", "Use the command without subcommand to see help."), DESC_ARG_BACKUP_FILE("command.argument.backupFile.description", "CMD Arg - backup-file", "Name of the backup file (case sensitive)"), @@ -70,6 +72,8 @@ public enum HelpLang implements Lang { EXPORT("command.help.export.description", "Command Help - /plan export", "Export html or json files manually"), IMPORT("command.help.import.description", "Command Help - /plan import", "Import data"), JSON("command.help.json.description", "Command Help - /plan json", "View json of Player's raw data."), + SET_GROUP("command.help.setgroup.description", "Command Help - /plan setgroup", "Change users web permission group."), + GROUPS("command.help.groups.description", "Command Help - /plan groups", "List web permission groups."), LOGOUT("command.help.logout.description", "Command Help - /plan logout", "Log out other users from the panel."), JOIN_ADDRESS_REMOVAL("command.help.removejoinaddresses.description", "Command Help - /plan db removejoinaddresses", "Remove join addresses of a specified server"), ONLINE_UUID_MIGRATION("command.help.migrateToOnlineUuids.description", "Command Help - /plan db migratetoonlineuuids", "Migrate offline uuid data to online uuids"); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index 77019c28f..ec826a2e2 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -36,6 +36,12 @@ public enum HtmlLang implements Lang { SIDE_LINKS("html.label.links", "LINKS"), SIDE_PERFORMANCE("html.label.performance", "Performance"), SIDE_PLUGINS_OVERVIEW("html.label.pluginsOverview", "Plugins Overview"), + SIDE_MANAGE("html.label.manage", "Manage"), + SIDE_MANAGE_GROUPS("html.label.groupPermissions", "Manage Groups"), + SIDE_MANAGE_GROUP_USERS("html.label.groupUsers", "Manage Group Users"), + SIDE_MANAGE_USER_GROUPS("html.label.users", "Manage Users"), + SIDE_ERRORS("html.label.errors", "Plan Error Logs"), + SIDE_DOCS("html.label.docs", "Swagger Docs"), QUERY_MAKE("html.label.query", "Make a query"), UNIT_NO_DATA("generic.noData", "No Data"), // Generic GRAPH_NO_DATA("html.label.noDataToDisplay", "No Data to Display"), @@ -319,6 +325,22 @@ public enum HtmlLang implements Lang { HELP_ACTIVITY_INDEX_EXAMPLE_3("html.label.help.activityIndexExample3", "The index approaches 5 indefinitely."), HELP_ACTIVITY_INDEX_VISUALIZATION("html.label.help.activityIndexVisual", "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."), + HELP_GROUPS_1("html.label.help.manage.groups.line-1", "This view allows you to modify web group permissions."), + HELP_GROUPS_2("html.label.help.manage.groups.line-2", "User's web group is determined during {{command}} by checking if Player has {{permission}} permission."), + HELP_GROUPS_3("html.label.help.manage.groups.line-3", "You can use {{command}} to change permission group after registering."), + HELP_GROUPS_4("html.label.help.manage.groups.line-4", "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}."), + HELP_GROUPS_5("html.label.help.manage.groups.line-5", "Permission inheritance"), + HELP_GROUPS_6("html.label.help.manage.groups.line-6", "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc."), + HELP_GROUPS_7("html.label.help.manage.groups.line-7", "Access vs Page -permissions"), + HELP_GROUPS_8("html.label.help.manage.groups.line-8", "You need to assign both access and page permissions for users."), + HELP_GROUPS_9("html.label.help.manage.groups.line-9", "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network."), + HELP_GROUPS_10("html.label.help.manage.groups.line-10", "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints."), + HELP_GROUPS_11("html.label.help.manage.groups.line-11", "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}."), + HELP_GROUPS_12("html.label.help.manage.groups.line-12", "Saving changes"), + HELP_GROUPS_13("html.label.help.manage.groups.line-13", "When you add a group or delete a group that action is saved immediately after confirm (no undo)."), + HELP_GROUPS_14("html.label.help.manage.groups.line-14", "When you modify permissions those changes need to be saved by pressing the Save-button"), + HELP_GROUPS_15("html.label.help.manage.groups.line-15", "Documentation can be found from {{link}}"), + HELP_GRAPH_ZOOM("html.label.help.graph.zoom", "You can Zoom in by click + dragging on the graph."), HELP_GRAPH_TITLE("html.label.help.graph.title", "Graph"), HELP_GRAPH_LABEL("html.label.help.graph.labels", "You can hide/show a group by clicking on the label at the bottom."), @@ -372,6 +394,24 @@ public enum HtmlLang implements Lang { Y_AXIS("html.label.yAxis", "Y Axis"), PERCENTAGE("html.label.unit.percentage", "Percentage"), PLAYER_COUNT("html.label.unit.playerCount", "Player Count"), + MANAGE_GROUP_HEADER("html.label.managePage.groupHeader", "Manage Group Permissions"), + MANAGE_ADD_GROUP("html.label.managePage.addGroup.header", "Add group"), + MANAGE_ADD_GROUP_NAME("html.label.managePage.addGroup.name", "Name of the group"), + MANAGE_ADD_GROUP_NAME_INVALID("html.label.managePage.addGroup.invalidName", "Group name can be 100 characters maximum."), + MANAGE_GROUP_PERMISSION_LIST("html.label.managePage.groupPermissions", "Permissions of {{groupName}}"), + MANAGE_SAVE_CHANGES("html.label.managePage.changes.save", "Save"), + MANAGE_DISCARD_CHANGES("html.label.managePage.changes.discard", "Discard Changes"), + MANAGE_UNSAVED_CHANGES("html.label.managePage.changes.unsaved", "Unsaved changes"), + MANAGE_DELETE_GROUP_HEADER("html.label.managePage.deleteGroup.header", "Delete '{{groupName}}'"), + MANAGE_DELETE_GROUP_CONFIRM("html.label.managePage.deleteGroup.confirm", "Confirm & Delete {{groupName}}"), + MANAGE_DELETE_GROUP_MOVE_TO("html.label.managePage.deleteGroup.moveToSelect", "Move remaining users to group"), + MANAGE_DELETE_CONFIRM_DESCRIPTION("html.label.managePage.deleteGroup.confirmDescription", "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!"), + MANAGE_ALERT_GROUP_DELETE_FAIL("html.label.managePage.alert.groupDeleteFail", "Failed to delete group: {{error}}"), + MANAGE_ALERT_GROUP_DELETE_SUCCESS("html.label.managePage.alert.groupDeleteSuccess", "Deleted group '{{groupName}}'"), + MANAGE_ALERT_GROUP_ADD_FAIL("html.label.managePage.alert.groupAddFail", "Failed to add group: {{error}}"), + MANAGE_ALERT_GROUP_ADD_SUCCESS("html.label.managePage.alert.groupAddSuccess", "Added group '{{groupName}}'"), + MANAGE_ALERT_SAVE_FAIL("html.label.managePage.alert.saveFail", "Failed to save changes: {{error}}"), + MANAGE_ALERT_SAVE_SUCCESS("html.label.managePage.alert.saveSuccess", "Changes saved successfully!"), WARNING_NO_GAME_SERVERS("html.description.noGameServers", "Some data requires Plan to be installed on game servers."), WARNING_PERFORMANCE_NO_GAME_SERVERS("html.description.performanceNoGameServers", "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop."), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java index 175c7194f..4c483d499 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java @@ -243,6 +243,11 @@ public abstract class SQLDB extends AbstractDatabase { new BadJoinAddressDataCorrectionPatch(), new AfterBadJoinAddressDataCorrectionPatch(), new CorrectWrongCharacterEncodingPatch(logger, config), + new UpdateWebPermissionsPatch(), + new WebGroupDefaultGroupsPatch(), + new WebGroupAddMissingAdminGroupPatch(), + new LegacyPermissionLevelGroupsPatch(), + new SecurityTableGroupPatch() }; } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/LargeStoreQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/LargeStoreQueries.java index c7563f6d2..b88d4059f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/LargeStoreQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/LargeStoreQueries.java @@ -30,6 +30,7 @@ import com.djrapitops.plan.storage.database.sql.tables.*; import com.djrapitops.plan.storage.database.transactions.ExecBatchStatement; import com.djrapitops.plan.storage.database.transactions.Executable; import org.apache.commons.lang3.StringUtils; +import org.intellij.lang.annotations.Language; import java.sql.Connection; import java.sql.PreparedStatement; @@ -127,7 +128,7 @@ public class LargeStoreQueries { statement.setString(2, user.getLinkedToUUID().toString()); } statement.setString(3, user.getPasswordHash()); - statement.setInt(4, user.getPermissionLevel()); + statement.setString(4, user.getPermissionGroup()); statement.addBatch(); } } @@ -428,4 +429,58 @@ public class LargeStoreQueries { } }; } + + public static Executable storeGroupNames(List groups) { + if (groups == null || groups.isEmpty()) return Executable.empty(); + + return new ExecBatchStatement(WebGroupTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (String group : groups) { + statement.setString(1, group); + statement.addBatch(); + } + } + }; + } + + + public static Executable storePermissions(List permissions) { + if (permissions == null || permissions.isEmpty()) return Executable.empty(); + + return new ExecBatchStatement(WebPermissionTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (String permission : permissions) { + statement.setString(1, permission); + statement.addBatch(); + } + } + }; + } + + public static Executable storeGroupPermissionRelations(Map> groupPermissions) { + if (groupPermissions == null || groupPermissions.isEmpty()) return Executable.empty(); + + @Language("SQL") + String sql = "INSERT INTO " + WebGroupToPermissionTable.TABLE_NAME + " (" + + WebGroupToPermissionTable.GROUP_ID + ',' + WebGroupToPermissionTable.PERMISSION_ID + + ") VALUES ((" + + WebGroupTable.SELECT_GROUP_ID + "),(" + WebPermissionTable.SELECT_PERMISSION_ID + + "))"; + + return new ExecBatchStatement(sql) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (var permissionsOfGroup : groupPermissions.entrySet()) { + String group = permissionsOfGroup.getKey(); + for (String permission : permissionsOfGroup.getValue()) { + statement.setString(1, group); + statement.setString(2, permission); + statement.addBatch(); + } + } + } + }; + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java index 17c087b3a..95f0af4e8 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java @@ -16,20 +16,19 @@ */ package com.djrapitops.plan.storage.database.queries.objects; -import com.djrapitops.plan.delivery.domain.WebUser; import com.djrapitops.plan.delivery.domain.auth.User; import com.djrapitops.plan.storage.database.queries.Query; -import com.djrapitops.plan.storage.database.sql.tables.CookieTable; -import com.djrapitops.plan.storage.database.sql.tables.SecurityTable; -import com.djrapitops.plan.storage.database.sql.tables.UsersTable; +import com.djrapitops.plan.storage.database.queries.QueryAllStatement; +import com.djrapitops.plan.storage.database.sql.building.Sql; +import com.djrapitops.plan.storage.database.sql.tables.*; import com.djrapitops.plan.utilities.dev.Untrusted; +import com.djrapitops.plan.utilities.java.Lists; +import org.intellij.lang.annotations.Language; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; import static com.djrapitops.plan.storage.database.sql.building.Sql.*; @@ -45,45 +44,98 @@ public class WebUserQueries { } public static Query> fetchUser(@Untrusted String username) { - String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME + + String sql = SELECT + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + ',' + + "GROUP_CONCAT(" + WebPermissionTable.PERMISSION + ",',') as user_permissions" + + FROM + SecurityTable.TABLE_NAME + " s" + + INNER_JOIN + WebGroupTable.TABLE_NAME + " g on g." + WebGroupTable.ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gtp on gtp." + WebGroupToPermissionTable.GROUP_ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebPermissionTable.TABLE_NAME + " p on gtp." + WebGroupToPermissionTable.PERMISSION_ID + "=p." + WebPermissionTable.ID + LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID + - WHERE + SecurityTable.USERNAME + "=?" + LIMIT + "1"; - + WHERE + SecurityTable.USERNAME + "=?" + + GROUP_BY + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + + LIMIT + "1"; return db -> db.queryOptional(sql, WebUserQueries::extractUser, username); } - public static Query> fetchUserLinkedTo(String playerName) { - String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME + - LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID + - WHERE + UsersTable.USER_NAME + "=?" + LIMIT + "1"; - return db -> db.queryOptional(sql, WebUserQueries::extractUser, playerName); - } - public static Query> fetchUser(UUID linkedToUUID) { - String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME + + String sql = SELECT + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + ',' + + "GROUP_CONCAT(" + WebPermissionTable.PERMISSION + ",',') as user_permissions" + + FROM + SecurityTable.TABLE_NAME + " s" + + INNER_JOIN + WebGroupTable.TABLE_NAME + " g on g." + WebGroupTable.ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gtp on gtp." + WebGroupToPermissionTable.GROUP_ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebPermissionTable.TABLE_NAME + " p on gtp." + WebGroupToPermissionTable.PERMISSION_ID + "=p." + WebPermissionTable.ID + LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID + - WHERE + SecurityTable.LINKED_TO + "=?" + LIMIT + "1"; + WHERE + SecurityTable.LINKED_TO + "=?" + + GROUP_BY + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + + LIMIT + "1"; return db -> db.queryOptional(sql, WebUserQueries::extractUser, linkedToUUID); } public static Query> fetchAllUsers() { - String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME + - LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID; + String sql = SELECT + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + ',' + + "GROUP_CONCAT(" + WebPermissionTable.PERMISSION + ",',') as user_permissions" + + FROM + SecurityTable.TABLE_NAME + " s" + + INNER_JOIN + WebGroupTable.TABLE_NAME + " g on g." + WebGroupTable.ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gtp on gtp." + WebGroupToPermissionTable.GROUP_ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebPermissionTable.TABLE_NAME + " p on gtp." + WebGroupToPermissionTable.PERMISSION_ID + "=p." + WebPermissionTable.ID + + LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID + + GROUP_BY + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME; return db -> db.queryList(sql, WebUserQueries::extractUser); } - public static Query> matchUsers(String partOfUsername) { - String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME + - LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID + - WHERE + "LOWER(" + SecurityTable.USERNAME + ") LIKE LOWER(?)"; - return db -> db.queryList(sql, WebUserQueries::extractUser, '%' + partOfUsername + '%'); - } - public static Query> fetchActiveCookies() { - String sql = SELECT + '*' + FROM + CookieTable.TABLE_NAME + - INNER_JOIN + SecurityTable.TABLE_NAME + " on " + CookieTable.TABLE_NAME + '.' + CookieTable.WEB_USERNAME + '=' + SecurityTable.TABLE_NAME + '.' + SecurityTable.USERNAME + + String sql = SELECT + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + ',' + + CookieTable.COOKIE + ',' + + "GROUP_CONCAT(" + WebPermissionTable.PERMISSION + ",',') as user_permissions" + + FROM + CookieTable.TABLE_NAME + " c" + + INNER_JOIN + SecurityTable.TABLE_NAME + " s on c." + CookieTable.WEB_USERNAME + "=s." + SecurityTable.USERNAME + + INNER_JOIN + WebGroupTable.TABLE_NAME + " g on g." + WebGroupTable.ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gtp on gtp." + WebGroupToPermissionTable.GROUP_ID + "=s." + SecurityTable.GROUP_ID + + LEFT_JOIN + WebPermissionTable.TABLE_NAME + " p on gtp." + WebGroupToPermissionTable.PERMISSION_ID + "=p." + WebPermissionTable.ID + LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID + - WHERE + CookieTable.EXPIRES + ">?"; + WHERE + CookieTable.EXPIRES + ">?" + + GROUP_BY + + SecurityTable.USERNAME + ',' + + UsersTable.USER_NAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + WebGroupTable.NAME + ',' + + CookieTable.COOKIE; return db -> db.queryMap(sql, (set, byCookie) -> byCookie.put(set.getString(CookieTable.COOKIE), extractUser(set)), System.currentTimeMillis()); @@ -94,13 +146,97 @@ public class WebUserQueries { String linkedTo = set.getString(UsersTable.USER_NAME); UUID linkedToUUID = linkedTo != null ? UUID.fromString(set.getString(SecurityTable.LINKED_TO)) : null; String passwordHash = set.getString(SecurityTable.SALT_PASSWORD_HASH); - int permissionLevel = set.getInt(SecurityTable.PERMISSION_LEVEL); - List permissions = WebUser.getPermissionsForLevel(permissionLevel); - return new User(username, linkedTo != null ? linkedTo : "console", linkedToUUID, passwordHash, permissionLevel, permissions); + String permissionGroup = set.getString(WebGroupTable.NAME); + String userPermissions = set.getString("user_permissions"); + List permissions = userPermissions != null ? Arrays.stream(userPermissions.split(",")) + .filter(permission -> !permission.isEmpty()) + .collect(Collectors.toList()) : List.of(); + return new User(username, linkedTo != null ? linkedTo : "console", linkedToUUID, passwordHash, permissionGroup, new HashSet<>(permissions)); } public static Query> getCookieExpiryTimes() { String sql = SELECT + CookieTable.COOKIE + ',' + CookieTable.EXPIRES + FROM + CookieTable.TABLE_NAME; return db -> db.queryMap(sql, (set, expiryTimes) -> expiryTimes.put(set.getString(CookieTable.COOKIE), set.getLong(CookieTable.EXPIRES))); } + + public static Query> fetchGroupNames() { + String sql = SELECT + WebGroupTable.NAME + FROM + WebGroupTable.TABLE_NAME; + return db -> db.queryList(sql, row -> row.getString(WebGroupTable.NAME)); + } + + public static Query> fetchGroupPermissions(@Untrusted String group) { + String sql = SELECT + WebPermissionTable.PERMISSION + + FROM + WebGroupTable.TABLE_NAME + " g" + + INNER_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gtp ON g." + WebGroupTable.ID + "=gtp." + WebGroupToPermissionTable.GROUP_ID + + INNER_JOIN + WebPermissionTable.TABLE_NAME + " p ON p." + WebPermissionTable.ID + "=gtp." + WebGroupToPermissionTable.PERMISSION_ID + + WHERE + WebGroupTable.NAME + "=?"; + return db -> db.queryList(sql, row -> row.getString(WebPermissionTable.PERMISSION), group); + } + + public static Query> fetchAvailablePermissions() { + String sql = SELECT + WebPermissionTable.PERMISSION + FROM + WebPermissionTable.TABLE_NAME; + return db -> db.queryList(sql, row -> row.getString(WebPermissionTable.PERMISSION)); + } + + public static Query> fetchGroupId(@Untrusted String name) { + return db -> db.queryOptional(WebGroupTable.SELECT_GROUP_ID, row -> row.getInt(WebGroupTable.ID), name); + } + + public static Query> fetchPermissionIds(@Untrusted Collection permissions) { + String sql = SELECT + WebPermissionTable.ID + + FROM + WebPermissionTable.TABLE_NAME + + WHERE + WebPermissionTable.PERMISSION + " IN (" + Sql.nParameters(permissions.size()) + ")"; + return db -> { + if (permissions.isEmpty()) return Collections.emptyList(); + return db.queryList(sql, row -> row.getInt(WebPermissionTable.ID), permissions); + }; + } + + public static Query> fetchAllUsernames() { + return db -> db.queryList(SELECT + SecurityTable.USERNAME + FROM + SecurityTable.TABLE_NAME, row -> row.getString(SecurityTable.USERNAME)); + } + + public static Query> fetchGroupNamesWithPermission(String permission) { + @Language("SQL") + String sql = SELECT + WebGroupTable.NAME + + FROM + WebGroupTable.TABLE_NAME + " g" + + INNER_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gp ON gp." + WebGroupToPermissionTable.GROUP_ID + "=g." + WebGroupTable.ID + + INNER_JOIN + WebPermissionTable.TABLE_NAME + " p ON gp." + WebGroupToPermissionTable.PERMISSION_ID + "=p." + WebPermissionTable.ID + + WHERE + WebPermissionTable.PERMISSION + "=?"; + return db -> db.queryList(sql, row -> row.getString(WebGroupTable.NAME), permission); + } + + public static Query> fetchPermissionId(String permission) { + String sql = SELECT + WebPermissionTable.ID + FROM + WebPermissionTable.TABLE_NAME + WHERE + WebPermissionTable.PERMISSION + "=?"; + return db -> db.queryOptional(sql, row -> row.getInt(WebPermissionTable.ID), permission); + } + + public static Query> fetchGroupIds(List groups) { + String sql = SELECT + WebGroupTable.ID + + FROM + WebGroupTable.TABLE_NAME + + WHERE + WebGroupTable.NAME + " IN (" + Sql.nParameters(groups.size()) + ')'; + return db -> db.queryList(sql, row -> row.getInt(WebGroupTable.ID), groups); + } + + public static Query>> fetchAllGroupPermissions() { + String sql = SELECT + WebGroupTable.NAME + ',' + WebPermissionTable.PERMISSION + + FROM + WebGroupTable.TABLE_NAME + " g" + + INNER_JOIN + WebGroupToPermissionTable.TABLE_NAME + " gtp ON g." + WebGroupTable.ID + "=gtp." + WebGroupToPermissionTable.GROUP_ID + + INNER_JOIN + WebPermissionTable.TABLE_NAME + " p ON p." + WebPermissionTable.ID + "=gtp." + WebGroupToPermissionTable.PERMISSION_ID; + return new QueryAllStatement<>(sql, 100) { + @Override + public Map> processResults(ResultSet set) throws SQLException { + Map> groupMap = new HashMap<>(); + while (set.next()) { + String group = set.getString(WebGroupTable.NAME); + String permission = set.getString(WebPermissionTable.PERMISSION); + + List permissionList = groupMap.computeIfAbsent(group, Lists::create); + + permissionList.add(permission); + } + return groupMap; + } + }; + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/SecurityTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/SecurityTable.java index 3b29d56a9..f7db04d18 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/SecurityTable.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/SecurityTable.java @@ -24,6 +24,7 @@ import com.djrapitops.plan.storage.database.sql.building.Sql; * Table information about 'plan_security' * * @author AuroraLS3 + * @see com.djrapitops.plan.storage.database.transactions.patches.SecurityTableGroupPatch */ public class SecurityTable { @@ -33,13 +34,13 @@ public class SecurityTable { public static final String USERNAME = "username"; public static final String LINKED_TO = "linked_to_uuid"; public static final String SALT_PASSWORD_HASH = "salted_pass_hash"; - public static final String PERMISSION_LEVEL = "permission_level"; + public static final String GROUP_ID = "group_id"; public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" + USERNAME + ',' + LINKED_TO + ',' + SALT_PASSWORD_HASH + ',' + - PERMISSION_LEVEL + ") VALUES (?,?,?,?)"; + GROUP_ID + ") VALUES (?,?,?,(" + WebGroupTable.SELECT_GROUP_ID + "))"; private SecurityTable() { /* Static information class */ @@ -51,7 +52,8 @@ public class SecurityTable { .column(USERNAME, Sql.varchar(100)).notNull().unique() .column(LINKED_TO, Sql.varchar(36)).defaultValue("''") .column(SALT_PASSWORD_HASH, Sql.varchar(100)).notNull().unique() - .column(PERMISSION_LEVEL, Sql.INT).notNull() + .column(GROUP_ID, Sql.INT).notNull() + .foreignKey(GROUP_ID, WebGroupTable.TABLE_NAME, WebGroupTable.ID) .toString(); } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupTable.java new file mode 100644 index 000000000..9bc0225eb --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupTable.java @@ -0,0 +1,51 @@ +/* + * 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.storage.database.sql.tables; + +import com.djrapitops.plan.storage.database.DBType; +import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder; +import com.djrapitops.plan.storage.database.sql.building.Sql; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.*; + +/** + * Represents the plan_web_group table. + * + * @author AuroraLS3 + */ +public class WebGroupTable { + + public static final String TABLE_NAME = "plan_web_group"; + + public static final String ID = "id"; + public static final String NAME = "group_name"; + + public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" + NAME + ") VALUES (?)"; + + public static final String SELECT_GROUP_ID = SELECT + ID + FROM + TABLE_NAME + WHERE + NAME + "=?"; + + private WebGroupTable() { + /* Static information class */ + } + + public static String createTableSQL(DBType dbType) { + return CreateTableBuilder.create(TABLE_NAME, dbType) + .column(ID, Sql.INT).primaryKey() + .column(NAME, Sql.varchar(100)).notNull().unique() + .toString(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupToPermissionTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupToPermissionTable.java new file mode 100644 index 000000000..bd10d3ccf --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebGroupToPermissionTable.java @@ -0,0 +1,51 @@ +/* + * 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.storage.database.sql.tables; + +import com.djrapitops.plan.storage.database.DBType; +import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder; +import com.djrapitops.plan.storage.database.sql.building.Sql; + +/** + * Represents the plan_web_permission table. + * + * @author AuroraLS3 + */ +public class WebGroupToPermissionTable { + + public static final String TABLE_NAME = "plan_web_group_to_permission"; + + public static final String ID = "id"; + public static final String GROUP_ID = "group_id"; + public static final String PERMISSION_ID = "permission_id"; + + public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" + GROUP_ID + ',' + PERMISSION_ID + ") VALUES (?,?)"; + + private WebGroupToPermissionTable() { + /* Static information class */ + } + + public static String createTableSQL(DBType dbType) { + return CreateTableBuilder.create(TABLE_NAME, dbType) + .column(ID, Sql.INT).primaryKey() + .column(GROUP_ID, Sql.INT).notNull() + .column(PERMISSION_ID, Sql.INT).notNull() + .foreignKey(GROUP_ID, WebGroupTable.TABLE_NAME, WebGroupTable.ID) + .foreignKey(PERMISSION_ID, WebPermissionTable.TABLE_NAME, WebPermissionTable.ID) + .toString(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebPermissionTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebPermissionTable.java new file mode 100644 index 000000000..eda690336 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/WebPermissionTable.java @@ -0,0 +1,50 @@ +/* + * 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.storage.database.sql.tables; + +import com.djrapitops.plan.storage.database.DBType; +import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder; +import com.djrapitops.plan.storage.database.sql.building.Sql; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.*; + +/** + * Represents the plan_web_permission table. + * + * @author AuroraLS3 + */ +public class WebPermissionTable { + + public static final String TABLE_NAME = "plan_web_permission"; + + public static final String ID = "id"; + public static final String PERMISSION = "permission"; + + public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" + PERMISSION + ") VALUES (?)"; + public static final String SELECT_PERMISSION_ID = SELECT + ID + FROM + TABLE_NAME + WHERE + PERMISSION + "=?"; + + private WebPermissionTable() { + /* Static information class */ + } + + public static String createTableSQL(DBType dbType) { + return CreateTableBuilder.create(TABLE_NAME, dbType) + .column(ID, Sql.INT).primaryKey() + .column(PERMISSION, Sql.varchar(100)).notNull().unique() + .toString(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/BackupCopyTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/BackupCopyTransaction.java index 80cb1cccd..6b5523858 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/BackupCopyTransaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/BackupCopyTransaction.java @@ -54,6 +54,7 @@ public class BackupCopyTransaction extends RemoveEverythingTransaction { copyCommonUserInformation(); copyWorldNames(); copyTPSData(); + copyWebGroups(); copyPlanWebUsers(); copyGeoInformation(); copyNicknameData(); @@ -62,6 +63,12 @@ public class BackupCopyTransaction extends RemoveEverythingTransaction { copyPingData(); } + private void copyWebGroups() { + copy(LargeStoreQueries::storeGroupNames, WebUserQueries.fetchGroupNames()); + copy(LargeStoreQueries::storePermissions, WebUserQueries.fetchAvailablePermissions()); + copy(LargeStoreQueries::storeGroupPermissionRelations, WebUserQueries.fetchAllGroupPermissions()); + } + private void copy(Function executableCreator, Query dataQuery) { // Creates a new Executable from the queried data of the source database execute(executableCreator.apply(sourceDB.query(dataQuery))); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/DeleteWebGroupTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/DeleteWebGroupTransaction.java new file mode 100644 index 000000000..5c5ad948a --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/DeleteWebGroupTransaction.java @@ -0,0 +1,87 @@ +/* + * 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.storage.database.transactions; + +import com.djrapitops.plan.exceptions.database.DBOpException; +import com.djrapitops.plan.storage.database.sql.tables.SecurityTable; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupTable; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupToPermissionTable; +import com.djrapitops.plan.utilities.dev.Untrusted; +import org.intellij.lang.annotations.Language; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.DELETE_FROM; +import static com.djrapitops.plan.storage.database.sql.building.Sql.WHERE; + +/** + * Removes a web group from the database. + * + * @author AuroraLS3 + */ +public class DeleteWebGroupTransaction extends Transaction { + + private final String name; + private final String moveTo; + + public DeleteWebGroupTransaction(@Untrusted String name, @Untrusted String moveTo) { + this.name = name; + this.moveTo = moveTo; + } + + @Override + protected void performOperations() { + String selectIdSql = WebGroupTable.SELECT_GROUP_ID; + + Integer moveToId = query(db -> db.queryOptional(selectIdSql, row -> row.getInt(WebGroupTable.ID), moveTo)) + .orElseThrow(() -> new DBOpException("Group not found for given name")); + + query(db -> db.queryOptional(selectIdSql, row -> row.getInt(WebGroupTable.ID), name)) + .ifPresent(groupId -> { + @Language("SQL") + String deletePermissionLinks = DELETE_FROM + WebGroupToPermissionTable.TABLE_NAME + WHERE + WebGroupToPermissionTable.GROUP_ID + "=?"; + execute(new ExecStatement(deletePermissionLinks) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setInt(1, groupId); + } + }); + + @Language("SQL") + String moveUsersSql = "UPDATE " + SecurityTable.TABLE_NAME + " SET " + SecurityTable.GROUP_ID + "=?" + + WHERE + SecurityTable.GROUP_ID + "=?"; + execute(new ExecStatement(moveUsersSql) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setInt(1, moveToId); + statement.setInt(2, groupId); + } + }); + + @Language("SQL") + String deleteGroup = DELETE_FROM + WebGroupTable.TABLE_NAME + WHERE + WebGroupTable.ID + "=?"; + execute(new ExecStatement(deleteGroup) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setInt(1, groupId); + } + }); + } + ); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/GrantWebPermissionToGroupsWithPermissionTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/GrantWebPermissionToGroupsWithPermissionTransaction.java new file mode 100644 index 000000000..a03dcf4e8 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/GrantWebPermissionToGroupsWithPermissionTransaction.java @@ -0,0 +1,69 @@ +/* + * 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.storage.database.transactions; + +import com.djrapitops.plan.exceptions.database.DBOpException; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupToPermissionTable; +import org.intellij.lang.annotations.Language; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +/** + * Adds a permission to any group that already has the other given permission. + * + * @author AuroraLS3 + */ +public class GrantWebPermissionToGroupsWithPermissionTransaction extends Transaction { + + private final String permissionToGive; + private final String whenHasPermission; + + public GrantWebPermissionToGroupsWithPermissionTransaction(String permissionToGive, String whenHasPermission) { + this.permissionToGive = permissionToGive; + this.whenHasPermission = whenHasPermission; + } + + @Override + protected void performOperations() { + List groupsWithPermission = query(WebUserQueries.fetchGroupNamesWithPermission(whenHasPermission)); + List groupsWithPermissionGiven = query(WebUserQueries.fetchGroupNamesWithPermission(permissionToGive)); + groupsWithPermission.removeAll(groupsWithPermissionGiven); + + List groupIds = query(WebUserQueries.fetchGroupIds(groupsWithPermission)); + if (groupIds.isEmpty()) return; + Integer permissionId = query(WebUserQueries.fetchPermissionId(permissionToGive)) + .orElseThrow(() -> new DBOpException("Permission called '" + permissionToGive + "' not found in database.")); + + @Language("SQL") + String sql = "INSERT INTO " + WebGroupToPermissionTable.TABLE_NAME + '(' + + WebGroupToPermissionTable.GROUP_ID + ',' + WebGroupToPermissionTable.PERMISSION_ID + + ") VALUES (?, ?)"; + execute(new ExecBatchStatement(sql) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (Integer groupId : groupIds) { + statement.setInt(1, groupId); + statement.setInt(2, permissionId); + statement.addBatch(); + } + } + }); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreMissingWebPermissionsTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreMissingWebPermissionsTransaction.java new file mode 100644 index 000000000..002b6ba6e --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreMissingWebPermissionsTransaction.java @@ -0,0 +1,61 @@ +/* + * 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.storage.database.transactions; + +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.sql.tables.WebPermissionTable; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Adds missing permissions to the permission table. + * + * @author AuroraLS3 + */ +public class StoreMissingWebPermissionsTransaction extends Transaction { + + private final Collection permissions; + + public StoreMissingWebPermissionsTransaction(Collection permissions) { + this.permissions = permissions; + } + + @Override + protected void performOperations() { + List storedPermissions = query(WebUserQueries.fetchAvailablePermissions()); + Set missingPermissions = new HashSet<>(); + for (String permission : permissions) { + if (!storedPermissions.contains(permission)) { + missingPermissions.add(permission); + } + } + execute(new ExecBatchStatement(WebPermissionTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (String permission : missingPermissions) { + statement.setString(1, permission); + statement.addBatch(); + } + } + }); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreWebGroupTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreWebGroupTransaction.java new file mode 100644 index 000000000..d2188bc62 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/StoreWebGroupTransaction.java @@ -0,0 +1,100 @@ +/* + * 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.storage.database.transactions; + +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupTable; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupToPermissionTable; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.DELETE_FROM; +import static com.djrapitops.plan.storage.database.sql.building.Sql.WHERE; + +/** + * Adds or updates a web permission group with specific permissions. + * + * @author AuroraLS3 + */ +public class StoreWebGroupTransaction extends Transaction { + + private final String name; + private final Collection permissions; + + public StoreWebGroupTransaction(String name, Collection permissions) { + this.name = name; + this.permissions = permissions; + } + + @Override + protected void performOperations() { + executeOther(new StoreMissingWebPermissionsTransaction(permissions)); + commitMidTransaction(); + + Optional id = query(WebUserQueries.fetchGroupId(name)); + if (id.isPresent()) { + updateGroup(id.get()); + } else { + insertGroup(); + } + } + + private void insertGroup() { + int id = executeReturningId(new ExecStatement(WebGroupTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, name); + } + }); + updateGroup(id); + } + + private void deletePermissionsOfGroup(int id) { + String sql = DELETE_FROM + WebGroupToPermissionTable.TABLE_NAME + WHERE + WebGroupToPermissionTable.GROUP_ID + "=?"; + execute(new ExecStatement(sql) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setInt(1, id); + } + }); + } + + private void insertPermissionIdsOfGroup(int id, List permissionIds) { + if (permissionIds.isEmpty()) return; + + execute(new ExecBatchStatement(WebGroupToPermissionTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (Integer permissionId : permissionIds) { + statement.setInt(1, id); + statement.setInt(2, permissionId); + statement.addBatch(); + } + } + }); + } + + private void updateGroup(Integer id) { + List permissionIds = query(WebUserQueries.fetchPermissionIds(permissions)); + deletePermissionsOfGroup(id); + insertPermissionIdsOfGroup(id, permissionIds); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java index 91546cbc8..fd2696929 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java @@ -148,6 +148,15 @@ public abstract class Transaction { return rollbackStatusMsg; } + protected void commitMidTransaction() { + try { + connection.commit(); + initializeTransaction(); + } catch (SQLException e) { + manageFailure(e); + } + } + /** * Override this method for conditional execution. *

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 89fd25434..9ce63cf21 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 @@ -46,6 +46,9 @@ public class RemoveEverythingTransaction extends Patch { clearTable(UserInfoTable.TABLE_NAME); clearTable(UsersTable.TABLE_NAME); clearTable(TPSTable.TABLE_NAME); + clearTable(WebGroupToPermissionTable.TABLE_NAME); + clearTable(WebPermissionTable.TABLE_NAME); + clearTable(WebGroupTable.TABLE_NAME); clearTable(SecurityTable.TABLE_NAME); clearTable(ServerTable.TABLE_NAME); clearTable(CookieTable.TABLE_NAME); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/StoreWebUserTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/StoreWebUserTransaction.java index 1dc439539..6ffd0b067 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/StoreWebUserTransaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/commands/StoreWebUserTransaction.java @@ -17,13 +17,19 @@ package com.djrapitops.plan.storage.database.transactions.commands; import com.djrapitops.plan.delivery.domain.auth.User; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; import com.djrapitops.plan.storage.database.sql.tables.SecurityTable; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupTable; import com.djrapitops.plan.storage.database.transactions.ExecStatement; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; import com.djrapitops.plan.storage.database.transactions.Transaction; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; +import java.util.Optional; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.WHERE; /** * Transaction to save a new Plan {@link User} to the database. @@ -40,18 +46,34 @@ public class StoreWebUserTransaction extends Transaction { @Override protected void performOperations() { - execute(new ExecStatement(SecurityTable.INSERT_STATEMENT) { + Optional groupId = query(WebUserQueries.fetchGroupId(user.getPermissionGroup())); + if (groupId.isEmpty()) { + executeOther(new StoreWebGroupTransaction(user.getPermissionGroup(), user.getPermissions())); + } + + String sql = "UPDATE " + SecurityTable.TABLE_NAME + " SET " + SecurityTable.GROUP_ID + "=(" + WebGroupTable.SELECT_GROUP_ID + ')' + + WHERE + SecurityTable.USERNAME + "=?"; + boolean updated = execute(new ExecStatement(sql) { @Override public void prepare(PreparedStatement statement) throws SQLException { - statement.setString(1, user.getUsername()); - if (user.getLinkedToUUID() == null) { - statement.setNull(2, Types.VARCHAR); - } else { - statement.setString(2, user.getLinkedToUUID().toString()); - } - statement.setString(3, user.getPasswordHash()); - statement.setInt(4, user.getPermissionLevel()); + statement.setString(1, user.getPermissionGroup()); + statement.setString(2, user.getUsername()); } }); + if (!updated) { + execute(new ExecStatement(SecurityTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, user.getUsername()); + if (user.getLinkedToUUID() == null) { + statement.setNull(2, Types.VARCHAR); + } else { + statement.setString(2, user.getLinkedToUUID().toString()); + } + statement.setString(3, user.getPasswordHash()); + statement.setString(4, user.getPermissionGroup()); + } + }); + } } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java index b37551843..52f9c7118 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java @@ -45,10 +45,13 @@ public class CreateTablesTransaction extends OperationCriticalTransaction { execute(TPSTable.createTableSQL(dbType)); execute(WorldTable.createTableSQL(dbType)); execute(WorldTimesTable.createTableSQL(dbType)); - execute(SecurityTable.createTableSQL(dbType)); execute(SettingsTable.createTableSQL(dbType)); execute(CookieTable.createTableSQL(dbType)); execute(AccessLogTable.createTableSql(dbType)); + execute(WebGroupTable.createTableSQL(dbType)); + execute(WebPermissionTable.createTableSQL(dbType)); + execute(WebGroupToPermissionTable.createTableSQL(dbType)); + execute(SecurityTable.createTableSQL(dbType)); // DataExtension tables execute(ExtensionIconTable.createTableSQL(dbType)); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/LegacyPermissionLevelGroupsPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/LegacyPermissionLevelGroupsPatch.java new file mode 100644 index 000000000..9f7e5bb36 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/LegacyPermissionLevelGroupsPatch.java @@ -0,0 +1,79 @@ +/* + * 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.storage.database.transactions.patches; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.sql.tables.SecurityTable; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; + +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * Adds permission level based permission groups to group table. + *

+ * + * @author AuroraLS3 + */ +public class LegacyPermissionLevelGroupsPatch extends Patch { + + @Override + public boolean hasBeenApplied() { + return !hasColumn(SecurityTable.TABLE_NAME, "permission_level") + || query(WebUserQueries.fetchGroupId("legacy_level_100")).isPresent(); + } + + @Override + protected void applyPatch() { + executeOther(new StoreWebGroupTransaction("legacy_level_0", Arrays.stream(new WebPermission[]{ + WebPermission.PAGE_NETWORK, + WebPermission.PAGE_SERVER, + WebPermission.ACCESS_QUERY, + WebPermission.ACCESS_PLAYERS, + WebPermission.PAGE_PLAYER, + WebPermission.ACCESS_PLAYER, + WebPermission.ACCESS_SERVER, + WebPermission.ACCESS_NETWORK + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("legacy_level_1", Arrays.stream(new WebPermission[]{ + WebPermission.ACCESS_QUERY, + WebPermission.ACCESS_PLAYERS, + WebPermission.PAGE_PLAYER, + WebPermission.ACCESS_PLAYER + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("legacy_level_2", Arrays.stream(new WebPermission[]{ + WebPermission.PAGE_PLAYER, + WebPermission.ACCESS_PLAYER_SELF + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("legacy_level_100", Collections.emptyList())); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/SecurityTableGroupPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/SecurityTableGroupPatch.java new file mode 100644 index 000000000..466a2c740 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/SecurityTableGroupPatch.java @@ -0,0 +1,80 @@ +/* + * 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.storage.database.transactions.patches; + +import com.djrapitops.plan.exceptions.database.DBOpException; +import com.djrapitops.plan.storage.database.DBType; +import com.djrapitops.plan.storage.database.sql.tables.SecurityTable; +import com.djrapitops.plan.storage.database.sql.tables.WebGroupTable; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.*; + +/** + * Replaces permission_level with group_id to plan_security table. + * + * @author AuroraLS3 + */ +public class SecurityTableGroupPatch extends Patch { + + private final String tempTableName; + private final String tableName; + + public SecurityTableGroupPatch() { + tableName = SecurityTable.TABLE_NAME; + tempTableName = "temp_security"; + } + + @Override + public boolean hasBeenApplied() { + return hasColumn(tableName, SecurityTable.GROUP_ID) + && !hasColumn(tableName, "permission_level") + && !hasTable(tempTableName); // If this table exists the patch has failed to finish. + } + + @Override + protected void applyPatch() { + try { + tempOldTable(); + dropTable(tableName); + execute(SecurityTable.createTableSQL(dbType)); + + execute("INSERT INTO " + tableName + " (" + + SecurityTable.USERNAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + SecurityTable.GROUP_ID + + ") " + SELECT + + SecurityTable.USERNAME + ',' + + SecurityTable.LINKED_TO + ',' + + SecurityTable.SALT_PASSWORD_HASH + ',' + + "(" + SELECT + WebGroupTable.ID + FROM + WebGroupTable.TABLE_NAME + WHERE + WebGroupTable.NAME + "=" + (dbType == DBType.SQLITE ? "'legacy_level_' || permission_level" : "CONCAT('legacy_level_', permission_level)") + ")" + + FROM + tempTableName + ); + + dropTable(tempTableName); + } catch (Exception e) { + throw new DBOpException(SecurityTableGroupPatch.class.getSimpleName() + " failed.", e); + } + } + + + private void tempOldTable() { + if (!hasTable(tempTableName)) { + renameTable(tableName, tempTableName); + } + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java new file mode 100644 index 000000000..6b07c7494 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java @@ -0,0 +1,68 @@ +/* + * 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.storage.database.transactions.patches; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.sql.tables.WebPermissionTable; +import com.djrapitops.plan.storage.database.transactions.ExecBatchStatement; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Adds missing web permissions to the permission table. + * + * @author AuroraLS3 + */ +public class UpdateWebPermissionsPatch extends Patch { + + private List missingPermissions; + + @Override + public boolean hasBeenApplied() { + List defaultPermissions = Arrays.stream(WebPermission.values()) + .map(WebPermission::getPermission) + .collect(Collectors.toList()); + List storedPermissions = query(WebUserQueries.fetchAvailablePermissions()); + missingPermissions = new ArrayList<>(); + for (String permission : defaultPermissions) { + if (!storedPermissions.contains(permission)) { + missingPermissions.add(permission); + } + } + + return missingPermissions.isEmpty(); + } + + @Override + protected void applyPatch() { + execute(new ExecBatchStatement(WebPermissionTable.INSERT_STATEMENT) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + for (String permission : missingPermissions) { + statement.setString(1, permission); + statement.addBatch(); + } + } + }); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java new file mode 100644 index 000000000..02577404d --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java @@ -0,0 +1,50 @@ +/* + * 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.storage.database.transactions.patches; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Adds admin group back to the database if user accidentally deletes it. + * + * @author AuroraLS3 + */ +public class WebGroupAddMissingAdminGroupPatch extends Patch { + + @Override + public boolean hasBeenApplied() { + return !query(WebUserQueries.fetchGroupNamesWithPermission(WebPermission.MANAGE_GROUPS.getPermission())).isEmpty(); + } + + @Override + protected void applyPatch() { + executeOther(new StoreWebGroupTransaction("admin", Arrays.stream(new WebPermission[]{ + WebPermission.PAGE, + WebPermission.ACCESS, + WebPermission.MANAGE_GROUPS, + WebPermission.MANAGE_USERS + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java new file mode 100644 index 000000000..6003383c1 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java @@ -0,0 +1,84 @@ +/* + * 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.storage.database.transactions.patches; + +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; + +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * Adds default groups to plan_web_group table. + * + * @author AuroraLS3 + */ +public class WebGroupDefaultGroupsPatch extends Patch { + + @Override + public boolean hasBeenApplied() { + return query(WebUserQueries.fetchGroupId("no_access")).isPresent(); + } + + @Override + protected void applyPatch() { + executeOther(new StoreWebGroupTransaction("admin", Arrays.stream(new WebPermission[]{ + WebPermission.PAGE, + WebPermission.ACCESS, + WebPermission.MANAGE_GROUPS, + WebPermission.MANAGE_USERS + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("read_all", Arrays.stream(new WebPermission[]{ + WebPermission.PAGE_NETWORK, + WebPermission.PAGE_SERVER, + WebPermission.ACCESS_QUERY, + WebPermission.ACCESS_PLAYERS, + WebPermission.PAGE_PLAYER, + WebPermission.ACCESS_PLAYER, + WebPermission.ACCESS_RAW_PLAYER_DATA, + WebPermission.ACCESS_SERVER, + WebPermission.ACCESS_NETWORK + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("player_analyst", Arrays.stream(new WebPermission[]{ + WebPermission.ACCESS_QUERY, + WebPermission.ACCESS_PLAYERS, + WebPermission.PAGE_PLAYER, + WebPermission.ACCESS_PLAYER, + WebPermission.ACCESS_RAW_PLAYER_DATA + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("player", Arrays.stream(new WebPermission[]{ + WebPermission.PAGE_PLAYER, + WebPermission.ACCESS_PLAYER_SELF, + WebPermission.ACCESS_RAW_PLAYER_DATA + }) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ); + executeOther(new StoreWebGroupTransaction("no_access", Collections.emptyList())); + } +} diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml index e21365283..5844d4df7 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml @@ -23,6 +23,9 @@ command: feature: description: "要禁用的功能名称:${0}" name: "功能" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "导入类型" nameOrUUID: description: "玩家的名称或 UUID" @@ -150,6 +153,9 @@ command: export: description: "手动导出 html 或 json 文件" inDepth: "把数据导出到配置文件中指定的导出位置。" + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "导入数据" inDepth: "执行导入,将数据加载到数据库。" @@ -193,6 +199,9 @@ command: servers: description: "列出数据库中的服务器" inDepth: "列出数据库中所有服务器的ID、名称和UUID。" + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "注销一个 Plan 网页账户" inDepth: "不含参数使用会注销当前绑定的账户,使用用户名作为参数能注销另一个用户。" @@ -226,6 +235,7 @@ command: info: database: " §2当前数据库:§f${0}" proxy: " §2连接至代理:§f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2有可用更新:§f${0}" version: " §2版本:§f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "独立:" description: newPlayerRetention: "这个数值是基于之前的玩家数据预测的。" + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "要获取某些数据,你需要将 Plan 安装在游戏服务器上。" noGeolocations: "需要在配置文件中启用地理位置收集(Accept GeoLite2 EULA)。" noServerOnlinActivity: "没有可显示在线活动的服务器" noServers: "数据库中找不到服务器" noServersLong: '看起来 Plan 没有安装在任何游戏服务器上或者游戏服务器未连接到相同的数据库。 群组网络教程请参见:wiki' noSpongeChunks: "区块数据在 Sponge 服务端不可用" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "这个数值是基于之前的玩家数据预测的" error: 401Unauthorized: "未认证" @@ -257,8 +271,10 @@ html: emptyForm: "未指定用户名与密码" expiredCookie: "用户 Cookie 已过期" generic: "认证时发生错误" + groupNotFound: "Web Permission Group does not exist" loginFailed: "用户名和密码不匹配" noCookie: "用户 cookie 不存在" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "注册失败,请重试(注册代码有效期 15 分钟)" userNotFound: "用户名不存在" authFailed: "认证失败。" @@ -315,9 +331,11 @@ html: deaths: "死亡数" disk: "硬盘空间" diskSpace: "剩余硬盘空间" + docs: "Swagger Docs" downtime: "停机时间" duringLowTps: "持续低 TPS 时间" entities: "实体" + errors: "Plan Error Logs" exported: "数据导出时间" favoriteServer: "最喜爱的服务器" firstSession: "第一次会话" @@ -331,6 +349,8 @@ html: miller: "米勒投影" ortographic: "正射投影" geolocations: "地理位置" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "活跃指数基于过去3周(21天)内的非AFK游戏时间。每周单独考虑。" activityIndexExample1: "如果有人每周玩的时间达到阈值,他们将获得活跃指数约为3。" @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "小时" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "最长会话时间" lowTpsSpikes: "低 TPS 时间" lowTpsSpikes7days: "低 TPS 时间(7天)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "最大可用硬盘空间" medianSessionLength: "会话长度中位数" minFreeDisk: "最小可用硬盘空间" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "非常活跃" weekComparison: "每周对比" weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'" @@ -557,6 +619,89 @@ html: password: "密码" register: "创建一个账户!" username: "用户名" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "报告问题" @@ -648,6 +793,7 @@ html: completion3: "在游戏中使用以下命令完成注册:" completion4: "或使用控制台:" createNewUser: "创建一个新用户" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "检查注册状态失败:" failed: "注册失败:" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml index 922c04cb7..d26162b85 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml @@ -23,6 +23,9 @@ command: feature: description: "Název funkce k vypnutí: ${0}" name: "funkce" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "druh importu" nameOrUUID: description: "Jméno či UUID hráče" @@ -150,6 +153,9 @@ command: export: description: "Export html či json souborů manuálně" inDepth: "Vykoná export k lokaci definované v configu." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Import dat" inDepth: "Vykoná import k načtení dat z databáze." @@ -193,6 +199,9 @@ command: servers: description: "Seznam serverů v databázi" inDepth: "Seznam id, jmen a uuid serverů v databázi." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Odregistrovat uživatele z Plan webu" inDepth: "Použijte bez argumentů k odregistraci sebe, nebo zadejte jméno jiného uživatele." @@ -226,6 +235,7 @@ command: info: database: " §2Aktivní databáze: §f${0}" proxy: " §2Připojen na Proxy: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Dostupná aktualizace: §f${0}" version: " §2Verze: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Unikátní:" description: newPlayerRetention: "Tato hodnota je odhad dle předchozích hráčů." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Některá data vyžadují, aby byla na herní servery nainstalována aplikace Plan." noGeolocations: "V konfiguraci je třeba povolit shromažďování geolokací (Přijměte GeoLite2 EULA)." noServerOnlinActivity: "Žádný server pro který lze ukázat online aktivitu" noServers: "Žádné servery nenalezeny v databázi" noServersLong: 'Vypadá to, že plugin není nainstalován na žádném serveru nebo není propojen s databází. Podívejte se na tutoriál na wiki.' noSpongeChunks: "Chunky nejsou na Sponge dostupné" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Tato hodnota je odhad založený na předchozích hráčích" error: 401Unauthorized: "Neautorizováno" @@ -257,8 +271,10 @@ html: emptyForm: "Uživatel a heslo nespecifikováno" expiredCookie: "Uživatelské cookie expirovalo" generic: "Autentifikace neuspěšná kvůli chybě" + groupNotFound: "Web Permission Group does not exist" loginFailed: "Uživatel nebo heslo nesouhlasí" noCookie: "Cookies uživatele nejsou k dispozici." + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registrace selhala, zkuste to znovu. (Kód expiruje za 15 minut)" userNotFound: "Uživatel neexistuje" authFailed: "Ověření selhalo." @@ -315,9 +331,11 @@ html: deaths: "Smrti" disk: "Místo na disku" diskSpace: "Volné místo na disku" + docs: "Swagger Docs" downtime: "Offline doba" duringLowTps: "Při nízkých TPS:" entities: "Entity" + errors: "Plan Error Logs" exported: "Doba exportu dat" favoriteServer: "Oblíbený server" firstSession: "První relace" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortografie" geolocations: "Geolokace" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Index aktivity vychází z času stráveného hraním mimo AFK za poslední 3 týdny (21 dní). Každý týden se posuzuje zvlášť." activityIndexExample1: "Pokud někdo hraje každý týden tolik, kolik je limit, má index aktivity ~3." @@ -343,6 +363,23 @@ html: labels: "Skupinu můžete skrýt/zobrazit kliknutím na štítek v dolní části." title: "Graf" zoom: "Graf si můžete přiblížit kliknutím a táhnutím." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hodiny" retention: calculationStep1: "Nejprve se data filtrují pomocí možnosti '<>'. Hráči s 'registerDate' mimo časový rozsah jsou ignorováni." @@ -402,6 +439,30 @@ html: longestSession: "Nejdelší relace" lowTpsSpikes: "Nejnižší TPS" lowTpsSpikes7days: "Nízké hodnoty TPS (7 dní)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Max. volného disku" medianSessionLength: "Medián délky relace" minFreeDisk: "Min. volného disku" @@ -537,6 +598,7 @@ html: unit: percentage: "Procento" playerCount: "Počet hráčů" + users: "Manage Users" veryActive: "Velmi aktivní" weekComparison: "Týdenní srovnání" weekdays: "'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota', 'Neděle'" @@ -557,6 +619,89 @@ html: password: "Heslo" register: "Vytvořte si účet!" username: "Uživatelské jméno" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Nahlásit potíže" @@ -648,6 +793,7 @@ html: completion3: "Pro dokončení registrace použijte následující příkaz ve hře:" completion4: "Nebo použijte příkaz v konzoli:" createNewUser: "Vytvořit nového uživatele" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Kontrola registrace neúspěšná: " failed: "Registrace se nezdařila: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml index cca2eda3a..b46cb7e13 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml @@ -23,6 +23,9 @@ command: feature: description: "Name des zu deaktivierenden Features: ${0}" name: "Feature" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "Import-Art" nameOrUUID: description: "Name oder UUID eines Spieler" @@ -150,6 +153,9 @@ command: export: description: "Exportiere JSON oder HTMl Dateien manuell" inDepth: "Führt einen Export zum in der Config bestimmten Exportstandort aus." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Importiere Daten" inDepth: "Führt einen Import durch, um Daten in die Datenbank zu laden." @@ -193,6 +199,9 @@ command: servers: description: "Liste die Server in der Datenbank auf" inDepth: "List ids, names and uuids of servers in the database." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Registrierung eines Benutzers der Plan-Website aufheben" inDepth: "Use without arguments to unregister player linked user, or with username argument to unregister another user." @@ -226,6 +235,7 @@ command: info: database: " §2Aktuelle Datenbank: §f${0}" proxy: " §2Verbunden mit Bungee: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Update verfügbar: §f${0}" version: " §2Version: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Einzigartig:" description: newPlayerRetention: "Dieser Wert ist eine Vorhersage, die auf früheren Spielern basiert." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Some data requires Plan to be installed on game servers." noGeolocations: "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)." noServerOnlinActivity: "Keine Server gefunden, um die Online Aktivität anzuzeigen" noServers: "Keine Server in der Datenbank gefunden" noServersLong: 'It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial.' noSpongeChunks: "Chunks unavailable on Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Dieser Wert ist eine Vorraussage, der sich auf, der auf den Spielern basiert" error: 401Unauthorized: "Unautorisiert" @@ -257,8 +271,10 @@ html: emptyForm: "User und Passwort nicht spezifiziert" expiredCookie: "Benutzer-Cookie ist abgelaufen" generic: "Authentifizierung fehlgeschlagen" + groupNotFound: "Web Permission Group does not exist" loginFailed: "User und Password stimmen nicht überein" noCookie: "Benutzer-Cookie nicht vorhanden" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registration failed, try again (The code expires after 15 minutes)" userNotFound: "User existiert nicht" authFailed: "Authentifizierung fehlgeschlagen." @@ -315,9 +331,11 @@ html: deaths: "Tode" disk: "Festplattenspeicher" diskSpace: "Freier Festplattenspeicher" + docs: "Swagger Docs" downtime: "Downtime" duringLowTps: "Während niedriger TPS-Spitzen:" entities: "Entitäten" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Lieblingsserver" firstSession: "Erste Sitzung" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Geolocations" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Längste Sitzung" lowTpsSpikes: "Low TPS Spitzen" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Max Freier Speicher" medianSessionLength: "Median Session Length" minFreeDisk: "Min Freier Speicher" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Sehr aktiv" weekComparison: "Wochenvergleich" weekdays: "'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'" @@ -557,6 +619,89 @@ html: password: "Password" register: "Create an Account!" username: "Username" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Melde einen Bug" @@ -648,6 +793,7 @@ html: completion3: "Use the following command in game to finish registration:" completion4: "Or using console:" createNewUser: "Create a new user" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Checking registration status failed: " failed: "Registration failed: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml index 921cee3af..9c67aaa2c 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml @@ -23,6 +23,9 @@ command: feature: description: "Name of the feature to disable: ${0}" name: "feature" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "import kind" nameOrUUID: description: "Name or UUID of a player" @@ -150,6 +153,9 @@ command: export: description: "Export html or json files manually" inDepth: "Performs an export to export location defined in the config." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Import data" inDepth: "Performs an import to load data into the database." @@ -193,6 +199,9 @@ command: servers: description: "List servers in Database" inDepth: "List ids, names and uuids of servers in the database." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Unregister a user of Plan website" inDepth: "Use without arguments to unregister player linked user, or with username argument to unregister another user." @@ -226,6 +235,7 @@ command: info: database: " §2Current Database: §f${0}" proxy: " §2Connected to Proxy: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Update Available: §f${0}" version: " §2Version: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Unique:" description: newPlayerRetention: "This value is a prediction based on previous players." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Some data requires Plan to be installed on game servers." noGeolocations: "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)." noServerOnlinActivity: "No server to display online activity for" noServers: "No servers found in the database" noServersLong: 'It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial.' noSpongeChunks: "Chunks unavailable on Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "This value is a prediction based on previous players" error: 401Unauthorized: "Unauthorized" @@ -257,8 +271,10 @@ html: emptyForm: "User and Password not specified" expiredCookie: "User cookie has expired" generic: "Authentication failed due to error" + groupNotFound: "Web Permission Group does not exist" loginFailed: "User and Password did not match" noCookie: "User cookie not present" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registration failed, try again (The code expires after 15 minutes)" userNotFound: "User does not exist" authFailed: "Authentication Failed." @@ -315,9 +331,11 @@ html: deaths: "Deaths" disk: "Disk space" diskSpace: "Free Disk Space" + docs: "Swagger Docs" downtime: "Downtime" duringLowTps: "During Low TPS Spikes:" entities: "Entities" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Favorite Server" firstSession: "First session" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Geolocations" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Longest Session" lowTpsSpikes: "Low TPS Spikes" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Max Free Disk" medianSessionLength: "Median Session Length" minFreeDisk: "Min Free Disk" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Very Active" weekComparison: "Week Comparison" weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'" @@ -557,6 +619,89 @@ html: password: "Password" register: "Create an Account!" username: "Username" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Report Issues" @@ -648,6 +793,7 @@ html: completion3: "Use the following command in game to finish registration:" completion4: "Or using console:" createNewUser: "Create a new user" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Checking registration status failed: " failed: "Registration failed: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml index 093ff2062..57479798f 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml @@ -23,6 +23,9 @@ command: feature: description: "Nombre de la característica a deshabilitar: ${0}" name: "caracteristica" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "importar tipo" nameOrUUID: description: "Nombre o UUID de un jugador" @@ -150,6 +153,9 @@ command: export: description: "Exportar archivos html o json manualmente" inDepth: "Realiza una exportación al directorio de exportación definido en la configuración." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Importar datos" inDepth: "Realiza un importe de los datos hacia la base de datos." @@ -193,6 +199,9 @@ command: servers: description: "Listar los servidores en la base de datos" inDepth: "Lista de ids, nombres y uuids de los servidores en la base de datos." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Quitar el registro de un usuario en la página web de Plan" inDepth: "Usa sin argumentos para eliminar del registro al jugador vinculado, o con un argumento de nombre de usuario para eliminar su registro." @@ -226,6 +235,7 @@ command: info: database: " §2Base de datos actual: §f${0}" proxy: " §2Conectado al Proxy: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Actualización disponible: §f${0}" version: " §2Versión: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Únicos:" description: newPlayerRetention: "Este valor es una predicción basada en jugadores anteriores." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Some data requires Plan to be installed on game servers." noGeolocations: "La recopilación de la Geolocalización necesita ser habilitada en la configuración (Y aceptar el GeoLite2 EULA)." noServerOnlinActivity: "Sin un servidor donde registrar la actividad online." noServers: "Ningun servidor encontrado en la base de datos." noServersLong: 'It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial.' noSpongeChunks: "Chunks no disponibles en Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Este valor es una predicción que viene dado por jugadores anteriores." error: 401Unauthorized: "No autorizado" @@ -257,8 +271,10 @@ html: emptyForm: "Usuario y contraseña no especificados" expiredCookie: "Las cookies del jugador han expirado" generic: "La autenticación ha llevado a error" + groupNotFound: "Web Permission Group does not exist" loginFailed: "El usuario y la contraseña no coincide" noCookie: "Las cookies del jugador no esta presentes" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registro fallido, vuelve a intentar (El código expirara en 15 minutos)" userNotFound: "El usuario no existe" authFailed: "Autenticación fallada." @@ -315,9 +331,11 @@ html: deaths: "Muertes" disk: "Espacio del disco" diskSpace: "Espacio libre en el disco duro" + docs: "Swagger Docs" downtime: "Falta de tiempo" duringLowTps: "Durante picos bajos de TPS:" entities: "Entidades" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Servidor favorito" firstSession: "Primera sesión" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Geolocalizaciones" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Sesión más larga" lowTpsSpikes: "Picos bajos TPS" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Máximo espacio libre en el disco duro" medianSessionLength: "Median Session Length" minFreeDisk: "Mínimo espacio libre en el disco duro" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Muy activo" weekComparison: "Comparación semanal" weekdays: "'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'" @@ -557,6 +619,89 @@ html: password: "Contraseña" register: "Crear una cuenta!" username: "Usuario" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Reportar errores" @@ -648,6 +793,7 @@ html: completion3: "Usa el siguiente comando en el juego para finalizar el registro:" completion4: "O usando la consola:" createNewUser: "Crear un nuevo usuario" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Checking registration status failed: " failed: "Registración fallida: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml index 035c4f849..51061e42b 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml @@ -23,6 +23,9 @@ command: feature: description: "Ominaisuuden nimi joka poistetaan käytöstä: ${0}" name: "ominaisuus" + group: + description: "Käyttöoikeus Ryhmä, huomioi kirjainkoon" + name: "ryhmä" importKind: "tuonnin muoto" nameOrUUID: description: "Pelaajan UUID tai nimi." @@ -100,7 +103,7 @@ command: featureDisabled: "§aSammutettiin '${0}' toistaiseksi, kunnes Plan ladataan uudelleen." noAddress: "§eOsoitetta ei ollut saatavilla - käytetään localhost:ia sen sijasta. Aseta 'Alternative_IP' asetukset." noWebuser: "Sinulla ei ehkä ole Verkkokäyttäjää, käytä /plan register -komentoa" - notifyWebUserRegister: "Rekisteröitiin uusi Verkkokäyttäjä: '${0}' Lupa taso: ${1}" + notifyWebUserRegister: "Rekisteröitiin uusi Verkkokäyttäjä: '${0}' Käyttäjäryhmä: ${1}" pluginDisabled: "§aPlan on nyt poissa päältä. Voit käyttää reload komentoa uudelleenkäynnistykseen." reloadComplete: "§aUudelleenlataus onnistui!" reloadFailed: "§cUudelleenlatauksessa esiintyi ongelmia. Käynnistystä uudelleen suositellaan." @@ -117,7 +120,7 @@ command: search: "> §2${0} Tulosta haulle §f${1}§2:" serverList: "id::nimi::uuid::versio" servers: "> §2Palvelimet" - webUserList: "käyttäjänimi::linkitetty pelaajaan::lupa taso" + webUserList: "käyttäjänimi::linkitetty pelaajaan::käyttäjäryhmä" webUsers: "> §2${0} Verkkokäyttäjät" help: database: @@ -150,6 +153,9 @@ command: export: description: "Vie html tai json tietoja manuaalisesti" inDepth: "Toimittaa viennin asetuksissa olevaan sijaintiin" + groups: + description: "Listaa käyttäjäryhmät." + inDepth: "Listaa saatavilla olevat käyttäjäryhmät joita hallitaan verkkonäkymästä." import: description: "Tuo tietoja" inDepth: "Tuo tietoja tietokantaan" @@ -193,6 +199,9 @@ command: servers: description: "Listaa tietokannassa olevat palvelimet" inDepth: "Listaa id:t, nimet ja uuid:t tietokannassa olevista palvelimista." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Poista Plan sivuston käyttäjä rekisteristä" inDepth: "Käytä ilman argumentteja poistaaksesi nykyiseen pelaajaan linkitetty käyttäjä, tai anna käyttäjänimi joka poistaa" @@ -226,6 +235,7 @@ command: info: database: " §2Nykyinen Tietokanta: §f${0}" proxy: " §2Yhdistetty Proxyyn: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Päivitys saatavilla: §f${0}" version: " §2Versio: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Uniikit:" description: newPlayerRetention: "Tämä arvo on ennuste, joka perustuu edellisiin pelaajiin" + noData24h: "Palvelin ei ole lähettänyt tietoja yli 24 tuntiin." + noData30d: "Palvelin ei ole lähettänyt tietoja yli 30 päivään." + noData7d: "Palvelin ei ole lähettänyt tietoja yli 7 päivään." noGameServers: "Osaa tietoja varten Plan täytyy asentaa peli palvelimille." noGeolocations: "Maa-tietojen keräys täytyy laittaa päälle asetustiedostosta (Hyväksy Geolite2 EULA)" noServerOnlinActivity: "Ei palvelinta jolle näyttää aktiivisuutta" noServers: "Palvelimia ei löytynyt tietokannasta" noServersLong: 'Vaikuttaa että Plan peli-palvelimia ei ole asennettu tai yhdistetty samaan tietokantaan. Katso wikiin lisätietoja varten.' noSpongeChunks: "Alueiden määrää ei voi laskea Sponge palvelimilla" + performanceNoGameServers: "TPS, Entiteetti tai Chunkki tietoja ei kerätä proxy palvelimilta, koska niillä ei ole peli-askel sykliä." predictedNewPlayerRetention: "Tämä arvo on arvattu ennustus edellisten pelaajien perusteella" error: 401Unauthorized: "Todennusta ei suoritettu loppuun." @@ -257,8 +271,10 @@ html: emptyForm: "Käyttäjää ja salasana vaaditaan." expiredCookie: "Käyttäjän kirjautumiseväste vanheni" generic: "Todennus epäonnistui virheen vuoksi" + groupNotFound: "Käyttäjäryhmää ei ole olemassa" loginFailed: "Käyttäjä ja salasana ei täsmää" noCookie: "Käyttäjän kirjautumisevästettä ei annettu" + noPermissionGroup: "Rekisteröityminen epäonnistui, pelaajalla ei ollut mitään 'plan.webgroup.{nimi}' lupaa" registrationFailed: "Rekisteröityminen epäonnistui, yritä uudestaan (Koodi vanhenee 15 minuutin jälkeen)" userNotFound: "Käyttäjää ei ole olemassa" authFailed: "Todennus ei onnistunut." @@ -268,7 +284,7 @@ html: serverNotExported: "Palvelinta ei löytynyt, sen tietoja ei ole välttämättä viety tiedostoon vielä" serverNotSeen: "Palvelinta ei löytynyt" generic: - none: "None" + none: "Ei mitään" label: active: "Aktiivinen" activePlaytime: "Aktiivinen peliaika" @@ -315,9 +331,11 @@ html: deaths: "Kuolemat" disk: "Levytila" diskSpace: "Vapaa Levytila" + docs: "Swagger Docs" downtime: "Poissa päältä" duringLowTps: "Matalan TPS:n aikana:" entities: "Entiteetit" + errors: "Plan Error Logs" exported: "Tietojen vientiaika" favoriteServer: "Lempipalvelin" firstSession: "Ensimmäinen sessio" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortografinen" geolocations: "Sijainnit" + groupPermissions: "Hallitse ryhmiä" + groupUsers: "Hallitse ryhmien käyttäjiä" help: activityIndexBasis: "Aktiivisuus indeksi perustuu ei-AFK peliaikaan viimeiseltä kolmelta viikolta (21 päivää). Jokaista viikkoa katsotaan erikseen" activityIndexExample1: "Jos joku pelaa kynnyksen määrän joka viikko, on aktiivisuus indeksi noin ~3." @@ -343,6 +363,23 @@ html: labels: "Voit piilottaa/näyttää ryhmän klikkaamalla nimeä käyrän alapuolella" title: "Käyrä" zoom: "Voit katsoa tietoja tarkemmin klikkaamalla ja vetämällä käyrän päällä" + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "tuntia" retention: calculationStep1: "Ensin tiedot rajataan käyttämällä '<>' valintaa. Pelaajat joiden 'registerDate' osuu aikarajauksen ulkopuolelle eivät tule mukaan laskuihin" @@ -402,6 +439,30 @@ html: longestSession: "Pisin istunto" lowTpsSpikes: "Matalan TPS:n piikit" lowTpsSpikes7days: "Matalan TPS:n piikit (7 päivää)" + manage: "Hallitse" + managePage: + addGroup: + header: "Lisää ryhmä" + invalidName: "Ryhmän nimi voi olla maksimissaan 100 kirjainta" + name: "Ryhmän nimi" + alert: + groupAddFail: "Ryhmän nimen lisäys epäonnistui: {{error}}" + groupAddSuccess: "Lisättiin ryhmä '{{groupName}}'" + groupDeleteFail: "Ryhmän poisto epäonnistui: {{error}}" + groupDeleteSuccess: "Poistettiin ryhmä '{{groupName}}'" + saveFail: "Muutosten talletus epäonnistui: {{error}}" + saveSuccess: "Muutokset tallennettu!" + changes: + discard: "Hylkää Muutokset" + save: "Tallenna" + unsaved: "Tallentamattomia muutoksia" + deleteGroup: + confirm: "Vahvista & Poista {{groupName}}" + confirmDescription: "Tämä siirtää kaikki '{{groupName}}':n käyttäjät ryhmään '{{moveTo}}'. Tätä ei voi peruuttaa!" + header: "Poista '{{groupName}}'" + moveToSelect: "Siirrä loput käyttäjät ryhmään" + groupHeader: "Hallitse ryhmän oikeuksia" + groupPermissions: "Ryhmän {{groupName}} oikeudet" maxFreeDisk: "Maksimi vapaa levytila" medianSessionLength: "Istuntopituuksien mediaani" minFreeDisk: "Minimi vapaa levytila" @@ -537,6 +598,7 @@ html: unit: percentage: "Prosentti" playerCount: "Pelaajamäärä" + users: "Manage Users" veryActive: "Todella Aktiivinen" weekComparison: "Viikkojen vertaus" weekdays: "'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'" @@ -557,6 +619,89 @@ html: password: "Salasana" register: "Luo käyttäjä!" username: "Käyttäjänimi" + manage: + permission: + description: + access: "Ohjaa pääsyä eri sivuille" + access_docs: "Antaa pääsyn /docs sivulle" + access_errors: "Antaa pääsyn /errors sivulle" + access_network: "Antaa pääsyn /network sivulle" + access_player: "Antaa pääsyn any /player sivuille" + access_player_self: "Antaa pääsyn own /player sivulle" + access_players: "Antaa pääsyn /players sivulle" + access_query: "Antaa pääsyn /query ja kysely tulos sivuille" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Antaa pääsyn all /server sivuille" + manage_groups: "Antaa muuttaa ryhmien oikeuksia & Antaa pääsyn /manage/groups sivulle" + manage_users: "Antaa muuttaa mitkä käyttäjät kuuluvat mihinkin ryhmään" + page: "Ohjaa mitä milläkin sivulla näkyy" + page_network: "Näkee koko verkosto sivun" + page_network_geolocations: "Näkee Geolokaatio osion" + page_network_geolocations_map: "Näkee Geolokaatio kartan" + page_network_geolocations_ping_per_country: "Näkee Viive per Maa -taulun" + page_network_join_addresses: "Näkee Liittymäosoitteet osion" + page_network_join_addresses_graphs: "Näkee Liittymisosoite kaaviot" + page_network_join_addresses_graphs_pie: "Näkee kaavion Viimeisimmistä liittymisosoitteista" + page_network_join_addresses_graphs_time: "Näkee kaavion Liittymisosoitteista ajan yli" + page_network_overview: "Näkee Verkoston katsaus osion" + page_network_overview_graphs: "Näkee Verkoston katsaus kaaviot" + page_network_overview_graphs_day_by_day: "Näkee Päivä Kerrallaan -kaavion" + page_network_overview_graphs_hour_by_hour: "Näkee Tunti Kerrallaan -kaavion" + page_network_overview_graphs_online: "Näkee Pelaajia paikalla -kaavion" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "Näkee Verkoston suorituskyky osion" + page_network_playerbase: "Näkee Pelaajakunnan katsaus osion" + page_network_playerbase_graphs: "Näkee Pelaajakunnan katsaus kaaviot" + page_network_playerbase_overview: "Näkee Pelaajakunnan katsauksen numeroina" + page_network_players: "Näkee Pelaajalista osion" + page_network_plugins: "Näkee Proxy palvelimen Lisäosat osion" + page_network_retention: "Näkee Pelaajien pysyvyys osion" + page_network_server_list: "Näkee listan palvelimista" + page_network_sessions: "Näkee Istunnot osion" + page_network_sessions_list: "Näkee listan istunnoista" + page_network_sessions_overview: "Näkee havaintoja istunnoista" + page_network_sessions_server_pie: "Näkee palvelin piirakkakaavion" + page_network_sessions_world_pie: "Näkee maailma piirakkakaavion" + page_player: "Näkee koko pelaaja sivun" + page_player_overview: "Näkee Pelaajan katsaus osion" + page_player_plugins: "Näkee Lisäosat osiot" + page_player_servers: "Näkee Palvelimet osion" + page_player_sessions: "Näkee Pelaajan Istunnot osion" + page_player_versus: "Näkee PvP & PvE osion" + page_server: "Näkee koko palvelin sivun" + page_server_geolocations: "Näkee Geolokaatio osion" + page_server_geolocations_map: "Näkee Geolokaatio kartan" + page_server_geolocations_ping_per_country: "Näkee Viive per Maa -taulun" + page_server_join_addresses: "Näkee Liittymäosoitteet osion" + page_server_join_addresses_graphs: "Näkee Liittymisosoite kaaviot" + page_server_join_addresses_graphs_pie: "Näkee kaavion Viimeisimmistä liittymisosoitteista" + page_server_join_addresses_graphs_time: "Näkee kaavion Liittymisosoitteista ajan yli" + page_server_online_activity: "Näkee Online Aktiivisuus osion" + page_server_online_activity_graphs: "Näkee Online Aktiivisuus kaaviot" + page_server_online_activity_graphs_calendar: "Näkee Palvelimen Kalenterin" + page_server_online_activity_graphs_day_by_day: "Näkee Päivä Kerrallaan -kaavion" + page_server_online_activity_graphs_hour_by_hour: "Näkee Tunti Kerrallaan -kaavion" + page_server_online_activity_graphs_punchcard: "Näkee Reikäkortti kaavion" + page_server_online_activity_overview: "Näkee Online Aktiivisuuden numeroina" + page_server_overview: "Näkee Palvelimen katsaus osion" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "Näkee Pelaajia paikalla -kaavion" + page_server_performance: "Näkee Suorituskyky osion" + page_server_performance_graphs: "Näkee Suorituskyky kaaviot" + page_server_performance_overview: "Näkee Suorituskyky numerot" + page_server_player_versus: "Näkee PvP & PvE osion" + page_server_player_versus_kill_list: "Näkee pelaajien tappo- ja kuolemalistat" + page_server_player_versus_overview: "Näkee PvP & PvE numerot" + page_server_playerbase: "Näkee Pelaajakunnan katsaus osion" + page_server_playerbase_graphs: "Näkee Pelaajakunnan katsaus kaaviot" + page_server_playerbase_overview: "Näkee Pelaajakunnan katsauksen numeroina" + page_server_players: "Näkee Pelaajalista osion" + page_server_plugins: "Näkee palvelinten Lisäosat osiot" + page_server_retention: "Näkee Pelaajien pysyvyys osion" + page_server_sessions: "Näkee Istunnot osion" + page_server_sessions_list: "Näkee listan istunnoista" + page_server_sessions_overview: "Näkee havaintoja istunnoista" + page_server_sessions_world_pie: "Näkee maailma piirakkakaavion" modal: info: bugs: "Ilmoita ongelmista" @@ -648,6 +793,7 @@ html: completion3: "Käytä seuraavaa komentoa pelissä viimeistelläksesi rekisteröinnin:" completion4: "Tai konsolia:" createNewUser: "Luo uusi käyttäjä" + disabled: "Uusien käyttäjien rekisteröiminen on poistettu käytöstä asetuksista." error: checkFailed: "Rekisteröitymisen tilan tarkistus epäonnistui:" failed: "Rekisteröinti epäonnistui:" @@ -657,7 +803,7 @@ html: login: "Aiempi käyttäjä? Kirjaudu sisään!" passwordTip: "Salasana kannattaa olla yli 8 merkkiä, mutta ei ole rajoituksia." register: "Rekisteröidy" - success: "Registered a new user successfully! You can now login." + success: "Käyttäjä rekisteröitiin onnistuneesti! Voit nyt kirjautua." usernameTip: "Käyttäjänimi voi olla enintään 50 merkkiä." text: clickToExpand: "Klikkaa laajentaaksesi" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml index 4758ab792..1dfde5b91 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml @@ -23,6 +23,9 @@ command: feature: description: "Nom de la fonctionnalité à désactiver : ${0}" name: "fonctionnalité" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "type d'importation" nameOrUUID: description: "Nom ou UUID d'un joueur" @@ -150,6 +153,9 @@ command: export: description: "Exporter les fichiers HTML ou JSON manuellement" inDepth: "Effectue une exportation vers un emplacement défini dans la configuration." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Importer des données" inDepth: "Effectue une importation pour charger des données dans la base de données." @@ -193,6 +199,9 @@ command: servers: description: "Obtenir la liste des serveurs dans la base de données" inDepth: "Liste les ids, noms et uuids des serveurs de la base de données." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Désenregistrer un utilisateur du site de Plan" inDepth: "Utilisez sans argument pour désenregistrer un utilisateur lié à un joueur, ou avec un nom d'utilisateur pour désenregistrer un autre utilisateur." @@ -226,6 +235,7 @@ command: info: database: " §2Base de données actuelle : §f${0}" proxy: " §2Connecté : §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Mise à jour disponible : §f${0}" version: " §2Version : §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Unique :" description: newPlayerRetention: "Cette valeur est une prédiction basée sur les joueurs précédents." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Certaines données nécessitent l'installation de Plan sur les serveurs de jeu." noGeolocations: "La collecte de la géolocalisation doit être activée dans la configuration (Accepter l'EULA de GeoLite2)." noServerOnlinActivity: "Aucun serveur pour afficher l'activité en ligne." noServers: "Il n'y a pas de serveur dans la base de données." noServersLong: 'Il semblerait que Plan ne soit installé sur aucun des serveurs de jeu ou qu'il ne soit pas connecté à la même base de données. Voir wiki pour un tutoriel sur la mise en place d'un Réseau.' noSpongeChunks: "Chunks indisponibles sur Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Cette valeur est une prédiction basée sur les anciennes données du joueur." error: 401Unauthorized: "Non autorisé." @@ -257,8 +271,10 @@ html: emptyForm: "Utilisateur et mot de passe non spécifiés" expiredCookie: "Le cookie de l'utilisateur a expiré" generic: "Authentification échouée en raison d'une erreur" + groupNotFound: "Web Permission Group does not exist" loginFailed: "L'utilisateur et le mot de passe ne correspondent pas" noCookie: "Cookie de l'utilisateur non présent" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Enregistrement échoué, veuillez réessayer (Le code expire au bout de 15 minutes)" userNotFound: "Cet utilisateur n'existe pas" authFailed: "Authentification échouée." @@ -315,9 +331,11 @@ html: deaths: "Morts" disk: "Espace Disque" diskSpace: "Espace Disque disponible" + docs: "Swagger Docs" downtime: "Temps Hors-Ligne" duringLowTps: "Pendant les pics de TPS bas :" entities: "Entités" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Serveur Favori" firstSession: "Première session" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Géolocalisation" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Session la plus Longue" lowTpsSpikes: "Pics de TPS bas" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Espace Disque MAX disponible" medianSessionLength: "Median Session Length" minFreeDisk: "Espace Disque MIN disponible" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Très Actif" weekComparison: "Comparaison Hebdomadaire" weekdays: "'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'" @@ -557,6 +619,89 @@ html: password: "Mot de Passe" register: "Créer un compte !" username: "Nom d'Utilisateur" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Rapport de bugs" @@ -648,6 +793,7 @@ html: completion3: "Utilisez la commande suivante en jeu pour terminer l'enregistrement :" completion4: "Ou en utilisant la console :" createNewUser: "Créer un nouvel utilisateur" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "La vérification de l'état de l'enregistrement a échoué : " failed: "Enregistrement échoué : " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml index d668b8519..24b4ea792 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml @@ -23,6 +23,9 @@ command: feature: description: "Name of the feature to disable: ${0}" name: "feature" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "import kind" nameOrUUID: description: "Name or UUID of a player" @@ -150,6 +153,9 @@ command: export: description: "Export html or json files manually" inDepth: "Performs an export to export location defined in the config." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Import data" inDepth: "Performs an import to load data into the database." @@ -193,6 +199,9 @@ command: servers: description: "Elenca i server nel Database" inDepth: "List ids, names and uuids of servers in the database." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Unregister a user of Plan website" inDepth: "Use without arguments to unregister player linked user, or with username argument to unregister another user." @@ -226,6 +235,7 @@ command: info: database: " §2Database corrente: §f${0}" proxy: " §2Connesso al Proxy: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Aggiornamento Disponibile: §f${0}" version: " §2Versione: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Unico:" description: newPlayerRetention: "This value is a prediction based on previous players." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Some data requires Plan to be installed on game servers." noGeolocations: "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)." noServerOnlinActivity: "Nessun server con cui mostare attività" noServers: "Nessun server trovato in questo database" noServersLong: 'It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial.' noSpongeChunks: "Chunks unavailable on Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Questo valore è una previsione basata sui giocatori precedenti" error: 401Unauthorized: "Non Autorizzato" @@ -257,8 +271,10 @@ html: emptyForm: "Utente e Password non specificati" expiredCookie: "User cookie has expired" generic: "Autenticazione fallita a causa di un errore" + groupNotFound: "Web Permission Group does not exist" loginFailed: "Utente e Password non corrispondono" noCookie: "User cookie not present" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registration failed, try again (The code expires after 15 minutes)" userNotFound: "Utente non esistente" authFailed: "Autenticazione Fallita." @@ -315,9 +331,11 @@ html: deaths: "Morti" disk: "Spazio sul Disco" diskSpace: "Spazio Disco Disponibile" + docs: "Swagger Docs" downtime: "Downtime" duringLowTps: "Durante Spicchi TPS bassi:" entities: "Entità" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Server Preferito" firstSession: "Prima sessione" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Geolocalizazione" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Sessione più lunga" lowTpsSpikes: "Spicchi TPS bassi" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Spazio Disco libero Max" medianSessionLength: "Median Session Length" minFreeDisk: "Spazio Disco libero Min" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Molto Attivo" weekComparison: "Confronto settimanale" weekdays: "'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'" @@ -557,6 +619,89 @@ html: password: "Password" register: "Create an Account!" username: "Username" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Segnala Problemi" @@ -648,6 +793,7 @@ html: completion3: "Use the following command in game to finish registration:" completion4: "Or using console:" createNewUser: "Create a new user" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Checking registration status failed: " failed: "Registration failed: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml index c83d6d05a..d5c2e120d 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml @@ -23,6 +23,9 @@ command: feature: description: "「${0}」を無効化する機能の名前" name: "機能" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "インポート系" nameOrUUID: description: "プレイヤーの名前もしくはUUID" @@ -150,6 +153,9 @@ command: export: description: "手動でHTMLかJsonファイルにエクスポートします" inDepth: "コンフィグで指定した出力先にデータをエクスポートします" + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "データをインポートします" inDepth: "データベースにデータをロードするためのインポートを行います" @@ -193,6 +199,9 @@ command: servers: description: "データベース内のBukkit/Spigotサーバー一覧を表示します" inDepth: "データベース内に存在する全サーバーのID、名前、UUIDを表示します" + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "ウェブページからユーザーを未登録にします" inDepth: "別のユーザーの登録を解除します。引数が未指定の場合、ユーザーにリンクされたのプレイヤーの登録を解除します。" @@ -262,8 +271,10 @@ html: emptyForm: "ユーザーとパスワードが入力されてません" expiredCookie: "ユーザーのクッキーの有効期限切れです" generic: "エラーが発生したため認証に失敗しました" + groupNotFound: "Web Permission Group does not exist" loginFailed: "入力されたユーザー名とパスワードが間違っています" noCookie: "ユーザーのクッキーが存在しません" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "登録に失敗しました。もう一度お試しください (コードの有効期限は 15 分後に切れます)" userNotFound: "入力されたユーザーは存在しません" authFailed: "認証に失敗しました" @@ -320,9 +331,11 @@ html: deaths: "死亡回数" disk: "ドライブの容量" diskSpace: "ドライブの空き容量" + docs: "Swagger Docs" downtime: "ダウンタイム" duringLowTps: "TPSの低下までの時間:" entities: "エンティティ数" + errors: "Plan Error Logs" exported: "データエクスポート時間" favoriteServer: "お気に入りのサーバー" firstSession: "初参加" @@ -336,6 +349,8 @@ html: miller: "ミラー図法" ortographic: "正射図法" geolocations: "地域" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "アクティビティ インデックスは、過去 3 週間 (21 日間) の非 AFK プレイ時間に基づいています。各週は個別に考慮されます" activityIndexExample1: "誰かが毎週しきい値に達するほどプレイした場合、その人にはアクティビティインデックス~3が与えられます" @@ -348,6 +363,23 @@ html: labels: "下部のラベルをクリックすることで、グループの表示/非表示を切り替えられます" title: "グラフ" zoom: "グラフをクリック+ドラッグで拡大できます" + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "時間" retention: calculationStep1: "まず、 '<>' を使ってデータをフィルタリングします。範囲外の登録日を持つプレイヤーは無視されます" @@ -407,6 +439,30 @@ html: longestSession: "最長接続時間" lowTpsSpikes: "TPSの低下値" lowTpsSpikes7days: "TPSの低下値(7日)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "ディスクの最大空き容量" medianSessionLength: "セッションの長さの中央値" minFreeDisk: "ディスクの最低空き容量" @@ -542,6 +598,7 @@ html: unit: percentage: "パーセンテージ" playerCount: "プレイヤー数" + users: "Manage Users" veryActive: "とてもログインしている" weekComparison: "直近1週間での比較" weekdays: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'" @@ -562,6 +619,89 @@ html: password: "パスワード" register: "アカウントを作ろう!" username: "ユーザー名" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "バグ報告" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml index 56a13d0d8..14aaeb5a4 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml @@ -23,6 +23,9 @@ command: feature: description: "비활성화할 기능의 이름: ${0}" name: "기능" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "로드 종류" nameOrUUID: description: "Name or UUID of a player" @@ -150,6 +153,9 @@ command: export: description: "HTML 또는 JSON 형식 파일로 내보냅니다." inDepth: "Performs an export to export location defined in the config." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "저장된 데이터를 불러옵니다." inDepth: "Performs an import to load data into the database." @@ -193,6 +199,9 @@ command: servers: description: "서버 목록 열람하기" inDepth: "List ids, names and uuids of servers in the database." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Plan 페이지 계정 탈퇴합니다." inDepth: "Use without arguments to unregister player linked user, or with username argument to unregister another user." @@ -226,6 +235,7 @@ command: info: database: " §2현재 데이터베이스: §f${0}" proxy: " §2프록시에 연결됨: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2최신 버전: §f${0}" version: " §2버전: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Unique:" description: newPlayerRetention: "This value is a prediction based on previous players." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Some data requires Plan to be installed on game servers." noGeolocations: "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)." noServerOnlinActivity: "온라인 활동을 표시 할 서버가 없습니다." noServers: "데이터베이스에 서버가 없습니다." noServersLong: 'It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial.' noSpongeChunks: "Chunks unavailable on Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "이 값은 기존 플레이어를 기반으로 한 예측입니다." error: 401Unauthorized: "Unauthorized 401" @@ -257,8 +271,10 @@ html: emptyForm: "사용자 및 암호가 지정되지 않았습니다." expiredCookie: "사용자 쿠키가 만료되었습니다." generic: "오류로 인해 인증에 실패했습니다." + groupNotFound: "Web Permission Group does not exist" loginFailed: "사용자와 비밀번호가 일치하지 않습니다" noCookie: "사용자 쿠키가 없습니다." + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "등록 실패, 다시 시도 (코드는 15분 후에 만료 됨)" userNotFound: "사용자가 존재하지 않습니다" authFailed: "Authentication Failed. 401" @@ -315,9 +331,11 @@ html: deaths: "죽은 횟수" disk: "디스크 공간" diskSpace: "여유 디스크 공간" + docs: "Swagger Docs" downtime: "다운타임" duringLowTps: "낮은 TPS 스파이크 동안:" entities: "엔티티" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "즐겨찾는 서버" firstSession: "첫 번째 세션" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "지리적 위치" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "가장 긴 접속 시간" lowTpsSpikes: "낮은 TPS 스파이크" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "최대 여유 디스크용량" medianSessionLength: "Median Session Length" minFreeDisk: "최소 여유 디스크용량" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "매우 활성화된" weekComparison: "주 비교" weekdays: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'" @@ -557,6 +619,89 @@ html: password: "Password" register: "Create an Account!" username: "Username" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "문제 보고" @@ -648,6 +793,7 @@ html: completion3: "Use the following command in game to finish registration:" completion4: "Or using console:" createNewUser: "Create a new user" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Checking registration status failed: " failed: "Registration failed: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml index 0305382e6..19101b3bf 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml @@ -23,6 +23,9 @@ command: feature: description: "Naam van de functie die moet worden uitgeschakeld: ${0}" name: "feature" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "soort invoer" nameOrUUID: description: "Naam of UUID van een speler" @@ -150,6 +153,9 @@ command: export: description: "Exporteer html- of json-bestanden handmatig" inDepth: "Voer een export uit naar de exportlocatie gedefinieerd in de configuratie." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Gegevens importeren" inDepth: "Voer een import uit om gegevens naar de database te laden." @@ -193,6 +199,9 @@ command: servers: description: "Servers in database weergeven" inDepth: "Geef een lijst met id's, namen en uuids van servers weer uit de database." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Een gebruiker van de Plan-website uitschrijven" inDepth: "Gebruik zonder argumenten om de aan een speler gekoppelde gebruiker uit te schrijven, of met gebruikersnaamargument om een andere gebruiker uit te schrijven." @@ -226,6 +235,7 @@ command: info: database: " §2Huidige database: §f${0}" proxy: " §2Verbonden met proxy: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Update Beschikbaar: §f${0}" version: " §2Versie: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Uniek:" description: newPlayerRetention: "Deze waarde is een voorspelling op basis van eerdere spelers." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Voor sommige gegevens moet Plan op gameservers zijn geïnstalleerd." noGeolocations: "Het verzamelen van geolocatie moet worden ingeschakeld in de configuratie (Accept GeoLite2 EULA)." noServerOnlinActivity: "Er is geen server om online activiteit voor weer te geven" noServers: "Er zijn geen servers gevonden in de database" noServersLong: 'Het lijkt erop dat Plan op geen enkele gameserver is geïnstalleerd of niet is verbonden met dezelfde database. Zie de wiki voor een netwerkhandleiding.' noSpongeChunks: "Chunks niet beschikbaar op Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Deze waarde is een voorspelling op basis van eerdere spelers" error: 401Unauthorized: "Onbevoegd" @@ -257,8 +271,10 @@ html: emptyForm: "Gebruiker en wachtwoord niet gespecificeerd" expiredCookie: "Gebruikerscookie is verlopen" generic: "Authenticatie mislukt vanwege fout" + groupNotFound: "Web Permission Group does not exist" loginFailed: "Gebruiker en wachtwoord kwamen niet overeen" noCookie: "Gebruikerscookie niet aanwezig" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registratie mislukt, probeer het opnieuw (de code verloopt na 15 minuten)" userNotFound: "Gebruiker bestaat niet" authFailed: "Authenticatie mislukt." @@ -315,9 +331,11 @@ html: deaths: "Sterfgevallen" disk: "Schijfruimte" diskSpace: "Vrije schijfruimte" + docs: "Swagger Docs" downtime: "Uitvaltijd" duringLowTps: "Tijdens lage TPS-pieken:" entities: "Entiteiten" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Favoeriete Server" firstSession: "Eerste sessie" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Geolocaties" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Langste sessie" lowTpsSpikes: "Lage TPS-pieken" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Max Vrije schijfruimte" medianSessionLength: "Median Session Length" minFreeDisk: "Min Vrije schijfruimte" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Heel Actief" weekComparison: "Weekvergelijking" weekdays: "'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'" @@ -557,6 +619,89 @@ html: password: "Wachtwoord" register: "Maak een account!" username: "Gebruikersnaam" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Problemen melden" @@ -648,6 +793,7 @@ html: completion3: "Gebruik de volgende opdracht in het spel om de registratie te voltooien::" completion4: "Of via console:" createNewUser: "Nieuwe gebruiker aanmaken" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Het controleren van de registratiestatus is mislukt: " failed: "Registratie mislukt: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml index d1c3bb53f..2e9f64df0 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml @@ -23,6 +23,9 @@ command: feature: description: "Name of the feature to disable: ${0}" name: "feature" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "import kind" nameOrUUID: description: "Name or UUID of a player" @@ -150,6 +153,9 @@ command: export: description: "Export html or json files manually" inDepth: "Performs an export to export location defined in the config." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Import data" inDepth: "Performs an import to load data into the database." @@ -193,6 +199,9 @@ command: servers: description: "Listar servidores do banco de dados" inDepth: "List ids, names and uuids of servers in the database." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Unregister a user of Plan website" inDepth: "Use without arguments to unregister player linked user, or with username argument to unregister another user." @@ -226,6 +235,7 @@ command: info: database: " §2Banco de dados atual: §f${0}" proxy: " §2Conectados ao Bungee: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Atualização Disponível: §f${0}" version: " §2Versão: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Únicos:" description: newPlayerRetention: "This value is a prediction based on previous players." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Some data requires Plan to be installed on game servers." noGeolocations: "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)." noServerOnlinActivity: "No server to display online activity for" noServers: "No servers found in the database" noServersLong: 'It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial.' noSpongeChunks: "Chunks unavailable on Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "This value is a prediction based on previous players" error: 401Unauthorized: "Acesso não autorizado" @@ -257,8 +271,10 @@ html: emptyForm: "Usuário e Senha não específicado" expiredCookie: "User cookie has expired" generic: "Falha ao autenticar" + groupNotFound: "Web Permission Group does not exist" loginFailed: "Usuário e Senha não coincidem" noCookie: "User cookie not present" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Registration failed, try again (The code expires after 15 minutes)" userNotFound: "Usuário não existe" authFailed: "Falha na Autenticação." @@ -315,9 +331,11 @@ html: deaths: "Mortes" disk: "Espaço de disco" diskSpace: "Espaço de Disco Livre" + docs: "Swagger Docs" downtime: "Downtime" duringLowTps: "During Low TPS Spikes:" entities: "Entidades" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Servidor Favorito" firstSession: "First session" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Geolocalizações" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Longest Session" lowTpsSpikes: "Low TPS Spikes" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Max Free Disk" medianSessionLength: "Median Session Length" minFreeDisk: "Min Free Disk" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Muito Ativo" weekComparison: "Week Comparison" weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'" @@ -557,6 +619,89 @@ html: password: "Password" register: "Create an Account!" username: "Username" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Report Issues" @@ -648,6 +793,7 @@ html: completion3: "Use the following command in game to finish registration:" completion4: "Or using console:" createNewUser: "Create a new user" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Checking registration status failed: " failed: "Registration failed: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml index 032b655ea..8f4e037cb 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml @@ -23,6 +23,9 @@ command: feature: description: "Тип датабазы для отключения: ${0}" name: "функция" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "вид импорта" nameOrUUID: description: "Ник или UUID игрока" @@ -150,6 +153,9 @@ command: export: description: "Экспортировать HTML или JSON файлы самостоятельно" inDepth: "Выполняет экспорт в место экспорта, указанное в конфигурации." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Импортировать дату" inDepth: "Выполняет импорт для загрузки данных в дата базу." @@ -193,6 +199,9 @@ command: servers: description: "Список серверов в базе данных" inDepth: "Список айди, ников и UUID серверов в базе данных." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Удалить веб-пользователя" inDepth: "Используйте без аргументов, чтобы отменить регистрацию пользователя." @@ -226,6 +235,7 @@ command: info: database: " §2Текущая база данных: §f${0}" proxy: " §2Подключен к прокси: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Доступно обновление: §f${0}" version: " §2Версия: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Уникальных:" description: newPlayerRetention: "Это значение является прогнозом, основанным на предыдущих игроках.." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Для некоторых данных требуется установка Plan на игровые сервера." noGeolocations: "Сбор геолокации должен быть включен в конфигурации (ПРОЧИТАЙТЕ и примите GeoLite2 EULA)." noServerOnlinActivity: "Нет сервера для отображения онлайн активности" noServers: "В базе данных не найдено ни одного сервера" noServersLong: 'Похоже, что Plan не установлен ни на одном игровом сервере или не подключен к той же базе данных. Смотрите вики для помощи.' noSpongeChunks: "Чанки не доступны на Sponge" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Это значение является прогнозом на основе предыдущих игроков" error: 401Unauthorized: "Не авторизован" @@ -257,8 +271,10 @@ html: emptyForm: "Имя пользователя и пароль не указаны" expiredCookie: "Срок действия файла cookie пользователя истек" generic: "Ошибка при авторизации" + groupNotFound: "Web Permission Group does not exist" loginFailed: "Пользователь и пароль не совпадают" noCookie: "Пользовательский файл cookie отсутствует" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Регистрация не удалась, попробуйте ещё раз (Код перестанет действовать через 15 минут)" userNotFound: "Пользователь не существует" authFailed: "Ошибка аутентификации." @@ -315,9 +331,11 @@ html: deaths: "Смерти" disk: "Дисковое пространство" diskSpace: "Свободное дисковое пространство" + docs: "Swagger Docs" downtime: "Время простоя" duringLowTps: "Во время низкого TPS:" entities: "Объекты" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Любимый сервер" firstSession: "Первая сессия" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Геолокация" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "Самая длинная сессия" lowTpsSpikes: "Низкий TPS" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Макс. свободный диск" medianSessionLength: "Средняя продолжительность сеанса" minFreeDisk: "Мин. свободный диск" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Очень активный" weekComparison: "Сравнение за неделю" weekdays: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'" @@ -557,6 +619,89 @@ html: password: "Пароль" register: "Создайте аккаунт!" username: "Имя пользователя" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Сообщить о проблемах" @@ -648,6 +793,7 @@ html: completion3: "Используйте следующую команду для окончания регистрации:" completion4: "Или используйте консоль:" createNewUser: "Создаём нового пользователя" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Не удалось проверить статус регистрации: " failed: "Регистрация не удалась: " diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml index 5b451a7f7..7f6e8f809 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml @@ -23,6 +23,9 @@ command: feature: description: "Devre dışı bırakılacak özelliğin adı: ${0}" name: "özellik" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "türü içe aktar" nameOrUUID: description: "Bir oyuncunun adı veya UUID'si" @@ -150,6 +153,9 @@ command: export: description: "Html veya json dosyalarını manuel olarak dışa aktarın" inDepth: "Yapılandırmada tanımlanan dışa aktarma için bir dışa aktarma gerçekleştirir." + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "Verileri içe aktar" inDepth: "Veritabanına veri yüklemek için bir içe aktarma gerçekleştirir." @@ -193,6 +199,9 @@ command: servers: description: "Sunucun tüm veritabanını listeler" inDepth: "Veritabanındaki sunucuların kimliklerini, adlarını ve kullanıcılarını listeleyin." + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "Plan web sitesinin bir kullanıcısının kaydını iptal edin" inDepth: "Oyuncu bağlantılı kullanıcının kaydını silmek için bağımsız değişken olmadan veya başka bir kullanıcının kaydını silmek için kullanıcı adı bağımsız değişkeniyle kullanın." @@ -226,6 +235,7 @@ command: info: database: " §2Mevcut veritabanı: §f${0}" proxy: " §2Bungee ye bağlan: §f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2Güncelleme mevcut: §f${0}" version: " §2Versiyon: §f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "Benzersiz:" description: newPlayerRetention: "Bu değer, önceki oyunculara dayalı bir tahmindir." + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "Bazı veriler, Plan'ın oyun sunucularına yüklenmesini gerektirir." noGeolocations: "Konum belirleme toplama yapılandırmada etkinleştirilmelidir (GeoLite2 EULA'yı Kabul Et)." noServerOnlinActivity: "Çevrimiçi etkinliği gösterecek sunucu yok" noServers: "Veritabanında sunucu bulunamadı" noServersLong: 'Plan'ın herhangi bir oyun sunucusuna yüklenmediği veya aynı veritabanına bağlı olmadığı anlaşılıyor. Ağ eğitimi için wiki 'ye bakın.' noSpongeChunks: "Chunklar Spongeda mevcut değil" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "Bu değer, önceki oyunculara dayalı bir tahmindir" error: 401Unauthorized: "Yetkisiz" @@ -257,8 +271,10 @@ html: emptyForm: "Kullanıcı ve Şifre belirtilmedi" expiredCookie: "Kullanıcı çerezinin süresi doldu" generic: "Kimlik doğrulama hata nedeniyle başarısız oldu" + groupNotFound: "Web Permission Group does not exist" loginFailed: "Kullanıcı adı ve şifre uyuşmuyor" noCookie: "Kullanıcı çerezi mevcut değil" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "Kayıt başarısız oldu, tekrar deneyin (Kodun süresi 15 dakika sonra dolar)" userNotFound: "Böyle Bir Kullanıcı Yok" authFailed: "Kimlik doğrulama başarısız oldu." @@ -315,9 +331,11 @@ html: deaths: "Ölümler" disk: "Disk Alanı" diskSpace: "Boş Disk Alanı" + docs: "Swagger Docs" downtime: "Arıza Süresi" duringLowTps: "Düşük TPS Artışları Sırasında:" entities: "Varlıklar" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "Favori Sunucu" firstSession: "İlk seans" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "Coğrafi Konumlar" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "En Uzun Oturum" lowTpsSpikes: "Low TPS Spikes" lowTpsSpikes7days: "Low TPS Spikes (7 days)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "Maksimum Boş Disk" medianSessionLength: "Median Session Length" minFreeDisk: "Minimum Boş Disk" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "Çok Aktif" weekComparison: "Hafta Karşılaştırması" weekdays: "'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'" @@ -557,6 +619,89 @@ html: password: "Parola" register: "Hesap oluştur!" username: "Kullanıcı adı" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "Sorunları Bildir" @@ -648,6 +793,7 @@ html: completion3: "Kaydı bitirmek için oyunda aşağıdaki komutu kullanın:" completion4: "Veya konsol kullanarak:" createNewUser: "Yeni bir kullanıcı oluşturun" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "Kayıt durumu kontrol edilemedi:" failed: "Kayıt başarısız:" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml index 224d6a006..3631416d0 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml @@ -23,6 +23,9 @@ command: feature: description: "要關閉的功能名稱:${0}" name: "功能" + group: + description: "Web Permission Group, case sensitive." + name: "group" importKind: "匯入類型" nameOrUUID: description: "玩家的名稱或 UUID" @@ -150,6 +153,9 @@ command: export: description: "手動匯出 html 或 json 檔案" inDepth: "把資料匯出到設定檔案中指定的匯出位置。" + groups: + description: "List web permission groups." + inDepth: "List available web permission groups that are managed on the web interface." import: description: "匯入資料" inDepth: "執行匯入,將資料載入到資料庫。" @@ -193,6 +199,9 @@ command: servers: description: "列出資料庫中的伺服器" inDepth: "列出資料庫中所有伺服器的ID、名稱和UUID。" + setgroup: + description: "Change users web permission group." + inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups." unregister: description: "註冊一個 Plan 網頁帳戶" inDepth: "不含參數使用會註冊目前綁定的帳戶,使用使用者名稱作為參數能註冊另一個使用者。" @@ -226,6 +235,7 @@ command: info: database: " §2目前資料庫:§f${0}" proxy: " §2連接至代理:§f${0}" + serverUUID: " §2Server UUID: §f${0}" update: " §2有可用更新:§f${0}" version: " §2版本:§f${0}" generic: @@ -238,12 +248,16 @@ html: unique: "獨立:" description: newPlayerRetention: "這個數值是基於之前的玩家資料預測的。" + noData24h: "Server has not sent data for over 24 hours." + noData30d: "Server has not sent data for over 30 days." + noData7d: "Server has not sent data for over 7 days." noGameServers: "要獲得某些資料,你需要將 Plan 安裝在遊戲伺服器上。" noGeolocations: "需要在設定檔案中啟用地理位置收集(Accept GeoLite2 EULA)。" noServerOnlinActivity: "沒有可顯示線上活動的伺服器" noServers: "資料庫中找不到伺服器" noServersLong: '看起來 Plan 沒有安裝在任何遊戲伺服器上或者遊戲伺服器未連接到相同的資料庫。 群組網路教程請參見:wiki' noSpongeChunks: "區塊資料在 Sponge 伺服端無法使用" + performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop." predictedNewPlayerRetention: "這個數值是基於之前的玩家資料預測的" error: 401Unauthorized: "未認證" @@ -257,8 +271,10 @@ html: emptyForm: "未指定使用者名稱與密碼" expiredCookie: "使用者 Cookie 已過期" generic: "認證時發生錯誤" + groupNotFound: "Web Permission Group does not exist" loginFailed: "使用者名稱和密碼無法配合" noCookie: "使用者 cookie 不存在" + noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission" registrationFailed: "註冊失敗,請重試(註冊代碼有效期 15 分鐘)" userNotFound: "使用者不存在" authFailed: "認證失敗。" @@ -315,9 +331,11 @@ html: deaths: "死亡數" disk: "硬碟空間" diskSpace: "剩餘硬碟空間" + docs: "Swagger Docs" downtime: "關機時間" duringLowTps: "持續低 TPS 時間" entities: "實體" + errors: "Plan Error Logs" exported: "Data export time" favoriteServer: "最喜愛的伺服器" firstSession: "第一此會話" @@ -331,6 +349,8 @@ html: miller: "Miller" ortographic: "Ortographic" geolocations: "地理位置" + groupPermissions: "Manage Groups" + groupUsers: "Manage Group Users" help: activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately." activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3." @@ -343,6 +363,23 @@ html: labels: "You can hide/show a group by clicking on the label at the bottom." title: "Graph" zoom: "You can Zoom in by click + dragging on the graph." + manage: + groups: + line-1: "This view allows you to modify web group permissions." + line-10: "{{permission}} permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints." + line-11: "{{permission1}} permissions are not required for data: {{permission2}} allows request to /v1/network/overview even without {{permission3}}." + line-12: "Saving changes" + line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)." + line-14: "When you modify permissions those changes need to be saved by pressing the Save-button" + line-15: "Documentation can be found from {{link}}" + line-2: "User's web group is determined during {{command}} by checking if Player has {{permission}} permission." + line-3: "You can use {{command}} to change permission group after registering." + line-4: "{{icon}} If you ever accidentally delete all groups with {{permission}}} permission just {{command}}." + line-5: "Permission inheritance" + line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. {{permission1}} also gives {{permission2}}, etc." + line-7: "Access vs Page -permissions" + line-8: "You need to assign both access and page permissions for users." + line-9: "{{permission1}} permissions allow user make the request to specific address, eg. {{permission2}} allows request to /network." playtimeUnit: "hours" retention: calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored." @@ -402,6 +439,30 @@ html: longestSession: "最長會話時間" lowTpsSpikes: "最低TPS" lowTpsSpikes7days: "最低TPS (7 天)" + manage: "Manage" + managePage: + addGroup: + header: "Add group" + invalidName: "Group name can be 100 characters maximum." + name: "Name of the group" + alert: + groupAddFail: "Failed to add group: {{error}}" + groupAddSuccess: "Added group '{{groupName}}'" + groupDeleteFail: "Failed to delete group: {{error}}" + groupDeleteSuccess: "Deleted group '{{groupName}}'" + saveFail: "Failed to save changes: {{error}}" + saveSuccess: "Changes saved successfully!" + changes: + discard: "Discard Changes" + save: "Save" + unsaved: "Unsaved changes" + deleteGroup: + confirm: "Confirm & Delete {{groupName}}" + confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!" + header: "Delete '{{groupName}}'" + moveToSelect: "Move remaining users to group" + groupHeader: "Manage Group Permissions" + groupPermissions: "Permissions of {{groupName}}" maxFreeDisk: "最大可用硬碟空間" medianSessionLength: "Median Session Length" minFreeDisk: "最小可用硬碟空間" @@ -537,6 +598,7 @@ html: unit: percentage: "Percentage" playerCount: "Player Count" + users: "Manage Users" veryActive: "非常活躍" weekComparison: "每週對比" weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'" @@ -557,6 +619,89 @@ html: password: "密碼" register: "建立一個帳戶!" username: "使用者名稱" + manage: + permission: + description: + access: "Controls access to pages" + access_docs: "Allows accessing /docs page" + access_errors: "Allows accessing /errors page" + access_network: "Allows accessing /network page" + access_player: "Allows accessing any /player pages" + access_player_self: "Allows accessing own /player page" + access_players: "Allows accessing /players page" + access_query: "Allows accessing /query and Query results pages" + access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions." + access_server: "Allows accessing all /server pages" + manage_groups: "Allows modifying group permissions & Access to /manage/groups page" + manage_users: "Allows modifying what users belong to what group" + page: "Controls what is visible on pages" + page_network: "See all of network page" + page_network_geolocations: "See Geolocations tab" + page_network_geolocations_map: "See Geolocations Map" + page_network_geolocations_ping_per_country: "See Ping Per Country table" + page_network_join_addresses: "See Join Addresses -tab" + page_network_join_addresses_graphs: "See Join Address graphs" + page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_network_join_addresses_graphs_time: "See Join Addresses over time graph" + page_network_overview: "See Network Overview -tab" + page_network_overview_graphs: "See Network Overview graphs" + page_network_overview_graphs_day_by_day: "See Day by Day graph" + page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph" + page_network_overview_graphs_online: "See Players Online graph" + page_network_overview_numbers: "See Network Overview numbers" + page_network_performance: "See network Performance tab" + page_network_playerbase: "See Playerbase Overview -tab" + page_network_playerbase_graphs: "See Playerbase Overview graphs" + page_network_playerbase_overview: "See Playerbase Overview numbers" + page_network_players: "See Player list -tab" + page_network_plugins: "See Plugins tab of Proxy" + page_network_retention: "See Player Retention -tab" + page_network_server_list: "See list of servers" + page_network_sessions: "See Sessions tab" + page_network_sessions_list: "See list of sessions" + page_network_sessions_overview: "See Session insights" + page_network_sessions_server_pie: "See Server Pie graph" + page_network_sessions_world_pie: "See World Pie graph" + page_player: "See all of player page" + page_player_overview: "See Player Overview -tab" + page_player_plugins: "See Plugins -tabs" + page_player_servers: "See Servers -tab" + page_player_sessions: "See Player Sessions -tab" + page_player_versus: "See PvP & PvE -tab" + page_server: "See all of server page" + page_server_geolocations: "See Geolocations tab" + page_server_geolocations_map: "See Geolocations Map" + page_server_geolocations_ping_per_country: "See Ping Per Country table" + page_server_join_addresses: "See Join Addresses -tab" + page_server_join_addresses_graphs: "See Join Address graphs" + page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" + page_server_join_addresses_graphs_time: "See Join Addresses over time graph" + page_server_online_activity: "See Online Activity -tab" + page_server_online_activity_graphs: "See Online Activity graphs" + page_server_online_activity_graphs_calendar: "See Server calendar" + page_server_online_activity_graphs_day_by_day: "See Day by Day graph" + page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph" + page_server_online_activity_graphs_punchcard: "See Punchcard graph" + page_server_online_activity_overview: "See Online Activity numbers" + page_server_overview: "See Server Overview -tab" + page_server_overview_numbers: "See Server Overview numbers" + page_server_overview_players_online_graph: "See Players Online graph" + page_server_performance: "See Performance tab" + page_server_performance_graphs: "See Performance graphs" + page_server_performance_overview: "See Performance numbers" + page_server_player_versus: "See PvP & PvE -tab" + page_server_player_versus_kill_list: "See Player kill and death lists" + page_server_player_versus_overview: "See PvP & PvE numbers" + page_server_playerbase: "See Playerbase Overview -tab" + page_server_playerbase_graphs: "See Playerbase Overview graphs" + page_server_playerbase_overview: "See Playerbase Overview numbers" + page_server_players: "See Player list -tab" + page_server_plugins: "See Plugins -tabs of servers" + page_server_retention: "See Player Retention -tab" + page_server_sessions: "See Sessions tab" + page_server_sessions_list: "See list of sessions" + page_server_sessions_overview: "See Session insights" + page_server_sessions_world_pie: "See World Pie graph" modal: info: bugs: "報告問題" @@ -648,6 +793,7 @@ html: completion3: "在遊戲中使用以下指令完成註冊:" completion4: "或使用控制台:" createNewUser: "建立一個新使用者" + disabled: "Registering new users has been disabled in the config." error: checkFailed: "檢查註冊狀態失敗:" failed: "註冊失敗:" diff --git a/Plan/common/src/test/java/com/djrapitops/plan/commands/PlanCommandTest.java b/Plan/common/src/test/java/com/djrapitops/plan/commands/PlanCommandTest.java index 866014c80..2a5555a01 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/commands/PlanCommandTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/commands/PlanCommandTest.java @@ -45,6 +45,7 @@ import java.util.Collections; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -154,11 +155,11 @@ class PlanCommandTest { } @Test - void networkCommandSendsLink(Database database) { + void networkCommandSendsLink(Database database) throws ExecutionException, InterruptedException { try { Server server = new Server(ServerUUID.randomUUID(), "Serve", "", ""); server.setProxy(true); - database.executeTransaction(new StoreServerInformationTransaction(server)); + database.executeTransaction(new StoreServerInformationTransaction(server)).get(); CMDSender sender = runCommand("network", "plan.network"); diff --git a/Plan/common/src/test/java/com/djrapitops/plan/commands/subcommands/RegistrationCommandsTest.java b/Plan/common/src/test/java/com/djrapitops/plan/commands/subcommands/RegistrationCommandsTest.java new file mode 100644 index 000000000..6975d638f --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/commands/subcommands/RegistrationCommandsTest.java @@ -0,0 +1,190 @@ +/* + * 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.commands.subcommands; + +import com.djrapitops.plan.PlanSystem; +import com.djrapitops.plan.commands.PlanCommand; +import com.djrapitops.plan.commands.use.Arguments; +import com.djrapitops.plan.commands.use.CMDSender; +import com.djrapitops.plan.commands.use.CommandWithSubcommands; +import com.djrapitops.plan.delivery.domain.auth.User; +import com.djrapitops.plan.delivery.webserver.Addresses; +import com.djrapitops.plan.settings.Permissions; +import com.djrapitops.plan.settings.config.PlanConfig; +import com.djrapitops.plan.settings.config.changes.ConfigUpdater; +import com.djrapitops.plan.settings.config.paths.WebserverSettings; +import com.djrapitops.plan.storage.database.Database; +import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction; +import com.djrapitops.plan.utilities.PassEncryptUtil; +import com.google.gson.Gson; +import extension.FullSystemExtension; +import org.apache.commons.compress.utils.IOUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.openqa.selenium.json.TypeToken; +import utilities.HTTPConnector; +import utilities.TestConstants; +import utilities.TestResources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author AuroraLS3 + */ +@ExtendWith(FullSystemExtension.class) +class RegistrationCommandsTest { + + @BeforeAll + static void beforeAll(@TempDir Path tempDir, PlanSystem system) throws Exception { + File file = tempDir.resolve("TestCert.p12").toFile(); + File testCert = TestResources.getTestResourceFile("TestCert.p12", ConfigUpdater.class); + Files.copy(testCert.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + String absolutePath = file.getAbsolutePath(); + + PlanConfig config = system.getConfigSystem().getConfig(); + config.set(WebserverSettings.CERTIFICATE_PATH, absolutePath); + config.set(WebserverSettings.CERTIFICATE_KEYPASS, "test"); + config.set(WebserverSettings.CERTIFICATE_STOREPASS, "test"); + config.set(WebserverSettings.CERTIFICATE_ALIAS, "test"); + + system.enable(); + } + + @AfterAll + static void afterAll(PlanSystem system) { + system.disable(); + } + + @Test + @DisplayName("User is registered with 'admin' group with 'plan.webgroup.admin' permission") + void normalRegistrationFlow(Addresses addresses, PlanCommand command, Database database) throws Exception { + String username = "normalRegistrationFlow"; + String code = registerUser(username, addresses); + + CMDSender sender = mock(CMDSender.class); + when(sender.isPlayer()).thenReturn(true); + when(sender.hasPermission(Permissions.REGISTER_SELF.getPermission())).thenReturn(true); + when(sender.hasPermission("plan.webgroup.admin")).thenReturn(true); + when(sender.getUUID()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_UUID)); + when(sender.getPlayerName()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_NAME)); + + command.build().executeCommand(sender, new Arguments(List.of("register", "--code", code))); + + User user = database.query(WebUserQueries.fetchUser(username)).orElseThrow(AssertionError::new); + assertEquals("admin", user.getPermissionGroup()); + } + + @Test + @DisplayName("User registration fails without any plan.webgroup.{name} permission.") + void noPermissionFlow(Addresses addresses, PlanCommand command, Database database) throws Exception { + String username = "noPermissionFlow"; + String code = registerUser(username, addresses); + + CMDSender sender = mock(CMDSender.class); + when(sender.isPlayer()).thenReturn(true); + when(sender.hasPermission(Permissions.REGISTER_SELF.getPermission())).thenReturn(true); + when(sender.getUUID()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_UUID)); + when(sender.getPlayerName()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_NAME)); + + CommandWithSubcommands cmd = command.build(); + Arguments arguments = new Arguments(List.of("register", "--code", code)); + assertThrows(IllegalArgumentException.class, () -> cmd.executeCommand(sender, arguments)); + + assertTrue(database.query(WebUserQueries.fetchUser(username)).isEmpty()); + } + + @Test + @DisplayName("User registration fails without any plan.webgroup.{name} permission attempting to bypass.") + void noPermissionFlowBypassAttempt(Addresses addresses, PlanCommand command, Database database) throws Exception { + String username = "noPermissionFlowBypassAttempt"; + String code = registerUser(username, addresses); + + CMDSender sender = mock(CMDSender.class); + when(sender.isPlayer()).thenReturn(true); + when(sender.hasPermission(Permissions.REGISTER_SELF.getPermission())).thenReturn(true); + when(sender.getUUID()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_UUID)); + when(sender.getPlayerName()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_NAME)); + + CommandWithSubcommands cmd = command.build(); + Arguments arguments = new Arguments(List.of("register", "--code", code, "superuser")); + assertThrows(IllegalArgumentException.class, () -> cmd.executeCommand(sender, arguments)); + + assertTrue(database.query(WebUserQueries.fetchUser(username)).isEmpty()); + } + + @Test + @DisplayName("User group is changed") + void setGroupCommandTest(PlanCommand command, Database database) throws Exception { + String username = "setGroupCommandTest"; + User user = new User(username, "console", null, PassEncryptUtil.createHash("testPass"), "admin", Collections.emptyList()); + database.executeTransaction(new StoreWebUserTransaction(user)).get(); + + CMDSender sender = mock(CMDSender.class); + when(sender.isPlayer()).thenReturn(true); + when(sender.hasPermission(Permissions.SET_GROUP.getPermission())).thenReturn(true); + when(sender.getUUID()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_UUID)); + when(sender.getPlayerName()).thenReturn(Optional.of(TestConstants.PLAYER_ONE_NAME)); + + command.build().executeCommand(sender, new Arguments(List.of("setgroup", username, "no_access"))); + + User modifiedUser = database.query(WebUserQueries.fetchUser(username)).orElseThrow(AssertionError::new); + assertEquals("no_access", modifiedUser.getPermissionGroup()); + } + + private static String registerUser(String username, Addresses addresses) throws IOException, KeyManagementException, NoSuchAlgorithmException { + HTTPConnector connector = new HTTPConnector(); + HttpURLConnection connection = null; + String code; + try { + String address = addresses.getFallbackLocalhostAddress(); + connection = connector.getConnection("POST", address + "/auth/register"); + connection.setDoOutput(true); + connection.getOutputStream().write(("user=" + username + "&password=testPass").getBytes()); + try (InputStream in = connection.getInputStream()) { + String responseBody = new String(IOUtils.toByteArray(in)); + assertTrue(responseBody.contains("\"code\":"), () -> "Not successful: " + responseBody); + Map read = new Gson().fromJson(responseBody, new TypeToken>() {}.getType()); + code = (String) read.get("code"); + System.out.println("Got registration code: " + code); + } + } finally { + if (connection != null) connection.disconnect(); + } + return code; + } +} \ No newline at end of file diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java index 8de295d2b..cf9b94fda 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java @@ -18,24 +18,29 @@ package com.djrapitops.plan.delivery.webserver; import com.djrapitops.plan.PlanSystem; import com.djrapitops.plan.delivery.domain.auth.User; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.extension.Caller; import com.djrapitops.plan.identification.Server; -import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.changes.ConfigUpdater; import com.djrapitops.plan.settings.config.paths.WebserverSettings; +import com.djrapitops.plan.storage.database.Database; import com.djrapitops.plan.storage.database.queries.ExtensionsDatabaseTest; import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction; import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction; import com.djrapitops.plan.utilities.PassEncryptUtil; import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import utilities.HTTPConnector; import utilities.RandomData; import utilities.TestConstants; @@ -51,10 +56,13 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /** * Tests for limiting user access control based on permissions. @@ -67,11 +75,87 @@ class AccessControlTest { private static PlanSystem system; private static String address; - private static String cookieLevel0; - private static String cookieLevel1; - private static String cookieLevel2; - private static ServerUUID serverUUID; - private static String cookieLevel100; + private static String cookieNoAccess; + + static Stream testCases() { + return Stream.of( + Arguments.of("/", WebPermission.ACCESS, 302, 403), + Arguments.of("/server", WebPermission.ACCESS_SERVER, 302, 403), + Arguments.of("/server/" + TestConstants.SERVER_UUID_STRING + "", WebPermission.ACCESS_SERVER, 200, 403), + Arguments.of("/css/style.css", WebPermission.ACCESS, 200, 200), + Arguments.of("/js/color-selector.js", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/serverOverview?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_OVERVIEW_NUMBERS, 200, 403), + Arguments.of("/v1/onlineOverview?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_OVERVIEW, 200, 403), + Arguments.of("/v1/sessionsOverview?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS, 200, 403), + Arguments.of("/v1/playerVersus?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYER_VERSUS, 200, 403), + Arguments.of("/v1/playerbaseOverview?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYERBASE_OVERVIEW, 200, 403), + Arguments.of("/v1/performanceOverview?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW, 200, 403), + Arguments.of("/v1/graph?type=optimizedPerformance&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, 200, 403), + Arguments.of("/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, 200, 403), + Arguments.of("/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS_WORLD_PIE, 200, 403), + Arguments.of("/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, 200, 403), + Arguments.of("/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE, 200, 403), + Arguments.of("/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP, 200, 403), + Arguments.of("/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY, 200, 403), + Arguments.of("/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_HOUR_BY_HOUR, 200, 403), + Arguments.of("/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR, 200, 403), + Arguments.of("/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_PUNCHCARD, 200, 403), + Arguments.of("/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + "", WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, 200, 403), + Arguments.of("/v1/players?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYERS, 200, 403), + Arguments.of("/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYER_VERSUS_KILL_LIST, 200, 403), + Arguments.of("/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_GEOLOCATIONS_PING_PER_COUNTRY, 200, 403), + Arguments.of("/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS_LIST, 200, 403), + Arguments.of("/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_RETENTION, 200, 403), + Arguments.of("/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_RETENTION, 200, 403), + Arguments.of("/network", WebPermission.ACCESS_NETWORK, 302, 403), + Arguments.of("/v1/network/overview", WebPermission.PAGE_NETWORK_OVERVIEW_NUMBERS, 200, 403), + Arguments.of("/v1/network/servers", WebPermission.PAGE_NETWORK_SERVER_LIST, 200, 403), + Arguments.of("/v1/network/sessionsOverview", WebPermission.PAGE_NETWORK_SESSIONS_OVERVIEW, 200, 403), + Arguments.of("/v1/network/playerbaseOverview", WebPermission.PAGE_NETWORK_PLAYERBASE_OVERVIEW, 200, 403), + Arguments.of("/v1/sessions", WebPermission.PAGE_NETWORK_SESSIONS_LIST, 200, 403), + Arguments.of("/v1/graph?type=playersOnline&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH, 200, 403), + Arguments.of("/v1/graph?type=uniqueAndNew", WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_DAY_BY_DAY, 200, 403), + Arguments.of("/v1/graph?type=hourlyUniqueAndNew", WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR, 200, 403), + Arguments.of("/v1/graph?type=serverPie", WebPermission.PAGE_NETWORK_SESSIONS_SERVER_PIE, 200, 403), + Arguments.of("/v1/graph?type=joinAddressPie", WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE, 200, 403), + Arguments.of("/v1/graph?type=activity", WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, 200, 403), + Arguments.of("/v1/graph?type=geolocation", WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, 200, 403), + Arguments.of("/v1/network/pingTable", WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY, 200, 403), + Arguments.of("/player/" + TestConstants.PLAYER_ONE_NAME + "", WebPermission.ACCESS_PLAYER, 200, 403), + Arguments.of("/player/" + TestConstants.PLAYER_TWO_NAME + "", WebPermission.ACCESS_PLAYER, 404, 403), + Arguments.of("/player/" + TestConstants.PLAYER_ONE_UUID_STRING + "", WebPermission.ACCESS_PLAYER, 200, 403), + Arguments.of("/player/" + TestConstants.PLAYER_TWO_UUID_STRING + "", WebPermission.ACCESS_PLAYER, 404, 403), + Arguments.of("/v1/player?player=" + TestConstants.PLAYER_ONE_NAME + "", WebPermission.ACCESS_PLAYER, 200, 403), + Arguments.of("/v1/player?player=" + TestConstants.PLAYER_TWO_NAME + "", WebPermission.ACCESS_PLAYER, 400, 403), + Arguments.of("/players", WebPermission.ACCESS_PLAYERS, 200, 403), + Arguments.of("/v1/players", WebPermission.ACCESS_PLAYERS, 200, 403), + Arguments.of("/query", WebPermission.ACCESS_QUERY, 200, 403), + Arguments.of("/v1/filters", WebPermission.ACCESS_QUERY, 200, 403), + Arguments.of("/v1/query", WebPermission.ACCESS_QUERY, 400, 403), + Arguments.of("/v1/query?q=%5B%5D&view=%7B%22afterDate%22%3A%2224%2F10%2F2022%22%2C%22afterTime%22%3A%2218%3A21%22%2C%22beforeDate%22%3A%2223%2F11%2F2022%22%2C%22beforeTime%22%3A%2217%3A21%22%2C%22servers%22%3A%5B%0A%7B%22serverUUID%22%3A%22" + TestConstants.SERVER_UUID_STRING + "%22%2C%22serverName%22%3A%22" + TestConstants.SERVER_NAME + "%22%2C%22proxy%22%3Afalse%7D%5D%7D", WebPermission.ACCESS_QUERY, 200, 403), + Arguments.of("/v1/errors", WebPermission.ACCESS_ERRORS, 200, 403), + Arguments.of("/errors", WebPermission.ACCESS_ERRORS, 200, 403), + Arguments.of("/v1/network/listServers", WebPermission.PAGE_NETWORK_PERFORMANCE, 200, 403), + Arguments.of("/v1/network/serverOptions", WebPermission.PAGE_NETWORK_PERFORMANCE, 200, 403), + Arguments.of("/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "]", WebPermission.PAGE_NETWORK_PERFORMANCE, 200, 403), + Arguments.of("/v1/version", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/whoami", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/metadata", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/networkMetadata", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/serverIdentity?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.ACCESS_SERVER, 200, 403), + Arguments.of("/v1/locale", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/locale/EN", WebPermission.ACCESS, 200, 200), + Arguments.of("/v1/locale/NonexistingLanguage", WebPermission.ACCESS, 404, 404), + Arguments.of("/docs/swagger.json", WebPermission.ACCESS_DOCS, 500, 403), // swagger.json not available during tests + Arguments.of("/docs", WebPermission.ACCESS_DOCS, 200, 403), + Arguments.of("/pageExtensionApi.js", WebPermission.ACCESS, 200, 200), + Arguments.of("/manage", WebPermission.MANAGE_GROUPS, 200, 403), + Arguments.of("/v1/groupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 200, 403), + Arguments.of("/v1/webGroups", WebPermission.MANAGE_GROUPS, 200, 403), + Arguments.of("/v1/deleteGroup?group=admin&moveTo=no_access", WebPermission.MANAGE_GROUPS, 400, 403), + Arguments.of("/v1/saveGroupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 400, 403) + ); + } @BeforeAll static void setUpClass(@TempDir Path tempDir) throws Exception { @@ -95,17 +179,13 @@ class AccessControlTest { system.enable(); - User userLevel0 = new User("test0", "console", null, PassEncryptUtil.createHash("testPass"), 0, Collections.emptyList()); - User userLevel1 = new User("test1", "console", null, PassEncryptUtil.createHash("testPass"), 1, Collections.emptyList()); - User userLevel2 = new User("test2", TestConstants.PLAYER_ONE_NAME, TestConstants.PLAYER_ONE_UUID, PassEncryptUtil.createHash("testPass"), 2, Collections.emptyList()); - User userLevel100 = new User("test100", "console", null, PassEncryptUtil.createHash("testPass"), 100, Collections.emptyList()); - system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(userLevel0)); - system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(userLevel1)); - system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(userLevel2)); - system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(userLevel100)); + User userNoAccess = new User("test0", "console", null, PassEncryptUtil.createHash("testPass"), "no_access", Collections.emptyList()); - system.getDatabaseSystem().getDatabase().executeTransaction(new PlayerRegisterTransaction(TestConstants.PLAYER_ONE_UUID, () -> 0L, TestConstants.PLAYER_ONE_NAME)); - system.getDatabaseSystem().getDatabase().executeTransaction(new StoreServerInformationTransaction(new Server( + Database database = system.getDatabaseSystem().getDatabase(); + database.executeTransaction(new StoreWebUserTransaction(userNoAccess)); + + database.executeTransaction(new PlayerRegisterTransaction(TestConstants.PLAYER_ONE_UUID, () -> 0L, TestConstants.PLAYER_ONE_NAME)); + database.executeTransaction(new StoreServerInformationTransaction(new Server( TestConstants.SERVER_UUID, TestConstants.SERVER_NAME, address, @@ -119,10 +199,7 @@ class AccessControlTest { assertTrue(system.getWebServerSystem().getWebServer().isAuthRequired()); address = "https://localhost:" + TEST_PORT_NUMBER; - cookieLevel0 = login(address, userLevel0.getUsername()); - cookieLevel1 = login(address, userLevel1.getUsername()); - cookieLevel2 = login(address, userLevel2.getUsername()); - cookieLevel100 = login(address, userLevel100.getUsername()); + cookieNoAccess = login(address, userNoAccess.getUsername()); } @AfterAll @@ -132,7 +209,7 @@ class AccessControlTest { } } - static String login(String address, String username) throws IOException, KeyManagementException, NoSuchAlgorithmException { + public static String login(String address, String username) throws IOException, KeyManagementException, NoSuchAlgorithmException { HttpURLConnection loginConnection = null; String cookie; try { @@ -151,318 +228,56 @@ class AccessControlTest { return cookie; } - @DisplayName("Access control test, level 0:") - @ParameterizedTest(name = "{0}: expecting {1}") - @CsvSource({ - "/,302", - "/server,302", - "/server/" + TestConstants.SERVER_UUID_STRING + ",200", - "/css/style.css,200", - "/js/color-selector.js,200", - "/v1/serverOverview?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/onlineOverview?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/sessionsOverview?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/playerVersus?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/playerbaseOverview?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/performanceOverview?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=optimizedPerformance&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",200", - "/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/network,302", - "/v1/network/overview,200", - "/v1/network/servers,200", - "/v1/network/sessionsOverview,200", - "/v1/network/playerbaseOverview,200", - "/v1/sessions,200", - "/v1/graph?type=playersOnline&server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/graph?type=uniqueAndNew,200", - "/v1/graph?type=hourlyUniqueAndNew,200", - "/v1/graph?type=serverPie,200", - "/v1/graph?type=joinAddressPie,200", - "/v1/graph?type=activity,200", - "/v1/graph?type=geolocation,200", - "/v1/network/pingTable,200", - "/player/" + TestConstants.PLAYER_ONE_NAME + ",200", - "/player/" + TestConstants.PLAYER_TWO_NAME + ",404", - "/player/" + TestConstants.PLAYER_ONE_UUID_STRING + ",200", - "/player/" + TestConstants.PLAYER_TWO_UUID_STRING + ",404", - "/v1/player?player=" + TestConstants.PLAYER_ONE_NAME + ",200", - "/v1/player?player=" + TestConstants.PLAYER_TWO_NAME + ",400", - "/players,200", - "/v1/players,200", - "/query,200", - "/v1/filters,200", - "/v1/query,400", - "/v1/query?q=%5B%5D&view=%7B%22afterDate%22%3A%2224%2F10%2F2022%22%2C%22afterTime%22%3A%2218%3A21%22%2C%22beforeDate%22%3A%2223%2F11%2F2022%22%2C%22beforeTime%22%3A%2217%3A21%22%2C%22servers%22%3A%5B%0A%7B%22serverUUID%22%3A%22" + TestConstants.SERVER_UUID_STRING + "%22%2C%22serverName%22%3A%22" + TestConstants.SERVER_NAME + "%22%2C%22proxy%22%3Afalse%7D%5D%7D,200", - "/v1/errors,200", - "/errors,200", - "/v1/network/listServers,200", - "/v1/network/serverOptions,200", - "/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],200", - "/v1/version,200", - "/v1/whoami,200", - "/v1/metadata,200", - "/v1/networkMetadata,200", - "/v1/serverIdentity?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/locale,200", - "/v1/locale/EN,200", - "/v1/locale/NonexistingLanguage,404", - "/docs/swagger.json,500", // swagger.json not available during tests - "/docs,200", - "/pageExtensionApi.js,200", - }) - void levelZeroCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException { - int responseCode = access(resource, cookieLevel0); - assertEquals(Integer.parseInt(expectedResponseCode), responseCode, () -> "User level 0, Wrong response code for " + resource + ", expected " + expectedResponseCode + " but was " + responseCode); + @DisplayName("Access control test") + @ParameterizedTest(name = "{0}: Permission {1}, expecting {2} with & {3} without permission") + @MethodSource("testCases") + void accessControlTest(String resource, WebPermission permission, int expectedWithPermission, int expectedWithout) throws Exception { + String cookie = login(address, createUserWithPermissions(resource, permission).getUsername()); + int responseCodeWithPermission = access(resource, cookie); + int responseCodeWithout = access(resource, cookieNoAccess); + + assertAll( + () -> assertEquals(expectedWithPermission, responseCodeWithPermission, + () -> "Permission '" + permission.getPermission() + "', Wrong response code for " + resource + ", expected " + expectedWithPermission + " but was " + responseCodeWithPermission), + () -> assertEquals(expectedWithout, responseCodeWithout, + () -> "No Permissions, Wrong response code for " + resource + ", expected " + expectedWithout + " but was " + responseCodeWithout) + ); } - @DisplayName("Access control test, level 1:") - @ParameterizedTest(name = "{0}: expecting {1}") - @CsvSource({ - "/,302", - "/server,403", - "/server/" + TestConstants.SERVER_UUID_STRING + ",403", - "/css/style.css,200", - "/js/color-selector.js,200", - "/v1/serverOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/onlineOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/sessionsOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/playerVersus?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/playerbaseOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/performanceOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=optimizedPerformance&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",403", - "/network,403", - "/v1/network/overview,403", - "/v1/network/servers,403", - "/v1/network/sessionsOverview,403", - "/v1/network/playerbaseOverview,403", - "/v1/sessions,403", - "/v1/graph?type=playersOnline&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=uniqueAndNew,403", - "/v1/graph?type=hourlyUniqueAndNew,403", - "/v1/graph?type=serverPie,403", - "/v1/graph?type=joinAddressPie,403", - "/v1/graph?type=activity,403", - "/v1/graph?type=geolocation,403", - "/v1/network/pingTable,403", - "/player/" + TestConstants.PLAYER_ONE_NAME + ",200", - "/player/" + TestConstants.PLAYER_TWO_NAME + ",404", - "/player/" + TestConstants.PLAYER_ONE_UUID_STRING + ",200", - "/player/" + TestConstants.PLAYER_TWO_UUID_STRING + ",404", - "/v1/player?player=" + TestConstants.PLAYER_ONE_NAME + ",200", - "/v1/player?player=" + TestConstants.PLAYER_TWO_NAME + ",400", - "/players,200", - "/v1/players,200", - "/query,200", - "/v1/filters,200", - "/v1/query,400", - "/v1/query?q=%5B%5D&view=%7B%22afterDate%22%3A%2224%2F10%2F2022%22%2C%22afterTime%22%3A%2218%3A21%22%2C%22beforeDate%22%3A%2223%2F11%2F2022%22%2C%22beforeTime%22%3A%2217%3A21%22%2C%22servers%22%3A%5B%0A%7B%22serverUUID%22%3A%22" + TestConstants.SERVER_UUID_STRING + "%22%2C%22serverName%22%3A%22" + TestConstants.SERVER_NAME + "%22%2C%22proxy%22%3Afalse%7D%5D%7D,200", - "/v1/errors,403", - "/errors,403", - "/v1/network/listServers,403", - "/v1/network/serverOptions,403", - "/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],403", - "/v1/version,200", - "/v1/whoami,200", - "/v1/metadata,200", - "/v1/networkMetadata,200", - "/v1/serverIdentity?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/locale,200", - "/v1/locale/EN,200", - "/v1/locale/NonexistingLanguage,404", - "/docs/swagger.json,403", - "/docs,403", - "/pageExtensionApi.js,200", - }) - void levelOneCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException { - int responseCode = access(resource, cookieLevel1); - assertEquals(Integer.parseInt(expectedResponseCode), responseCode, () -> "User level 1, Wrong response code for " + resource + ", expected " + expectedResponseCode + " but was " + responseCode); + @DisplayName("Access test player/uuid/raw") + @Test + void playerRawAccess() throws Exception { + String resource = "/player/" + TestConstants.PLAYER_ONE_UUID + "/raw"; + int expectedWithPermission = 200; + int expectedWithout = 403; + String cookie = login(address, createUserWithPermissions(resource, WebPermission.ACCESS_PLAYER, WebPermission.ACCESS_RAW_PLAYER_DATA).getUsername()); + String cookieJustPage = login(address, createUserWithPermissions(resource, WebPermission.ACCESS_PLAYER).getUsername()); + int responseCodeWithPermission = access(resource, cookie); + int responseCodeJustPage = access(resource, cookieJustPage); + int responseCodeWithout = access(resource, cookieNoAccess); + + assertAll( + () -> assertEquals(expectedWithPermission, responseCodeWithPermission, + () -> "Permission 'access.player', 'access.raw.player.data', Wrong response code for " + resource + ", expected " + expectedWithPermission + " but was " + responseCodeWithPermission), + () -> assertEquals(expectedWithout, responseCodeJustPage, + () -> "Just page visibility permissions, Wrong response code for " + resource + ", expected " + expectedWithout + " but was " + responseCodeJustPage), + () -> assertEquals(expectedWithout, responseCodeWithout, + () -> "No Permissions, Wrong response code for " + resource + ", expected " + expectedWithout + " but was " + responseCodeWithout) + ); } - @DisplayName("Access control test, level 2:") - @ParameterizedTest(name = "{0}: expecting {1}") - @CsvSource({ - "/,302", - "/server,403", - "/server/" + TestConstants.SERVER_UUID_STRING + ",403", - "/css/style.css,200", - "/js/color-selector.js,200", - "/v1/serverOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/onlineOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/sessionsOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/playerVersus?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/playerbaseOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/performanceOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=optimizedPerformance&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",403", - "/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/network,403", - "/v1/network/overview,403", - "/v1/network/servers,403", - "/v1/network/sessionsOverview,403", - "/v1/network/playerbaseOverview,403", - "/v1/sessions,403", - "/v1/graph?type=playersOnline&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=uniqueAndNew,403", - "/v1/graph?type=hourlyUniqueAndNew,403", - "/v1/graph?type=serverPie,403", - "/v1/graph?type=joinAddressPie,403", - "/v1/graph?type=activity,403", - "/v1/graph?type=geolocation,403", - "/v1/network/pingTable,403", - "/player/" + TestConstants.PLAYER_ONE_NAME + ",200", - "/player/" + TestConstants.PLAYER_TWO_NAME + ",403", - "/player/" + TestConstants.PLAYER_ONE_UUID_STRING + ",200", - "/player/" + TestConstants.PLAYER_TWO_UUID_STRING + ",403", - "/v1/player?player=" + TestConstants.PLAYER_ONE_NAME + ",200", - "/v1/player?player=" + TestConstants.PLAYER_TWO_NAME + ",403", - "/players,403", - "/v1/players,403", - "/query,403", - "/v1/filters,403", - "/v1/query,403", - "/v1/query?q=%5B%5D&view=%7B%22afterDate%22%3A%2224%2F10%2F2022%22%2C%22afterTime%22%3A%2218%3A21%22%2C%22beforeDate%22%3A%2223%2F11%2F2022%22%2C%22beforeTime%22%3A%2217%3A21%22%2C%22servers%22%3A%5B%0A%7B%22serverUUID%22%3A%22" + TestConstants.SERVER_UUID_STRING + "%22%2C%22serverName%22%3A%22" + TestConstants.SERVER_NAME + "%22%2C%22proxy%22%3Afalse%7D%5D%7D,403", - "/v1/errors,403", - "/errors,403", - "/v1/network/listServers,403", - "/v1/network/serverOptions,403", - "/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],403", - "/v1/version,200", - "/v1/whoami,200", - "/v1/metadata,200", - "/v1/networkMetadata,200", - "/v1/serverIdentity?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/locale,200", - "/v1/locale/EN,200", - "/v1/locale/NonexistingLanguage,404", - "/docs/swagger.json,403", - "/docs,403", - "/pageExtensionApi.js,200", - }) - void levelTwoCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException { - int responseCode = access(resource, cookieLevel2); - assertEquals(Integer.parseInt(expectedResponseCode), responseCode, () -> "User level 2, Wrong response code for " + resource + ", expected " + expectedResponseCode + " but was " + responseCode); - } + private User createUserWithPermissions(String resource, WebPermission... permissions) throws ExecutionException, InterruptedException { + Database db = system.getDatabaseSystem().getDatabase(); - @DisplayName("Access control test, level 100:") - @ParameterizedTest(name = "{0}: expecting {1}") - @CsvSource({ - "/,403", - "/server,403", - "/server/" + TestConstants.SERVER_UUID_STRING + ",403", - "/css/style.css,200", - "/js/color-selector.js,200", - "/v1/serverOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/onlineOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/sessionsOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/playerVersus?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/playerbaseOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/performanceOverview?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=optimizedPerformance&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",403", - "/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/network,403", - "/v1/network/overview,403", - "/v1/network/servers,403", - "/v1/network/sessionsOverview,403", - "/v1/network/playerbaseOverview,403", - "/v1/sessions,403", - "/v1/graph?type=playersOnline&server=" + TestConstants.SERVER_UUID_STRING + ",403", - "/v1/graph?type=uniqueAndNew,403", - "/v1/graph?type=hourlyUniqueAndNew,403", - "/v1/graph?type=serverPie,403", - "/v1/graph?type=joinAddressPie,403", - "/v1/graph?type=activity,403", - "/v1/graph?type=geolocation,403", - "/v1/network/pingTable,403", - "/player/" + TestConstants.PLAYER_ONE_NAME + ",403", - "/player/" + TestConstants.PLAYER_TWO_NAME + ",403", - "/player/" + TestConstants.PLAYER_ONE_UUID_STRING + ",403", - "/player/" + TestConstants.PLAYER_TWO_UUID_STRING + ",403", - "/v1/player?player=" + TestConstants.PLAYER_ONE_NAME + ",403", - "/v1/player?player=" + TestConstants.PLAYER_TWO_NAME + ",403", - "/players,403", - "/v1/players,403", - "/query,403", - "/v1/filters,403", - "/v1/query?q=%5B%5D&view=%7B%22afterDate%22%3A%2224%2F10%2F2022%22%2C%22afterTime%22%3A%2218%3A21%22%2C%22beforeDate%22%3A%2223%2F11%2F2022%22%2C%22beforeTime%22%3A%2217%3A21%22%2C%22servers%22%3A%5B%0A%7B%22serverUUID%22%3A%22" + TestConstants.SERVER_UUID_STRING + "%22%2C%22serverName%22%3A%22" + TestConstants.SERVER_NAME + "%22%2C%22proxy%22%3Afalse%7D%5D%7D,403", - "/v1/query,403", - "/v1/network/listServers,403", - "/v1/network/serverOptions,403", - "/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],403", - "/v1/version,200", - "/v1/whoami,200", - "/v1/metadata,200", - "/v1/networkMetadata,200", - "/v1/serverIdentity?server=" + TestConstants.SERVER_UUID_STRING + ",200", - "/v1/locale,200", - "/v1/locale/EN,200", - "/v1/locale/NonexistingLanguage,404", - "/docs/swagger.json,403", - "/docs,403", - "/pageExtensionApi.js,200", - }) - void levelHundredCanNotAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException { - int responseCode = access(resource, cookieLevel100); - assertEquals(Integer.parseInt(expectedResponseCode), responseCode, () -> "User level 100, Wrong response code for " + resource + ", expected " + expectedResponseCode + " but was " + responseCode); + String groupName = StringUtils.truncate(resource, 75); + db.executeTransaction( + new StoreWebGroupTransaction(groupName, Arrays.stream(permissions).map(WebPermission::getPermission).collect(Collectors.toList())) + ).get(); + + User user = new User(RandomData.randomString(45), "console", null, PassEncryptUtil.createHash("testPass"), groupName, Collections.emptyList()); + db.executeTransaction(new StoreWebUserTransaction(user)).get(); + + return user; } private int access(String resource, String cookie) throws IOException, KeyManagementException, NoSuchAlgorithmException { diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java new file mode 100644 index 000000000..db73257de --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java @@ -0,0 +1,405 @@ +/* + * 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; + +import com.djrapitops.plan.PlanSystem; +import com.djrapitops.plan.delivery.domain.auth.User; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.export.ExportTestUtilities; +import com.djrapitops.plan.identification.Server; +import com.djrapitops.plan.identification.ServerUUID; +import com.djrapitops.plan.settings.config.PlanConfig; +import com.djrapitops.plan.settings.config.changes.ConfigUpdater; +import com.djrapitops.plan.settings.config.paths.DataGatheringSettings; +import com.djrapitops.plan.settings.config.paths.DisplaySettings; +import com.djrapitops.plan.settings.config.paths.WebserverSettings; +import com.djrapitops.plan.storage.database.Database; +import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; +import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction; +import com.djrapitops.plan.storage.database.transactions.events.StoreServerPlayerTransaction; +import com.djrapitops.plan.utilities.PassEncryptUtil; +import extension.FullSystemExtension; +import extension.SeleniumExtension; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import utilities.RandomData; +import utilities.TestConstants; +import utilities.TestResources; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author AuroraLS3 + */ +@ExtendWith({SeleniumExtension.class, FullSystemExtension.class}) +class AccessControlVisibilityTest { + private static final int TEST_PORT_NUMBER = RandomData.randomInt(9005, 9500); + private static final String PASSWORD = "testPass"; + + @BeforeAll + static void setUp(PlanSystem system, @TempDir Path tempDir, PlanConfig config) throws Exception { + File certFile = tempDir.resolve("TestCert.p12").toFile(); + File testCert = TestResources.getTestResourceFile("TestCert.p12", ConfigUpdater.class); + Files.copy(testCert.toPath(), certFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + + config.set(WebserverSettings.PORT, TEST_PORT_NUMBER); + // Avoid accidentally DDoS:ing head image service during tests. + config.set(DisplaySettings.PLAYER_HEAD_IMG_URL, "data:image/png;base64,AA=="); + config.set(WebserverSettings.CERTIFICATE_PATH, certFile.getAbsolutePath()); + config.set(WebserverSettings.CERTIFICATE_KEYPASS, "test"); + config.set(WebserverSettings.CERTIFICATE_STOREPASS, "test"); + config.set(WebserverSettings.CERTIFICATE_ALIAS, "test"); + config.set(DataGatheringSettings.ACCEPT_GEOLITE2_EULA, true); + config.set(DataGatheringSettings.GEOLOCATIONS, true); + system.enable(); + } + + @AfterEach + void tearDownTest(WebDriver driver) { + SeleniumExtension.newTab(driver); + driver.manage().deleteAllCookies(); + } + + @AfterAll + static void tearDown(PlanSystem system) { + system.disable(); + } + + User registerUser(Database db, WebPermission... permissions) throws Exception { + String groupName = RandomData.randomString(75); + db.executeTransaction( + new StoreWebGroupTransaction(groupName, Arrays.stream(permissions) + .map(WebPermission::getPermission) + .collect(Collectors.toList())) + ).get(); + + User user = new User(RandomData.randomString(45), "console", null, PassEncryptUtil.createHash(PASSWORD), groupName, Collections.emptyList()); + db.executeTransaction(new StoreWebUserTransaction(user)).get(); + + return user; + } + + static Stream serverPageElementVisibleCases() { + return Stream.of( + Arguments.arguments(WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH, "players-online-graph", "overview"), + Arguments.arguments(WebPermission.PAGE_SERVER_OVERVIEW_NUMBERS, "last-7-days", "overview"), + Arguments.arguments(WebPermission.PAGE_SERVER_OVERVIEW_NUMBERS, "server-as-numbers", "overview"), + Arguments.arguments(WebPermission.PAGE_SERVER_OVERVIEW_NUMBERS, "week-comparison", "overview"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS, "online-activity-graphs", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY, "day-by-day-nav", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_HOUR_BY_HOUR, "hour-by-hour-nav", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR, "server-calendar-nav", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_PUNCHCARD, "punchcard-nav", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_OVERVIEW, "online-activity-numbers", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_OVERVIEW, "online-activity-insights", "online-activity"), + Arguments.arguments(WebPermission.PAGE_SERVER_SESSIONS_OVERVIEW, "session-insights", "sessions"), + Arguments.arguments(WebPermission.PAGE_SERVER_SESSIONS_WORLD_PIE, "world-pie", "sessions"), + Arguments.arguments(WebPermission.PAGE_SERVER_SESSIONS_LIST, "session-list", "sessions"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYER_VERSUS_OVERVIEW, "pvp-pve-as-numbers", "pvppve"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYER_VERSUS_OVERVIEW, "pvp-pve-insights", "pvppve"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYER_VERSUS_KILL_LIST, "pvp-kills-table", "pvppve"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_OVERVIEW, "playerbase-trends", "playerbase"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_OVERVIEW, "playerbase-insights", "playerbase"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, "playerbase-graph", "playerbase"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, "playerbase-current", "playerbase"), + Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, "join-address-graph", "join-addresses"), + Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE, "join-address-groups", "join-addresses"), + Arguments.arguments(WebPermission.PAGE_SERVER_RETENTION, "retention-graph", "retention"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERS, "players-table", "players"), + Arguments.arguments(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP, "geolocations", "geolocations"), + Arguments.arguments(WebPermission.PAGE_SERVER_GEOLOCATIONS_PING_PER_COUNTRY, "ping-per-country", "geolocations"), + Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, "performance-graphs", "performance"), + Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW, "performance-as-numbers", "performance"), + Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW, "performance-insights", "performance"), + Arguments.arguments(WebPermission.PAGE_SERVER_PLUGINS, "server-plugin-data", "plugins-overview") + ); + } + + static Stream networkPageElementVisibleCases() { + return Stream.of( + Arguments.arguments(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE, "online-activity-nav", "overview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_DAY_BY_DAY, "day-by-day-nav", "overview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR, "hour-by-hour-nav", "overview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_OVERVIEW, "recent-players", "overview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_OVERVIEW, "network-as-numbers", "overview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_OVERVIEW, "week-comparison", "overview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_SERVER_LIST, "row-network-servers-0", "serversOverview"), + Arguments.arguments(WebPermission.PAGE_NETWORK_SESSIONS_OVERVIEW, "session-insights", "sessions"), + Arguments.arguments(WebPermission.PAGE_NETWORK_SESSIONS_SERVER_PIE, "server-pie", "sessions"), + Arguments.arguments(WebPermission.PAGE_NETWORK_SESSIONS_LIST, "session-list", "sessions"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_OVERVIEW, "playerbase-trends", "playerbase"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_OVERVIEW, "playerbase-insights", "playerbase"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, "playerbase-graph", "playerbase"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, "playerbase-current", "playerbase"), + Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, "join-address-graph", "join-addresses"), + Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE, "join-address-groups", "join-addresses"), + Arguments.arguments(WebPermission.PAGE_NETWORK_RETENTION, "retention-graph", "retention"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERS, "players-table", "players"), + Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, "geolocations", "geolocations"), + Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY, "ping-per-country", "geolocations"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PERFORMANCE, "row-network-performance-0", "performance"), + Arguments.arguments(WebPermission.PAGE_NETWORK_PLUGINS, "server-plugin-data", "plugins-overview") + ); + } + + static Stream playerPageVisibleCases() { + return Stream.of( + Arguments.arguments(WebPermission.PAGE_PLAYER_OVERVIEW, "player-overview", "overview"), + Arguments.arguments(WebPermission.PAGE_PLAYER_SESSIONS, "player-sessions", "sessions"), + Arguments.arguments(WebPermission.PAGE_PLAYER_VERSUS, "player-pvp-pve", "pvppve"), + Arguments.arguments(WebPermission.PAGE_PLAYER_SERVERS, "player-servers", "servers"), + Arguments.arguments(WebPermission.PAGE_PLAYER_PLUGINS, "player-plugin-data", "plugins/Server%201") + ); + } + + static Stream pageLevelVisibleCases() { + return Stream.of( + Arguments.arguments(WebPermission.MANAGE_GROUPS, "slice_h_0", "manage"), + Arguments.arguments(WebPermission.ACCESS_QUERY, "query-button", "query"), + Arguments.arguments(WebPermission.ACCESS_PLAYERS, "players-table_wrapper", "players"), + Arguments.arguments(WebPermission.ACCESS_ERRORS, "content", "errors") +// Arguments.arguments(WebPermission.ACCESS_DOCS, "swagger-ui", "docs") + ); + } + + @DisplayName("Whole page is visible with permission") + @ParameterizedTest(name = "Access with visibility {0} can see element #{1} in /{2}") + @MethodSource("pageLevelVisibleCases") + void pageVisible(WebPermission permission, String element, String page, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + User user = registerUser(database, permission); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/" + page; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id(element), driver); + assertDoesNotThrow(() -> driver.findElement(By.id(element)), () -> "Did not see #" + element + " at " + address + " with permission '" + permission.getPermission() + "'"); + assertNoLogs(driver, address); + } + + private static void assertNoLogs(ChromeDriver driver, String address) { + List logs = new ArrayList<>(); + logs.addAll(driver.manage().logs().get(LogType.CLIENT).getAll()); + logs.addAll(driver.manage().logs().get(LogType.BROWSER).getAll()); + ExportTestUtilities.assertNoLogs(logs, address); + } + + @DisplayName("Whole page is not visible with permission") + @ParameterizedTest(name = "Access with no visibility (needs {0}) can't see element #{1} in /{2}") + @MethodSource("pageLevelVisibleCases") + void pageNotVisible(WebPermission permission, String element, String page, Database database, ChromeDriver driver) throws Exception { + User user = registerUser(database); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/" + page; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver); + By id = By.id(element); + assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to"); + assertNoLogs(driver, address); + } + + void login(ChromeDriver driver, User user) { +// String cookie = AccessControlTest.login("https://localhost:" + TEST_PORT_NUMBER, user.getUsername()); +// driver.manage().addCookie(new Cookie("auth", cookie.split("=")[1])); + SeleniumExtension.waitForPageLoadForSeconds(5, driver); + SeleniumExtension.waitForElementToBeVisible(By.id("inputUser"), driver); + + driver.findElement(By.id("inputUser")).sendKeys(user.getUsername()); + driver.findElement(By.id("inputPassword")).sendKeys(PASSWORD); + driver.findElement(By.id("login-button")).click(); + } + + @DisplayName("Server element is visible with permission") + @ParameterizedTest(name = "Access to server page with visibility {0} can see element #{1} in section /server/uuid/{2}") + @MethodSource("serverPageElementVisibleCases") + void serverPageElementVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + User user = registerUser(database, WebPermission.ACCESS_SERVER, permission); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/server/" + serverUUID + "/" + section; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id(element), driver); + assertDoesNotThrow(() -> driver.findElement(By.id(element)), () -> "Did not see #" + element + " at " + address + " with permission '" + permission.getPermission() + "'"); + assertNoLogs(driver, address); + } + + private static void storePlayer(Database database, ServerUUID serverUUID) throws ExecutionException, InterruptedException { + storePlayer(database, serverUUID, TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME); + } + + private static void storePlayer(Database database, ServerUUID serverUUID, UUID playerUUID, String playerName) throws ExecutionException, InterruptedException { + database.executeTransaction(new StoreServerPlayerTransaction(playerUUID, System.currentTimeMillis(), playerName, serverUUID, TestConstants.GET_PLAYER_HOSTNAME.get())) + .get(); + } + + @DisplayName("Server element is not visible without permission") + @ParameterizedTest(name = "Access to server page with no visibility (needs {0}) can't see element #{1} in section /server/uuid/{2}") + @MethodSource("serverPageElementVisibleCases") + void serverPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + User user = registerUser(database, WebPermission.ACCESS_SERVER); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/server/" + serverUUID + "/" + section; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver); + By id = By.id(element); + assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to"); + assertNoLogs(driver, address); + } + + private void registerProxy(Database database) throws ExecutionException, InterruptedException { + database.executeTransaction(new StoreServerInformationTransaction( + new Server(TestConstants.SERVER_TWO_UUID, "Proxy", "https://localhost", TestConstants.VERSION) + )).get(); + } + + @DisplayName("Network element is visible with permission") + @ParameterizedTest(name = "Access to network page with visibility {0} can see element #{1} in section /network/{2}") + @MethodSource("networkPageElementVisibleCases") + void networkPageElementVisible(WebPermission permission, String element, String section, Database database, ChromeDriver driver) throws Exception { + User user = registerUser(database, WebPermission.ACCESS_NETWORK, permission); + registerProxy(database); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/network/" + section; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id(element), driver); + assertDoesNotThrow(() -> driver.findElement(By.id(element)), () -> "Did not see #" + element + " at " + address + " with permission '" + permission.getPermission() + "'"); + assertNoLogs(driver, address); + } + + @DisplayName("Network element is not visible without permission") + @ParameterizedTest(name = "Access to network page with no visibility (needs {0}) can't see element #{1} in section /network/{2}") + @MethodSource("networkPageElementVisibleCases") + void networkPageElementNotVisible(WebPermission permission, String element, String section, Database database, ChromeDriver driver) throws Exception { + User user = registerUser(database, WebPermission.ACCESS_NETWORK); + registerProxy(database); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/network/" + section; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver); + By id = By.id(element); + assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to"); + assertNoLogs(driver, address); + } + + @DisplayName("Player element is visible with permission") + @ParameterizedTest(name = "Access to player page with visibility {0} can see element #{1} in section /player/uuid/{2}") + @MethodSource("playerPageVisibleCases") + void playerPageElementVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + User user = registerUser(database, WebPermission.ACCESS_PLAYER, permission); + storePlayer(database, serverUUID); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/player/" + TestConstants.PLAYER_ONE_UUID + "/" + section; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id(element), driver); + assertDoesNotThrow(() -> driver.findElement(By.id(element)), () -> "Did not see #" + element + " at " + address + " with permission '" + permission.getPermission() + "'"); + assertNoLogs(driver, address); + } + + @DisplayName("Player element is not visible without permission") + @ParameterizedTest(name = "Access to player page with no visibility (needs {0}) can't see element #{1} in section /player/uuid/{2}") + @MethodSource("playerPageVisibleCases") + void playerPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + User user = registerUser(database, WebPermission.ACCESS_PLAYER); + storePlayer(database, serverUUID); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/player/" + TestConstants.PLAYER_ONE_UUID + "/" + section; + driver.get(address); + login(driver, user); + + SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver); + By id = By.id(element); + assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to"); + assertNoLogs(driver, address); + } + + @Test + @DisplayName("ACCESS_PLAYER_SELF can see own player page") + void playerSelfVisibilityTests(Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + String element = "player-overview"; + + User user = registerUser(database, WebPermission.ACCESS_PLAYER_SELF, WebPermission.PAGE_PLAYER); + // Link a user to the player + User playerUser = new User("player_user", TestConstants.PLAYER_ONE_NAME, TestConstants.PLAYER_ONE_UUID, PassEncryptUtil.createHash(PASSWORD), user.getPermissionGroup(), user.getPermissions()); + database.executeTransaction(new StoreWebUserTransaction(playerUser)).get(); + + storePlayer(database, serverUUID, TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME); + storePlayer(database, serverUUID, TestConstants.PLAYER_TWO_UUID, TestConstants.PLAYER_TWO_NAME); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/player/" + TestConstants.PLAYER_ONE_UUID; + driver.get(address); + login(driver, playerUser); + + SeleniumExtension.waitForElementToBeVisible(By.id(element), driver); + assertDoesNotThrow(() -> driver.findElement(By.id(element)), () -> "Did not see #" + element + " at /player/" + TestConstants.PLAYER_ONE_UUID + " with permission '" + WebPermission.ACCESS_PLAYER_SELF.getPermission() + "'"); + assertNoLogs(driver, address); + } + + + @Test + @DisplayName("ACCESS_PLAYER_SELF can not see other player's page") + void playerSelfNonVisibilityTests(Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception { + String element = "player-overview"; + + User user = registerUser(database, WebPermission.ACCESS_PLAYER_SELF, WebPermission.PAGE_PLAYER); + // Link a user to the player + User playerUser = new User("player_user", TestConstants.PLAYER_ONE_NAME, TestConstants.PLAYER_ONE_UUID, PassEncryptUtil.createHash(PASSWORD), user.getPermissionGroup(), user.getPermissions()); + database.executeTransaction(new StoreWebUserTransaction(playerUser)); + + storePlayer(database, serverUUID, TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME); + storePlayer(database, serverUUID, TestConstants.PLAYER_TWO_UUID, TestConstants.PLAYER_TWO_NAME); + + String address = "https://localhost:" + TEST_PORT_NUMBER + "/player/" + TestConstants.PLAYER_TWO_UUID; + driver.get(address); + login(driver, playerUser); + + SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver); + By id = By.id(element); + assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at /player/" + TestConstants.PLAYER_TWO_UUID + " without permission to"); + } +} diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java new file mode 100644 index 000000000..3caf03281 --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java @@ -0,0 +1,117 @@ +/* + * 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; + +import com.djrapitops.plan.PlanSystem; +import com.djrapitops.plan.extension.Caller; +import com.djrapitops.plan.identification.Server; +import com.djrapitops.plan.settings.config.paths.WebserverSettings; +import com.djrapitops.plan.storage.database.Database; +import com.djrapitops.plan.storage.database.queries.ExtensionsDatabaseTest; +import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction; +import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction; +import extension.FullSystemExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import utilities.HTTPConnector; +import utilities.RandomData; +import utilities.TestConstants; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Test against endpoints that should not be visible without authentication. + * + * @author AuroraLS3 + */ +@ExtendWith(FullSystemExtension.class) +class HttpAccessControlTest { + + private static final int TEST_PORT_NUMBER = RandomData.randomInt(9005, 9500); + private static final String ADDRESS = "http://localhost:" + TEST_PORT_NUMBER; + + private static final HTTPConnector CONNECTOR = new HTTPConnector(); + + @BeforeAll + static void setUp(PlanSystem system) { + system.getConfigSystem().getConfig().set(WebserverSettings.PORT, TEST_PORT_NUMBER); + system.enable(); + + Database database = system.getDatabaseSystem().getDatabase(); + + database.executeTransaction(new PlayerRegisterTransaction(TestConstants.PLAYER_ONE_UUID, () -> 0L, TestConstants.PLAYER_ONE_NAME)); + database.executeTransaction(new StoreServerInformationTransaction(new Server( + TestConstants.SERVER_UUID, + TestConstants.SERVER_NAME, + ADDRESS, + TestConstants.VERSION))); + + Caller caller = system.getExtensionService().register(new ExtensionsDatabaseTest.PlayerExtension()) + .orElseThrow(AssertionError::new); + caller.updatePlayerData(TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME); + + assertFalse(system.getWebServerSystem().getWebServer().isAuthRequired()); + } + + @AfterAll + static void afterAll(PlanSystem system) { + system.disable(); + } + + @DisplayName("Endpoints disabled without authentication") + @ParameterizedTest(name = "Endpoint {0} is disabled without authentication") + @CsvSource({ + "/v1/webGroups", + "/v1/groupPermissions?group=admin", + "/v1/permissions", + "/v1/saveGroupPermissions", + "/v1/deleteGroup", + "/manage", + "/auth/register", + "/auth/login", + "/auth/logout", + "/login", + "/register" + }) + void endpointNotEnabled(String endpoint) throws Exception { + int code = access(endpoint); + assertEquals(404, code); + } + + + private int access(String resource) throws IOException, KeyManagementException, NoSuchAlgorithmException { + HttpURLConnection connection = null; + try { + connection = CONNECTOR.getConnection("GET", ADDRESS + resource); + + return connection.getResponseCode(); + + } finally { + if (connection != null) connection.disconnect(); + } + } +} diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JSErrorRegressionTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JSErrorRegressionTest.java index 072617275..61d3f9a1f 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JSErrorRegressionTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JSErrorRegressionTest.java @@ -43,7 +43,6 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.logging.LogEntry; import org.openqa.selenium.logging.LogType; -import org.testcontainers.shaded.org.awaitility.Awaitility; import utilities.RandomData; import utilities.TestConstants; import utilities.mocks.PluginMockComponent; @@ -52,7 +51,6 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -173,9 +171,7 @@ class JSErrorRegressionTest { for (String href : anchorLinks) { driver.get(address); - Awaitility.await() - .atMost(3, TimeUnit.SECONDS) - .until(() -> "complete".equals(driver.executeScript("return document.readyState"))); + SeleniumExtension.waitForPageLoadForSeconds(3, driver); List logs = new ArrayList<>(); logs.addAll(driver.manage().logs().get(LogType.CLIENT).getAll()); diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JksHttpsServerTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JksHttpsServerTest.java index dd2d4621e..a45a93102 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JksHttpsServerTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/JksHttpsServerTest.java @@ -60,7 +60,7 @@ class JksHttpsServerTest implements HttpsServerTest { system.enable(); - User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), 0, Collections.emptyList()); + User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), "admin", Collections.emptyList()); system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(user)); } diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/OpenRedirectFuzzTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/OpenRedirectFuzzTest.java index 13edeb8ce..575c66cf7 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/OpenRedirectFuzzTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/OpenRedirectFuzzTest.java @@ -88,7 +88,7 @@ class OpenRedirectFuzzTest implements HttpsServerTest { system.enable(); - User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), 0, Collections.emptyList()); + User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), "admin", Collections.emptyList()); system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(user)); loadPayloads(); diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/Pkcs12HttpsServerTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/Pkcs12HttpsServerTest.java index ed5929b72..ad5f50cda 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/Pkcs12HttpsServerTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/Pkcs12HttpsServerTest.java @@ -65,7 +65,7 @@ class Pkcs12HttpsServerTest implements HttpsServerTest { system.enable(); - User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), 0, Collections.emptyList()); + User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), "admin", Collections.emptyList()); system.getDatabaseSystem().getDatabase().executeTransaction(new StoreWebUserTransaction(user)); } 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 e37306edb..99ab15dd0 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 @@ -16,7 +16,6 @@ */ package com.djrapitops.plan.delivery.webserver.auth; -import com.djrapitops.plan.delivery.domain.WebUser; import com.djrapitops.plan.delivery.domain.auth.User; import com.djrapitops.plan.processing.Processing; import com.djrapitops.plan.settings.config.PlanConfig; @@ -30,6 +29,8 @@ import org.mockito.Mockito; import utilities.TestConstants; import utilities.TestPluginLogger; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.when; @@ -51,7 +52,7 @@ class ActiveCookieStoreTest { dbSystem, Mockito.mock(Processing.class), new TestPluginLogger()); - user = new User(TestConstants.PLAYER_ONE_NAME, "console", null, PassEncryptUtil.createHash("testPass"), 0, WebUser.getPermissionsForLevel(0)); + user = new User(TestConstants.PLAYER_ONE_NAME, "console", null, PassEncryptUtil.createHash("testPass"), "admin", List.of("page", "access")); } @AfterEach diff --git a/Plan/common/src/test/java/com/djrapitops/plan/settings/locale/LocaleSystemTest.java b/Plan/common/src/test/java/com/djrapitops/plan/settings/locale/LocaleSystemTest.java index f0a722836..05215eaa2 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/settings/locale/LocaleSystemTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/settings/locale/LocaleSystemTest.java @@ -18,7 +18,12 @@ package com.djrapitops.plan.settings.locale; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; class LocaleSystemTest { @@ -31,4 +36,18 @@ class LocaleSystemTest { void noIdentifierCollisions() { assertDoesNotThrow(LocaleSystem::getIdentifiers); } + + @Test + void noIdentifierParentValues() { + Set keys = LocaleSystem.getKeys().keySet(); + List invalidParentKeys = new ArrayList<>(); + for (String key : keys) { + for (String key2 : keys) { + if (!key.equals(key2) && key.contains(key2) && key.replace(key2, "").startsWith(".")) { + invalidParentKeys.add("'" + key2 + "' is the parent of '" + key + "' but has a value\n"); + } + } + } + assertTrue(invalidParentKeys.isEmpty(), invalidParentKeys::toString); + } } \ No newline at end of file diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/DatabaseBackupTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/DatabaseBackupTest.java index adbfd1f1f..1f4017b1c 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/DatabaseBackupTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/DatabaseBackupTest.java @@ -69,7 +69,7 @@ public interface DatabaseBackupTest extends DatabaseTestPreparer { Collections.singletonList(new DateObj<>(System.currentTimeMillis(), RandomData.randomInt(-1, 40)))) ); - User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), 0, Collections.emptyList()); + User user = new User("test", "console", null, PassEncryptUtil.createHash("testPass"), "admin", Collections.emptyList()); db().executeTransaction(new StoreWebUserTransaction(user)); } @@ -95,6 +95,8 @@ public interface DatabaseBackupTest extends DatabaseTestPreparer { assertQueryResultIsEqual(db(), backup, LargeFetchQueries.fetchAllTPSData()); assertQueryResultIsEqual(db(), backup, ServerQueries.fetchPlanServerInformation()); assertQueryResultIsEqual(db(), backup, WebUserQueries.fetchAllUsers()); + assertQueryResultIsEqual(db(), backup, WebUserQueries.fetchGroupNames()); + assertQueryResultIsEqual(db(), backup, WebUserQueries.fetchAvailablePermissions()); } finally { backup.close(); diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/PingQueriesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/PingQueriesTest.java index 21df9b0ec..df5c94b4a 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/PingQueriesTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/PingQueriesTest.java @@ -44,12 +44,12 @@ public interface PingQueriesTest extends DatabaseTestPreparer { } @Test - default void pingStoreTransactionOutOfOrderDoesNotFailDueToMissingUser() { + default void pingStoreTransactionOutOfOrderDoesNotFailDueToMissingUser() throws ExecutionException, InterruptedException { DateObj saved = RandomData.randomIntDateObject(); int value = saved.getValue(); db().executeTransaction(new PingStoreTransaction(player2UUID, serverUUID(), Collections.singletonList(saved) - )); + )).get(); Map> expected = Collections.singletonMap(player2UUID, Collections.singletonList( new Ping(saved.getDate(), serverUUID(), value, value, value) @@ -59,12 +59,12 @@ public interface PingQueriesTest extends DatabaseTestPreparer { } @Test - default void pingStoreTransactionOutOfOrderUpdatesUserInformation() { + default void pingStoreTransactionOutOfOrderUpdatesUserInformation() throws ExecutionException, InterruptedException { db().executeTransaction(new PingStoreTransaction(player2UUID, serverUUID(), Collections.singletonList(RandomData.randomIntDateObject()) - )); + )).get(); long registerDate = RandomData.randomTime(); - db().executeTransaction(new PlayerRegisterTransaction(player2UUID, () -> registerDate, TestConstants.PLAYER_ONE_NAME)); + db().executeTransaction(new PlayerRegisterTransaction(player2UUID, () -> registerDate, TestConstants.PLAYER_ONE_NAME)).get(); Optional expected = Optional.of(new BaseUser(player2UUID, TestConstants.PLAYER_ONE_NAME, registerDate, 0)); Optional result = db().query(BaseUserQueries.fetchBaseUserOfPlayer(player2UUID)); 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 2993c1ef5..760326807 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 @@ -16,36 +16,46 @@ */ package com.djrapitops.plan.storage.database.queries; -import com.djrapitops.plan.delivery.domain.WebUser; import com.djrapitops.plan.delivery.domain.auth.User; +import com.djrapitops.plan.delivery.domain.auth.WebPermission; import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask; import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore; import com.djrapitops.plan.processing.Processing; import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.storage.database.DatabaseTestPreparer; import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries; +import com.djrapitops.plan.storage.database.transactions.DeleteWebGroupTransaction; +import com.djrapitops.plan.storage.database.transactions.GrantWebPermissionToGroupsWithPermissionTransaction; +import com.djrapitops.plan.storage.database.transactions.StoreMissingWebPermissionsTransaction; +import com.djrapitops.plan.storage.database.transactions.StoreWebGroupTransaction; import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction; import com.djrapitops.plan.storage.database.transactions.commands.RemoveWebUserTransaction; import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction; +import com.djrapitops.plan.storage.database.transactions.patches.WebGroupDefaultGroupsPatch; import com.djrapitops.plan.utilities.PassEncryptUtil; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import utilities.TestConstants; +import utilities.TestErrorLogger; import utilities.TestPluginLogger; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; public interface WebUserQueriesTest extends DatabaseTestPreparer { String WEB_USERNAME = TestConstants.PLAYER_ONE_NAME; + String GROUP_NAME = "test_group"; @Test + @DisplayName("Web user with group 'admin' is registered") default void userIsRegistered() { - User expected = new User(WEB_USERNAME, "console", null, PassEncryptUtil.createHash("testPass"), 0, WebUser.getPermissionsForLevel(0)); + executeTransactions(new WebGroupDefaultGroupsPatch()); + executeTransactions(new StoreWebGroupTransaction("admin", List.of("page", "access", "manage.groups", "manage.users"))); + User expected = new User(WEB_USERNAME, "console", null, PassEncryptUtil.createHash("testPass"), "admin", + new HashSet<>(Arrays.asList("page", "access", "manage.groups", "manage.users"))); db().executeTransaction(new StoreWebUserTransaction(expected)); forcePersistenceCheck(); @@ -55,12 +65,18 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { } @Test + @DisplayName("WebUserQueries fetchAllUsers finds multiple users") default void multipleWebUsersAreFetchedAppropriately() { userIsRegistered(); - assertEquals(1, db().query(WebUserQueries.fetchAllUsers()).size()); + User secondUser = new User("2nd-user", "console", null, PassEncryptUtil.createHash("testPass"), "admin", + new HashSet<>(Arrays.asList("page", "access", "manage.groups", "manage.users"))); + db().executeTransaction(new StoreWebUserTransaction(secondUser)); + + assertEquals(2, db().query(WebUserQueries.fetchAllUsers()).size()); } @Test + @DisplayName("RemoveWebUserTransaction deletes user from database") default void webUserIsRemoved() { userIsRegistered(); db().executeTransaction(new RemoveWebUserTransaction(WEB_USERNAME)); @@ -68,6 +84,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { } @Test + @DisplayName("RemoveEverythingTransaction deletes user from database") default void removeEverythingRemovesWebUser() { userIsRegistered(); db().executeTransaction(new RemoveEverythingTransaction()); @@ -75,6 +92,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { } @Test + @DisplayName("ActiveCookieStore stores cookies it generates in database") default void activeCookieStoreSavesCookies() { userIsRegistered(); User user = db().query(WebUserQueries.fetchUser(WEB_USERNAME)).orElseThrow(AssertionError::new); @@ -89,6 +107,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { } @Test + @DisplayName("ActiveCookieStore deletes outdated cookies in database") default void activeCookieStoreDeletesCookies() { userIsRegistered(); User user = db().query(WebUserQueries.fetchUser(WEB_USERNAME)).orElseThrow(AssertionError::new); @@ -103,6 +122,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { } @Test + @DisplayName("RemoveWebUserTransaction deletes cookies in database") default void webUserRemovalDeletesCookies() { userIsRegistered(); User user = db().query(WebUserQueries.fetchUser(WEB_USERNAME)).orElseThrow(AssertionError::new); @@ -128,9 +148,138 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer { } @Test + @DisplayName("RemoveEverythingTransaction deletes cookies in database") default void removeEverythingRemovesCookies() { activeCookieStoreSavesCookies(); db().executeTransaction(new RemoveEverythingTransaction()); assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty()); } + + @Test + @DisplayName("Web group is stored in the database") + default void webGroupIsAdded() { + db().executeTransaction(new StoreWebGroupTransaction(GROUP_NAME, List.of(WebPermission.ACCESS.getPermission()))); + + List result = db().query(WebUserQueries.fetchGroupNames()); + assertTrue(result.contains(GROUP_NAME), () -> GROUP_NAME + " not found from db: " + result); + + List permissionExpected = List.of(WebPermission.ACCESS.getPermission()); + List permissionResult = db().query(WebUserQueries.fetchGroupPermissions(GROUP_NAME)); + assertEquals(permissionExpected, permissionResult); + } + + @Test + @DisplayName("Web group's permissions is stored in the database") + default void webGroupPermissionsAreStored() { + db().executeTransaction(new StoreWebGroupTransaction(GROUP_NAME, List.of(WebPermission.ACCESS.getPermission()))); + + assertTrue(db().query(WebUserQueries.fetchGroupId(GROUP_NAME)).isPresent()); + + List permissionExpected = List.of(WebPermission.ACCESS.getPermission()); + List permissionResult = db().query(WebUserQueries.fetchGroupPermissions(GROUP_NAME)); + assertEquals(permissionExpected, permissionResult); + } + + @Test + @DisplayName("WebUserQueries fetchGroupNamesWithPermission finds group with specific permission") + default void webGroupIsFoundByPermission() { + webGroupPermissionsAreStored(); + + List groupNames = db().query(WebUserQueries.fetchGroupNamesWithPermission(WebPermission.ACCESS.getPermission())); + assertTrue(groupNames.contains(GROUP_NAME)); + } + + @Test + @DisplayName("Web group's never seen permission 'test.permission' is stored in the database") + default void customWebPermissionsAreStored() throws Exception { + String customPermission = "test.permission"; + db().executeTransaction(new StoreWebGroupTransaction(GROUP_NAME, List.of(customPermission))).get(); + + List permissionExpected = List.of(customPermission); + List permissionResult = db().query(WebUserQueries.fetchGroupPermissions(GROUP_NAME)); + assertEquals(permissionExpected, permissionResult); + + assertTrue(db().query(WebUserQueries.fetchPermissionId(customPermission)).isPresent()); + assertFalse(db().query(WebUserQueries.fetchPermissionIds(List.of(customPermission))).isEmpty()); + } + + @Test + @DisplayName("StoreWebUserTransaction updates user's group") + default void webUserGroupIsChanged() throws Exception { + userIsRegistered(); + webGroupIsAdded(); + + String passHash = db().query(WebUserQueries.fetchUser(WEB_USERNAME)).orElseThrow(AssertionError::new).getPasswordHash(); + // Changes web group + User expected = new User(WEB_USERNAME, "console", null, passHash, GROUP_NAME, Set.of("access")); + db().executeTransaction(new StoreWebUserTransaction(expected)).get(); + + User stored = db().query(WebUserQueries.fetchUser(WEB_USERNAME)).orElseThrow(AssertionError::new); + + assertEquals(expected, stored); + } + + @Test + @DisplayName("GrantWebPermissionToGroupsWithPermissionTransaction gives permissions to existing groups") + default void grantNewWebPermissions() throws Exception { + db().executeTransaction(new WebGroupDefaultGroupsPatch()); + db().executeTransaction(new StoreMissingWebPermissionsTransaction(List.of("grant.permission"))).get(); + db().executeTransaction(new GrantWebPermissionToGroupsWithPermissionTransaction("grant.permission", WebPermission.MANAGE_GROUPS.getPermission())).get(); + + assertTrue(db().query(WebUserQueries.fetchPermissionId("grant.permission")).isPresent()); + + List expected = List.of("admin"); + List groups = db().query(WebUserQueries.fetchGroupNamesWithPermission("grant.permission")); + assertEquals(expected, groups); + } + + @Test + @DisplayName("DeleteWebGroupTransaction deletes a group") + default void webGroupIsDeleted() { + db().executeTransaction(new WebGroupDefaultGroupsPatch()); + webGroupIsAdded(); + + db().executeTransaction(new DeleteWebGroupTransaction(GROUP_NAME, "no_access")); + + assertTrue(db().query(WebUserQueries.fetchGroupId(GROUP_NAME)).isEmpty()); + } + + @Test + @DisplayName("DeleteWebGroupTransaction does not delete a group if moveTo group doesn't exist") + default void webGroupIsNotDeletedWhenMoveToGroupDoesNotExist() { + try { + TestErrorLogger.throwErrors(false); + webGroupIsAdded(); + + db().executeTransaction(new DeleteWebGroupTransaction(GROUP_NAME, "no_access")); + Throwable exception = TestErrorLogger.getLatest().orElseThrow(AssertionError::new); + assertEquals("com.djrapitops.plan.exceptions.database.DBOpException: Group not found for given name", exception.getMessage()); + + assertTrue(db().query(WebUserQueries.fetchGroupId(GROUP_NAME)).isPresent()); + } finally { + TestErrorLogger.throwErrors(true); + } + } + + @Test + @DisplayName("RemoveEverythingTransaction deletes all groups") + default void removeEverythingRemovesAllGroups() { + webGroupIsAdded(); + + db().executeTransaction(new RemoveEverythingTransaction()); + + assertTrue(db().query(WebUserQueries.fetchGroupId(GROUP_NAME)).isEmpty()); + assertTrue(db().query(WebUserQueries.fetchGroupId("admin")).isEmpty()); + } + + @Test + @DisplayName("RemoveEverythingTransaction deletes all permissions") + default void removeEverythingRemovesAllPermissions() throws Exception { + customWebPermissionsAreStored(); + + db().executeTransaction(new RemoveEverythingTransaction()).get(); + + assertTrue(db().query(WebUserQueries.fetchPermissionId("test.permission")).isEmpty()); + assertTrue(db().query(WebUserQueries.fetchPermissionId(WebPermission.ACCESS.getPermission())).isEmpty()); + } } \ No newline at end of file diff --git a/Plan/common/src/test/java/extension/FullSystemExtension.java b/Plan/common/src/test/java/extension/FullSystemExtension.java index 276f2868b..597b6e40a 100644 --- a/Plan/common/src/test/java/extension/FullSystemExtension.java +++ b/Plan/common/src/test/java/extension/FullSystemExtension.java @@ -20,6 +20,7 @@ import com.djrapitops.plan.PlanSystem; import com.djrapitops.plan.commands.PlanCommand; import com.djrapitops.plan.delivery.DeliveryUtilities; import com.djrapitops.plan.delivery.export.Exporter; +import com.djrapitops.plan.delivery.webserver.Addresses; import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.paths.WebserverSettings; @@ -76,6 +77,7 @@ public class FullSystemExtension implements ParameterResolver, BeforeAllCallback }) .put(Database.class, () -> planSystem.getDatabaseSystem().getDatabase()) .put(DeliveryUtilities.class, () -> planSystem.getDeliveryUtilities()) + .put(Addresses.class, () -> planSystem.getDeliveryUtilities().getAddresses()) .put(PublicHtmlFiles.class, () -> planSystem.getDeliveryUtilities().getPublicHtmlFiles()) .put(Webserver.class, () -> planSystem.getWebServerSystem().getWebServer()) .put(Exporter.class, () -> planSystem.getExportSystem().getExporter()) diff --git a/Plan/common/src/test/java/extension/SeleniumExtension.java b/Plan/common/src/test/java/extension/SeleniumExtension.java index 110297f18..35ace4992 100644 --- a/Plan/common/src/test/java/extension/SeleniumExtension.java +++ b/Plan/common/src/test/java/extension/SeleniumExtension.java @@ -19,18 +19,17 @@ package extension; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.extension.*; -import org.openqa.selenium.By; -import org.openqa.selenium.Keys; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.*; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.logging.LogType; import org.openqa.selenium.logging.LoggingPreferences; +import org.testcontainers.shaded.org.awaitility.Awaitility; import utilities.CIProperties; import java.io.File; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; /** @@ -48,6 +47,20 @@ public class SeleniumExtension implements ParameterResolver, BeforeAllCallback, driver.switchTo().window(new ArrayList<>(driver.getWindowHandles()).get(0)); } + public static void waitForPageLoadForSeconds(int i, ChromeDriver driver) { + Awaitility.await("waitForPageLoadForSeconds") + .atMost(5, TimeUnit.SECONDS) + .until(() -> "complete".equals(driver.executeScript("return document.readyState"))); + } + + public static void waitForElementToBeVisible(By by, ChromeDriver driver) { + SeleniumExtension.waitForPageLoadForSeconds(5, driver); + Awaitility.await("waitForElementToBeVisible " + by.toString()) + .atMost(5, TimeUnit.SECONDS) + .ignoreException(NoSuchElementException.class) + .until(() -> driver.findElement(by).isDisplayed()); + } + @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { final Class type = parameterContext.getParameter().getType(); diff --git a/Plan/common/src/test/java/utilities/TestErrorLogger.java b/Plan/common/src/test/java/utilities/TestErrorLogger.java index 7f9ac5f0c..19c0f1425 100644 --- a/Plan/common/src/test/java/utilities/TestErrorLogger.java +++ b/Plan/common/src/test/java/utilities/TestErrorLogger.java @@ -21,6 +21,7 @@ import com.djrapitops.plan.utilities.logging.ErrorLogger; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,6 +39,11 @@ public class TestErrorLogger implements ErrorLogger { return caught; } + public static Optional getLatest() { + if (caught.isEmpty()) return Optional.empty(); + return Optional.of(caught.get(caught.size() - 1)); + } + @Override public void critical(Throwable throwable, ErrorContext context) { System.out.println("[CRITICAL] Exception occurred during test, context: " + context); diff --git a/Plan/config/checkstyle/checkstyle.xml b/Plan/config/checkstyle/checkstyle.xml index 812c2fe13..2462cd6b1 100644 --- a/Plan/config/checkstyle/checkstyle.xml +++ b/Plan/config/checkstyle/checkstyle.xml @@ -76,8 +76,8 @@ - - + + diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 564fbbc27..58fc7235e 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -17,6 +17,7 @@ import MainPageRedirect from "./components/navigation/MainPageRedirect"; import {baseAddress, staticSite} from "./service/backendConfiguration"; import {PageExtensionContextProvider} from "./hooks/pageExtensionHook"; import ErrorBoundary from "./components/ErrorBoundary"; +import {AlertPopupContextProvider} from "./hooks/context/alertPopupContext"; const PlayerPage = React.lazy(() => import("./views/layout/PlayerPage")); const PlayerOverview = React.lazy(() => import("./views/player/PlayerOverview")); @@ -56,6 +57,9 @@ const QueryPage = React.lazy(() => import("./views/layout/QueryPage")); const NewQueryView = React.lazy(() => import("./views/query/NewQueryView")); const QueryResultView = React.lazy(() => import("./views/query/QueryResultView")); +const ManagePage = React.lazy(() => import("./views/layout/ManagePage")); +const GroupsView = React.lazy(() => import("./views/manage/GroupsView")); + const LoginPage = React.lazy(() => import("./views/layout/LoginPage")); const RegisterPage = React.lazy(() => import("./views/layout/RegisterPage")); const ErrorPage = React.lazy(() => import("./views/layout/ErrorPage")); @@ -68,15 +72,20 @@ const OverviewRedirect = () => { const NewRedirect = () => { return () } +const GroupsRedirect = () => { + return () +} const ContextProviders = ({children}) => ( - - {children} - + + + {children} + + @@ -179,6 +188,10 @@ function App() { icon: faMapSigns }}/>}/> + {!staticSite && }> + }/> + }/> + } {!staticSite && }> }/> }/> diff --git a/Plan/react/dashboard/src/components/CardTabs.js b/Plan/react/dashboard/src/components/CardTabs.js index 50e786a90..42be965ab 100644 --- a/Plan/react/dashboard/src/components/CardTabs.js +++ b/Plan/react/dashboard/src/components/CardTabs.js @@ -2,10 +2,10 @@ import React, {useEffect, useState} from "react"; import {useLocation, useNavigate} from "react-router-dom"; import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; -const TabButton = ({name, href, icon, color, active}) => { +const TabButton = ({id, name, href, icon, color, active}) => { const navigate = useNavigate(); return ( -
  • +
  • + + ) +} + +const AlertPopupArea = () => { + const {alerts} = useAlertPopupContext(); + + return ( +
    + {alerts.map(alert => )} +
    + ) +}; + +export default AlertPopupArea \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/calendar/ServerCalendar.js b/Plan/react/dashboard/src/components/calendar/ServerCalendar.js index fa0bf34a4..9fb98a0d6 100644 --- a/Plan/react/dashboard/src/components/calendar/ServerCalendar.js +++ b/Plan/react/dashboard/src/components/calendar/ServerCalendar.js @@ -4,24 +4,26 @@ import dayGridPlugin from '@fullcalendar/daygrid' const ServerCalendar = ({series, firstDay}) => { return ( - successCallback(series)} - /> +
    + successCallback(series)} + /> +
    ) } diff --git a/Plan/react/dashboard/src/components/cards/CardHeader.js b/Plan/react/dashboard/src/components/cards/CardHeader.js index f7d3b9cb9..3713fd977 100644 --- a/Plan/react/dashboard/src/components/cards/CardHeader.js +++ b/Plan/react/dashboard/src/components/cards/CardHeader.js @@ -9,7 +9,7 @@ const CardHeader = ({icon, color, label, children}) => { return (
    - {t(label)} + {label.length ? t(label) : label} {children}
    diff --git a/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js b/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js index 4b4540074..5c2a781e1 100644 --- a/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js +++ b/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js @@ -47,7 +47,7 @@ const GeolocationsCard = ({data}) => { } return ( - +
    {t('html.label.geolocations')} diff --git a/Plan/react/dashboard/src/components/cards/common/InsightsFor30DaysCard.js b/Plan/react/dashboard/src/components/cards/common/InsightsFor30DaysCard.js index 7a0418143..eb3a98f8b 100644 --- a/Plan/react/dashboard/src/components/cards/common/InsightsFor30DaysCard.js +++ b/Plan/react/dashboard/src/components/cards/common/InsightsFor30DaysCard.js @@ -4,10 +4,10 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import React from "react"; import {faLifeRing} from "@fortawesome/free-regular-svg-icons"; -const InsightsFor30DaysCard = ({children}) => { +const InsightsFor30DaysCard = ({id, children}) => { const {t} = useTranslation(); return ( - +
    {t('html.label.insights30days')} diff --git a/Plan/react/dashboard/src/components/cards/common/PingTableCard.js b/Plan/react/dashboard/src/components/cards/common/PingTableCard.js index 29305e39d..d91485d63 100644 --- a/Plan/react/dashboard/src/components/cards/common/PingTableCard.js +++ b/Plan/react/dashboard/src/components/cards/common/PingTableCard.js @@ -6,7 +6,7 @@ import PingTable from "../../table/PingTable"; const PingTableCard = ({data}) => { return ( - + diff --git a/Plan/react/dashboard/src/components/cards/common/PvpKillsTableCard.js b/Plan/react/dashboard/src/components/cards/common/PvpKillsTableCard.js index 1dd2eb2f7..54d1e6606 100644 --- a/Plan/react/dashboard/src/components/cards/common/PvpKillsTableCard.js +++ b/Plan/react/dashboard/src/components/cards/common/PvpKillsTableCard.js @@ -12,7 +12,7 @@ const PvpKillsTableCard = ({player_kills}) => { if (!player_kills) return ; return ( - +
    {t('html.label.recentPvpKills')} diff --git a/Plan/react/dashboard/src/components/cards/common/RecentSessionsCard.js b/Plan/react/dashboard/src/components/cards/common/RecentSessionsCard.js index eced43025..fcdbcf877 100644 --- a/Plan/react/dashboard/src/components/cards/common/RecentSessionsCard.js +++ b/Plan/react/dashboard/src/components/cards/common/RecentSessionsCard.js @@ -6,10 +6,10 @@ import Scrollable from "../../Scrollable"; import SessionAccordion from "../../accordion/SessionAccordion"; import React from "react"; -const RecentSessionsCard = ({sessions, isPlayer, isNetwork}) => { +const RecentSessionsCard = ({id, sessions, isPlayer, isNetwork}) => { const {t} = useTranslation(); return ( - +
    {t('html.label.recentSessions')} diff --git a/Plan/react/dashboard/src/components/cards/player/ConnectionsCard.js b/Plan/react/dashboard/src/components/cards/player/ConnectionsCard.js new file mode 100644 index 000000000..157c438c9 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/player/ConnectionsCard.js @@ -0,0 +1,45 @@ +import React from 'react'; +import {useTranslation} from "react-i18next"; +import {useTheme} from "../../../hooks/themeHook"; +import {Card} from "react-bootstrap"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faGlobe, faWifi} from "@fortawesome/free-solid-svg-icons"; +import Scrollable from "../../Scrollable"; +import {faClock} from "@fortawesome/free-regular-svg-icons"; + +const ConnectionsCard = ({connections}) => { + const {t} = useTranslation(); + const {nightModeEnabled} = useTheme(); + return ( + + +
    + {t('html.label.connectionInfo')} +
    +
    + + + + + + + + + {Boolean(connections?.length) && + {connections.map((connection, i) => ( + + + ))} + } + {!connections?.length && + + + + } +
    {t('html.label.country')} {t('html.label.lastConnected')}
    {connection.geolocation}{connection.date}
    {t('generic.noData')}
    +
    +
    + ) +} + +export default ConnectionsCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/player/NicknamesCard.js b/Plan/react/dashboard/src/components/cards/player/NicknamesCard.js index 518378649..bf4dc0613 100644 --- a/Plan/react/dashboard/src/components/cards/player/NicknamesCard.js +++ b/Plan/react/dashboard/src/components/cards/player/NicknamesCard.js @@ -7,7 +7,7 @@ import Scrollable from "../../Scrollable"; import {faClock} from "@fortawesome/free-regular-svg-icons"; import React from "react"; -const NicknamesCard = ({player}) => { +const NicknamesCard = ({nicknames}) => { const {t} = useTranslation(); const {nightModeEnabled} = useTheme(); return ( @@ -26,13 +26,18 @@ const NicknamesCard = ({player}) => { {t('html.label.lastSeen')} - - {player.nicknames.map(nickname => ( + {Boolean(nicknames?.length) && + {nicknames.map(nickname => ( {nickname.server} {nickname.date} ))} - + } + {!nicknames?.length && + + {t('generic.noData')} + + } diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js index 6ca7ed012..62d46d53f 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js @@ -12,7 +12,7 @@ import GroupVisualizer from "../../../graphs/GroupVisualizer"; export const CurrentPlayerbaseCardWithData = ({data, title}) => { const {t} = useTranslation(); return ( - +
    {t(title ? title : 'html.label.currentPlayerbase')} diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js index 31c9f5eed..4bb282f8f 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js @@ -18,7 +18,7 @@ const JoinAddressGroupCard = ({identifier}) => { if (!data) return ; return ( - +
    {t('html.label.latestJoinAddresses')} diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/NetworkOnlineActivityGraphsCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/NetworkOnlineActivityGraphsCard.js index 713c644a0..c3726c527 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/NetworkOnlineActivityGraphsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/NetworkOnlineActivityGraphsCard.js @@ -11,6 +11,7 @@ import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph"; import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph"; import {useMetadata} from "../../../../hooks/metadataHook"; import StackedPlayersOnlineGraph from "../../../graphs/StackedPlayersOnlineGraph"; +import {useAuth} from "../../../../hooks/authenticationHook"; const SingleProxyPlayersOnlineGraph = ({serverUUID}) => { const {data, loadingError} = useDataRequest(fetchPlayersOnlineGraph, [serverUUID]); @@ -48,7 +49,7 @@ const DayByDayTab = () => { if (loadingError) return if (!data) return ; - return + return } const HourByHourTab = () => { @@ -57,24 +58,29 @@ const HourByHourTab = () => { if (loadingError) return if (!data) return ; - return + return } const NetworkOnlineActivityGraphsCard = () => { + const {hasPermission} = useAuth(); const {t} = useTranslation(); + const tabs = [ + { + name: t('html.label.networkOnlineActivity'), icon: faChartArea, color: 'blue', href: 'online-activity', + element: , + permission: 'page.network.overview.graphs.online' + }, { + name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day', + element: , + permission: 'page.network.overview.graphs.day.by.day' + }, { + name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour', + element: , + permission: 'page.network.overview.graphs.hour.by.hour' + } + ].filter(tab => hasPermission(tab.permission)); return - - }, { - name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day', - element: - }, { - name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour', - element: - } - ]}/> + }; diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/OnlineActivityGraphsCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/OnlineActivityGraphsCard.js index e4fdfecdf..533446e90 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/OnlineActivityGraphsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/OnlineActivityGraphsCard.js @@ -17,6 +17,7 @@ import React from "react"; import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph"; import ServerCalendar from "../../../calendar/ServerCalendar"; import {ChartLoader} from "../../../navigation/Loader"; +import {useAuth} from "../../../../hooks/authenticationHook"; const DayByDayTab = () => { const {identifier} = useParams(); @@ -26,7 +27,7 @@ const DayByDayTab = () => { if (loadingError) return if (!data) return ; - return + return } const HourByHourTab = () => { @@ -37,7 +38,7 @@ const HourByHourTab = () => { if (loadingError) return if (!data) return ; - return + return } const ServerCalendarTab = () => { @@ -63,23 +64,29 @@ const PunchCardTab = () => { } const OnlineActivityGraphsCard = () => { + const {hasPermission} = useAuth(); const {t} = useTranslation(); - return - - }, { - name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour', - element: - }, { - name: t('html.label.serverCalendar'), icon: faCalendar, color: 'teal', href: 'server-calendar', - element: - }, { - name: t('html.label.punchcard30days'), icon: faBraille, color: 'black', href: 'punchcard', - element: - }, - ]}/> + const tabs = [ + { + name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day', + element: , + permission: 'page.server.online.activity.graphs.day.by.day' + }, { + name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour', + element: , + permission: 'page.server.online.activity.graphs.hour.by.hour' + }, { + name: t('html.label.serverCalendar'), icon: faCalendar, color: 'teal', href: 'server-calendar', + element: , + permission: 'page.server.online.activity.graphs.calendar' + }, { + name: t('html.label.punchcard30days'), icon: faBraille, color: 'black', href: 'punchcard', + element: , + permission: 'page.server.online.activity.graphs.punchcard' + }, + ].filter(tab => hasPermission(tab.permission)); + return + } diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js index 33d74ac5a..0a75a0d31 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js @@ -72,7 +72,7 @@ const PerformanceGraphsCard = () => { } }, [data, setParsedData]); - return + return { if (!data) return ; return ( - + }/> diff --git a/Plan/react/dashboard/src/components/cards/server/insights/PerformanceInsightsCard.js b/Plan/react/dashboard/src/components/cards/server/insights/PerformanceInsightsCard.js index ea456b78e..c5a21829e 100644 --- a/Plan/react/dashboard/src/components/cards/server/insights/PerformanceInsightsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/insights/PerformanceInsightsCard.js @@ -10,7 +10,7 @@ const PerformanceInsightsCard = ({data}) => { if (!data) return ; return ( - +

    {t('html.label.duringLowTps')}

    diff --git a/Plan/react/dashboard/src/components/cards/server/insights/PlayerbaseInsightsCard.js b/Plan/react/dashboard/src/components/cards/server/insights/PlayerbaseInsightsCard.js index 172a36069..099acf76c 100644 --- a/Plan/react/dashboard/src/components/cards/server/insights/PlayerbaseInsightsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/insights/PlayerbaseInsightsCard.js @@ -23,7 +23,7 @@ const PlayerbaseInsightsCard = ({data}) => { const {t} = useTranslation(); if (!data) return ; return ( - + { if (!data) return ; return ( - + { const insights = data?.insights; return ( - + { if (!data) return ; return ( - +
    {t('html.label.onlineActivityAsNumbers')} diff --git a/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js b/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js index 29672cf3d..2b3305144 100644 --- a/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js +++ b/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js @@ -17,8 +17,8 @@ const PerformanceAsNumbersCard = ({data, servers}) => { : '')); return ( - - + + {noDataAlert} diff --git a/Plan/react/dashboard/src/components/cards/server/tables/PlayerbaseTrendsCard.js b/Plan/react/dashboard/src/components/cards/server/tables/PlayerbaseTrendsCard.js index 10e0cb137..5537ce28c 100644 --- a/Plan/react/dashboard/src/components/cards/server/tables/PlayerbaseTrendsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/tables/PlayerbaseTrendsCard.js @@ -13,7 +13,7 @@ const PlayerbaseTrendsCard = ({data}) => { const {t} = useTranslation(); if (!data) return ; return ( - +
    {t('html.label.trends30days')} diff --git a/Plan/react/dashboard/src/components/cards/server/tables/PvpPveAsNumbersCard.js b/Plan/react/dashboard/src/components/cards/server/tables/PvpPveAsNumbersCard.js index a61d98c06..9113a426d 100644 --- a/Plan/react/dashboard/src/components/cards/server/tables/PvpPveAsNumbersCard.js +++ b/Plan/react/dashboard/src/components/cards/server/tables/PvpPveAsNumbersCard.js @@ -8,7 +8,7 @@ import ServerPvpPveAsNumbersTable from "../../../table/ServerPvpPveAsNumbersTabl const PvpPveAsNumbersCard = ({kill_data}) => { const {t} = useTranslation(); return ( - +
    {t('html.label.pvpPveAsNumbers')} diff --git a/Plan/react/dashboard/src/components/cards/server/tables/ServerWeekComparisonCard.js b/Plan/react/dashboard/src/components/cards/server/tables/ServerWeekComparisonCard.js index 85f542a67..7a8c5b680 100644 --- a/Plan/react/dashboard/src/components/cards/server/tables/ServerWeekComparisonCard.js +++ b/Plan/react/dashboard/src/components/cards/server/tables/ServerWeekComparisonCard.js @@ -13,7 +13,7 @@ const ServerWeekComparisonCard = ({data}) => { const {t} = useTranslation(); if (!data) return ; return ( - +
    {t('html.label.weekComparison')} diff --git a/Plan/react/dashboard/src/components/cards/server/values/ServerAsNumbersCard.js b/Plan/react/dashboard/src/components/cards/server/values/ServerAsNumbersCard.js index 79fdac2a3..af3b1b181 100644 --- a/Plan/react/dashboard/src/components/cards/server/values/ServerAsNumbersCard.js +++ b/Plan/react/dashboard/src/components/cards/server/values/ServerAsNumbersCard.js @@ -26,7 +26,7 @@ const ServerAsNumbersCard = ({data}) => { const isGameServer = data.player_kills !== undefined; const showPeaks = isGameServer || networkMetadata.usingRedisBungee || networkMetadata.servers.filter(server => server.proxy).length === 1; return ( - +
    {isGameServer ? t('html.label.serverAsNumberse') : t('html.label.networkAsNumbers')} diff --git a/Plan/react/dashboard/src/components/graphs/TimeByTimeGraph.js b/Plan/react/dashboard/src/components/graphs/TimeByTimeGraph.js index a291fcfeb..c530fc54c 100644 --- a/Plan/react/dashboard/src/components/graphs/TimeByTimeGraph.js +++ b/Plan/react/dashboard/src/components/graphs/TimeByTimeGraph.js @@ -5,7 +5,7 @@ import LineGraph from "./LineGraph"; import {useTheme} from "../../hooks/themeHook"; import {withReducedSaturation} from "../../util/colors"; -const TimeByTimeGraph = ({data}) => { +const TimeByTimeGraph = ({id, data}) => { const {t} = useTranslation(); const [series, setSeries] = useState([]); const {nightModeEnabled} = useTheme(); @@ -29,7 +29,7 @@ const TimeByTimeGraph = ({data}) => { }, [data, t, nightModeEnabled]) return ( - + ) } diff --git a/Plan/react/dashboard/src/components/input/Select.js b/Plan/react/dashboard/src/components/input/Select.js new file mode 100644 index 000000000..20b222d1b --- /dev/null +++ b/Plan/react/dashboard/src/components/input/Select.js @@ -0,0 +1,24 @@ +import React from 'react'; + +const MultiSelect = ({options, selectedIndex, setSelectedIndex}) => { + const handleChange = (event) => { + const renderedOptions = Object.values(event.target.selectedOptions) + .map(htmlElement => htmlElement.text) + .map(option => options.indexOf(option)); + setSelectedIndex(renderedOptions[0]); + } + + return ( + + ) +}; + +export default MultiSelect \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/input/Toggle.js b/Plan/react/dashboard/src/components/input/Toggle.js index e9239c029..3fee00fa1 100644 --- a/Plan/react/dashboard/src/components/input/Toggle.js +++ b/Plan/react/dashboard/src/components/input/Toggle.js @@ -1,6 +1,6 @@ import React, {useState} from 'react'; -const Toggle = ({children, value, onValueChange, color}) => { +const Toggle = ({children, value, onValueChange, color, inline}) => { const [renderTime] = useState(new Date().getTime()); const id = 'checkbox-' + renderTime; @@ -9,7 +9,7 @@ const Toggle = ({children, value, onValueChange, color}) => { } return ( -
    +
    diff --git a/Plan/react/dashboard/src/components/layout/OpaqueText.js b/Plan/react/dashboard/src/components/layout/OpaqueText.js new file mode 100644 index 000000000..020fe9474 --- /dev/null +++ b/Plan/react/dashboard/src/components/layout/OpaqueText.js @@ -0,0 +1,13 @@ +import React from 'react'; + +const OpaqueText = ({children, inline, floatEnd}) => { + if (inline) { + return {children} + } + + return ( +

    {children}

    + ) +}; + +export default OpaqueText \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/layout/SideNavTabs.js b/Plan/react/dashboard/src/components/layout/SideNavTabs.js new file mode 100644 index 000000000..6958f8c54 --- /dev/null +++ b/Plan/react/dashboard/src/components/layout/SideNavTabs.js @@ -0,0 +1,61 @@ +import React, {useState} from "react"; +import {Col, Row} from "react-bootstrap"; + + +const SliceHeader = ({i, open, onClick, slice}) => { + return ( +
  • + {slice.header} +
  • + ) +} + +const SliceBody = ({i, open, slice}) => { + return ( +
    + {slice.body} +
    + ) +} + +const SideNavTabs = ({slices, open}) => { + const [openSlice, setOpenSlice] = useState(open ? 0 : -1); + + return ( + <> + + +
      + {slices.length ? slices.map((slice, i) => ( + setOpenSlice(i)} + /> + )) :
    • No Data
    • } +
    + + + {slices.length ? slices.map((slice, i) => ( + + )) :

    No Data

    } + +
    + + ) +} + +export default SideNavTabs; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/modal/HelpModal.js b/Plan/react/dashboard/src/components/modal/HelpModal.js index 4b4ffb274..eda7f11e9 100644 --- a/Plan/react/dashboard/src/components/modal/HelpModal.js +++ b/Plan/react/dashboard/src/components/modal/HelpModal.js @@ -7,6 +7,7 @@ import ActivityIndexHelp from "./help/ActivityIndexHelp"; import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons"; import NewPlayerRetentionHelp from "./help/NewPlayerRetentionHelp"; import PlayerRetentionGraphHelp from "./help/PlayerRetentionGraphHelp"; +import GroupPermissionHelp from "./help/GroupPermissionHelp"; const HelpModal = () => { const {t} = useTranslation(); @@ -25,6 +26,10 @@ const HelpModal = () => { "player-retention-graph": { title: t('html.label.playerRetention'), body: + }, + "group-permissions": { + title: t('html.label.managePage.groupHeader'), + body: } } diff --git a/Plan/react/dashboard/src/components/modal/VersionInformationModal.js b/Plan/react/dashboard/src/components/modal/VersionInformationModal.js index 33dd6df2d..8c9f9243b 100644 --- a/Plan/react/dashboard/src/components/modal/VersionInformationModal.js +++ b/Plan/react/dashboard/src/components/modal/VersionInformationModal.js @@ -4,8 +4,6 @@ import {Modal} from "react-bootstrap"; import {faCheckCircle, faDownload} from "@fortawesome/free-solid-svg-icons"; import {useTranslation} from "react-i18next"; -// TODO translate - const UpdateAvailableModal = ({open, toggle, versionInfo}) => { const {t} = useTranslation(); return ( @@ -34,17 +32,19 @@ const UpdateAvailableModal = ({open, toggle, versionInfo}) => { ) } +/*eslint no-template-curly-in-string: "off"*/ const NewestVersionModal = ({open, toggle, versionInfo}) => { + const {t} = useTranslation(); return ( - You have version {versionInfo.currentVersion}. + {t('html.version.current').replace('${0}', versionInfo.currentVersion)}. diff --git a/Plan/react/dashboard/src/components/modal/help/GroupPermissionHelp.js b/Plan/react/dashboard/src/components/modal/help/GroupPermissionHelp.js new file mode 100644 index 000000000..28ab8b2c6 --- /dev/null +++ b/Plan/react/dashboard/src/components/modal/help/GroupPermissionHelp.js @@ -0,0 +1,55 @@ +import React from 'react'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; +import {useMetadata} from "../../../hooks/metadataHook"; +import {Trans} from "react-i18next"; + +const GroupPermissionHelp = () => { + const {mainCommand} = useMetadata(); + return ( +
    +

    +

    /{mainCommand} register, + permission: {"plan.webgroup.{group_name}"} + }}/>

    +

    /{mainCommand} setgroup {"{username} {group_name}"}}}/>

    +

    , + permission: manage.groups, + commands: /{mainCommand} reload + }}/>

    +
    +

    +

    page.network, permission2: page.network.overview}}/>

    +
    +

    +

    +
      +
    • + access, permission2: access.network}}/> +
    • +
    • page}}/>
    • +
    • access, + permission2: page.network.overview.numbers, + permission3: access.network + }}/>
    • +
    +
    +

    +

    +

    +
    +

    https://github.com/plan-player-analytics/Plan/wiki/Web-permissions + }}/>

    +
    + ) +}; + +export default GroupPermissionHelp \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/navigation/MainPageRedirect.js b/Plan/react/dashboard/src/components/navigation/MainPageRedirect.js index 6d9d4fd1b..11a6ac18f 100644 --- a/Plan/react/dashboard/src/components/navigation/MainPageRedirect.js +++ b/Plan/react/dashboard/src/components/navigation/MainPageRedirect.js @@ -51,7 +51,7 @@ const RedirectPlaceholder = () => { } const MainPageRedirect = () => { - const {authLoaded, authRequired, loggedIn, user} = useAuth(); + const {authLoaded, authRequired, loggedIn, user, hasPermission} = useAuth(); const {isProxy, serverName, serverUUID} = useMetadata(); if (staticSite) { @@ -67,15 +67,15 @@ const MainPageRedirect = () => { } const redirectBasedOnPermissions = () => { - if (isProxy && user.permissions.includes('page.network')) { + if (isProxy && hasPermission('access.network')) { return () - } else if (user.permissions.includes('page.server')) { + } else if (hasPermission('access.server.' + serverUUID)) { return () - } else if (user.permissions.includes('page.player.other')) { + } else if (hasPermission('access.player')) { return () - } else if (user.permissions.includes('page.player.self')) { - return () + } else if (hasPermission('access.player.self')) { + return () } }; diff --git a/Plan/react/dashboard/src/components/navigation/PageNavigationItem.js b/Plan/react/dashboard/src/components/navigation/PageNavigationItem.js index bdaeb137d..a7142ea67 100644 --- a/Plan/react/dashboard/src/components/navigation/PageNavigationItem.js +++ b/Plan/react/dashboard/src/components/navigation/PageNavigationItem.js @@ -11,23 +11,41 @@ const PageNavigationItem = ({page}) => { const {t} = useTranslation(); const navigate = useNavigate(); const location = useLocation(); - const {authRequired, loggedIn, user} = useAuth(); + const {authRequired, loggedIn, user, hasPermission} = useAuth(); const metadata = useMetadata(); const [currentPage, setCurrentPage] = useState(undefined); const [items, setItems] = useState([]); useEffect(() => { const networkMetadata = metadata?.networkMetadata; - if (networkMetadata && networkMetadata.servers) { + if (networkMetadata?.servers) { const hasProxy = networkMetadata.servers.filter(server => server.proxy).length let newItems = [ - {id: 'players', displayName: t('html.label.players'), href: "/players", permission: 'page.players'}, + {id: 'players', displayName: t('html.label.players'), href: "/players", permission: 'access.players'}, + { + id: 'manage', + displayName: t('html.label.manage'), + href: "/manage", + permission: 'manage.groups'//, 'manage.users'] + }, { id: 'query', displayName: t('html.query.title.text').replace('<', ''), href: "/query", - permission: 'page.players' + permission: 'access.query' + }, + { + id: 'errors', + displayName: t("html.label.errors"), + href: "/errors", + permission: 'access.errors' + }, + { + id: 'docs', + displayName: t("html.label.docs"), + href: "/docs", + permission: 'access.docs' }, ...networkMetadata.servers .filter(server => !server.proxy) @@ -36,7 +54,7 @@ const PageNavigationItem = ({page}) => { id: 'server-' + server.serverUUID, displayName: t('html.label.server') + ', ' + server.serverName, href: '/server/' + server.serverUUID, - permission: 'page.server' + permission: 'access.server.' + server.serverUUID } }) ]; @@ -46,19 +64,19 @@ const PageNavigationItem = ({page}) => { id: 'network', displayName: t('html.label.network'), href: "/network", - permission: 'page.network' + permission: 'access.network' }); } if (page) { newItems.unshift({id: 'page', displayName: page, href: location.pathname, permission: undefined}) } if (authRequired && loggedIn) { - newItems = newItems.filter(item => !item.permission || user.permissions.includes(item.permission)) + newItems = newItems.filter(item => !item.permission || hasPermission(item.permission)) } setItems(newItems); setCurrentPage(newItems.find(item => location.pathname.startsWith(item.href))?.id); } - }, [t, metadata, location, authRequired, loggedIn, user, page]); + }, [t, metadata, location, authRequired, loggedIn, user, hasPermission, page]); const getSharedPrefix = (one, two) => { let i = 0; diff --git a/Plan/react/dashboard/src/components/navigation/Sidebar.js b/Plan/react/dashboard/src/components/navigation/Sidebar.js index 442166cfb..f4adfa510 100644 --- a/Plan/react/dashboard/src/components/navigation/Sidebar.js +++ b/Plan/react/dashboard/src/components/navigation/Sidebar.js @@ -117,7 +117,7 @@ const FooterButtons = ({collapseSidebar, toggleInfoModal, toggleVersionModal, ve {authRequired ? - Logout + {t('html.login.logout')} : ''}
    @@ -191,6 +191,7 @@ const renderItem = (item, i, openCollapse, setOpenCollapse, t, windowWidth, coll const Sidebar = ({page, items}) => { const {t} = useTranslation(); const {currentTab, sidebarExpanded, setSidebarExpanded} = useNavigation(); + const {authRequired, hasPermission, hasChildPermission} = useAuth(); const [openCollapse, setOpenCollapse] = useState(undefined); const toggleCollapse = collapse => { @@ -242,6 +243,10 @@ const Sidebar = ({page, items}) => { loadVersion(); }, []); + const isVisible = (item) => { + return !authRequired || !item.permission || hasPermission(item.permission) || hasChildPermission(item.permission) + } + return ( <> {sidebarExpanded && @@ -249,7 +254,9 @@ const Sidebar = ({page, items}) => { - {items.length ? items.filter(item => item !== undefined).map((item, i) => renderItem(item, i, openCollapse, toggleCollapse, t, windowWidth, collapseConditionallyOnItemClick)) : ''} + {items.length ? items.filter(item => item !== undefined) + .filter(isVisible) + .map((item, i) => renderItem(item, i, openCollapse, toggleCollapse, t, windowWidth, collapseConditionallyOnItemClick)) : ''} { }, []) const hasPermission = useCallback(permission => { - return !authRequired || (loggedIn && user && user.permissions.filter(perm => perm === permission).length); + if (Array.isArray(permission)) { + for (const permissionOption of permission) { + if (hasPermission(permissionOption)) { + return true; + } + } + return false; + } + return !authRequired || (loggedIn && user && Boolean(user.permissions.filter(perm => permission.includes(perm)).length)); + }, [authRequired, loggedIn, user]); + + const hasChildPermission = useCallback(permission => { + if (Array.isArray(permission)) { + for (const permissionOption of permission) { + if (hasChildPermission(permissionOption)) { + return true; + } + } + return false; + } + return !authRequired || (loggedIn && user && Boolean(user.permissions.filter(perm => perm.includes(permission) || permission.includes(perm)).length)); }, [authRequired, loggedIn, user]); const hasPermissionOtherThan = useCallback(permission => { @@ -43,6 +63,7 @@ export const AuthenticationContextProvider = ({children}) => { user, loginError, hasPermission, + hasChildPermission, hasPermissionOtherThan, updateLoginDetails } @@ -53,6 +74,7 @@ export const AuthenticationContextProvider = ({children}) => { user, loginError, hasPermission, + hasChildPermission, hasPermissionOtherThan, updateLoginDetails ]) diff --git a/Plan/react/dashboard/src/hooks/context/alertPopupContext.js b/Plan/react/dashboard/src/hooks/context/alertPopupContext.js new file mode 100644 index 000000000..131e57e9d --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/alertPopupContext.js @@ -0,0 +1,32 @@ +import {createContext, useCallback, useContext, useMemo, useState} from "react"; + +const AlertPopupContext = createContext({}); + +export const AlertPopupContextProvider = ({children}) => { + const [alerts, setAlerts] = useState([]); + + const dismissAlert = useCallback(alert => { + setAlerts(alerts.filter(a => a.time - alert.time < 1)); + }, [alerts, setAlerts]); + + const addAlert = useCallback(alert => { + const time = Date.now() + Math.random(); + const addedAlert = {time, ...alert}; + setAlerts([...alerts, addedAlert]); + setTimeout(() => { + dismissAlert(addedAlert); + }, alert.timeout || 5000) + }, [alerts, setAlerts, dismissAlert]); + + const sharedState = useMemo(() => { + return {alerts, addAlert, dismissAlert}; + }, [alerts, addAlert, dismissAlert]); + return ( + {children} + + ) +} + +export const useAlertPopupContext = () => { + return useContext(AlertPopupContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/context/configurationStorageContextHook.js b/Plan/react/dashboard/src/hooks/context/configurationStorageContextHook.js new file mode 100644 index 000000000..4a1c6dae2 --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/configurationStorageContextHook.js @@ -0,0 +1,60 @@ +import React, {createContext, useCallback, useContext, useMemo, useState} from "react"; + +const ConfigurationStorageContext = createContext({}); + +/** + * This context provides a way to independently manage state and its storage, while managing save and discard together. + * + * @returns {JSX.Element} + * @constructor + */ +export const ConfigurationStorageContextProvider = ({children}) => { + const [saveRequested, setSaveRequested] = useState(0); + const [discardRequested, setDiscardRequested] = useState(0); + const [dirty, setDirty] = useState(false); + + const requestSave = useCallback(() => { + if (dirty) { + setSaveRequested(Date.now()); + setDirty(false); + } + }, [setSaveRequested, dirty, setDirty]); + + const requestDiscard = useCallback(() => { + if (dirty) { + setDiscardRequested(Date.now()); + setDirty(false); + } + }, [setDiscardRequested, dirty, setDirty]); + + const markDirty = useCallback(() => { + setDirty(true); + }, [setDirty]) + + const sharedState = useMemo(() => { + return { + dirty, + saveRequested, + discardRequested, + requestSave, + requestDiscard, + markDirty + } + }, [ + dirty, + saveRequested, + discardRequested, + requestSave, + requestDiscard, + markDirty + ]) + return ( + + {children} + + ) +} + +export const useConfigurationStorageContext = () => { + return useContext(ConfigurationStorageContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/context/dropdownStatusContextHook.js b/Plan/react/dashboard/src/hooks/context/dropdownStatusContextHook.js new file mode 100644 index 000000000..33003a16f --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/dropdownStatusContextHook.js @@ -0,0 +1,27 @@ +import {createContext, useCallback, useContext, useMemo, useState} from "react"; + +const DropdownStatusContext = createContext({}); + +export const DropdownStatusContextProvider = ({children}) => { + const [toggled, setToggled] = useState([]); + + const toggle = useCallback(key => { + if (toggled.includes(key)) { + setToggled(toggled.filter(k => k !== key)); + } else { + setToggled([...toggled, key]); + } + }, [toggled, setToggled]); + + const sharedState = useMemo(() => { + return {toggled, toggle}; + }, [toggled, toggle]); + return ( + {children} + + ) +} + +export const useDropdownStatusContext = () => { + return useContext(DropdownStatusContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/context/groupEditContextHook.js b/Plan/react/dashboard/src/hooks/context/groupEditContextHook.js new file mode 100644 index 000000000..b669534c8 --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/groupEditContextHook.js @@ -0,0 +1,247 @@ +import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from "react"; +import {fetchAvailablePermissions, fetchGroupPermissions, saveGroupPermissions} from "../../service/manageService"; +import {useConfigurationStorageContext} from "./configurationStorageContextHook"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faCheck, faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; +import {useAlertPopupContext} from "./alertPopupContext"; +import {Trans, useTranslation} from "react-i18next"; + +const GroupEditContext = createContext({}); + +const createPermissionTree = (allPermissions, toggledPermissions) => { + if (!allPermissions.length) { + return {children: []}; + } + allPermissions.sort(); + + const isParent = (permission, parentCandidate) => { + if (parentCandidate === permission) return false; + const substitute = permission.replace(parentCandidate, '')[0]; + // Last character is . so it is a sub-permission of the parent, eg. page.player, page.player.thing -> .thing + return substitute.length && substitute[0] === '.'; + } + + const idTree = []; + for (const permission of allPermissions) { + const parentCandidates = allPermissions.filter(parentCandidate => isParent(permission, parentCandidate)); + parentCandidates.sort(); + parentCandidates.reverse(); + const parent = parentCandidates.length ? parentCandidates[0] : null; + const parentIndex = allPermissions.indexOf(parent); + idTree.push({permission, parentIndex, children: [], toggled: toggledPermissions.includes(permission)}); + } + for (const permission of idTree) { + const parentIndex = permission.parentIndex; + if (parentIndex !== -1) { + permission.parent = idTree[parentIndex]; + idTree[parentIndex].children.push(permission); + } + } + const rootNodes = idTree.filter(node => node.parentIndex === -1); + return {children: rootNodes}; +} + +export const GroupEditContextProvider = ({groupName, children}) => { + const {t} = useTranslation(); + const [changed, setChanged] = useState(false); + const {markDirty, saveRequested, discardRequested} = useConfigurationStorageContext(); + const [lastSave, setLastSave] = useState(Date.now()); + const [lastDiscard, setLastDiscard] = useState(Date.now()); + const {addAlert} = useAlertPopupContext(); + + const [allPermissions, setAllPermissions] = useState([]); + useEffect(() => { + fetchAvailablePermissions().then(response => { + setAllPermissions(response?.data?.permissions); + }); + }, []); + + const [permissions, setPermissions] = useState([]); + const loadPermissions = useCallback(() => { + return fetchGroupPermissions(groupName).then(response => { + setPermissions(response?.data?.permissions); + setLastDiscard(Date.now()); + setChanged(false); + }); + }, [groupName, setChanged, setPermissions, setLastDiscard]); + useEffect(() => { + loadPermissions() + }, [loadPermissions]); + + const [permissionTree, setPermissionTree] = useState({children: []}); + useEffect(() => { + setPermissionTree(createPermissionTree(allPermissions, permissions)) + }, [allPermissions, permissions]); + + const dfs = useCallback((root, permission) => { + if (root.permission === permission) return root; + + for (const child of root.children) { + const found = dfs(child, permission); + if (found) return found; + } + return null; + }, []); + + const isNodeChecked = useCallback((node) => { + if (!node) return false; + const parentNode = node?.parent; + return node?.toggled ? node : isNodeChecked(parentNode) + }, []) + const isChecked = useCallback((permission) => { + const node = dfs(permissionTree, permission); + return isNodeChecked(node); + }, [permissionTree, isNodeChecked, dfs]); + + const isNodeIndeterminate = useCallback((node) => { + // Indeterminate if: + // permission node itself is not toggled, + // but some of its children are toggled or indeterminate. + if (!node || node.toggled) return false; + + const childNodes = node.children; + const toggledChildNodes = node.children.filter(child => child.toggled); + const indeterminate = toggledChildNodes.length !== 0; + if (indeterminate) return true; + + for (const child of childNodes) { + if (isNodeIndeterminate(child)) { + return true; + } + } + return false; + }, []) + const isIndeterminate = useCallback((permission) => { + const node = dfs(permissionTree, permission); + return isNodeIndeterminate(node); + }, [permissionTree, dfs, isNodeIndeterminate]); + + const removePermissions = useCallback((toRemoveArray) => { + const result = permissions.filter(p => !toRemoveArray.includes(p)); + setPermissions(result); + }, [setPermissions, permissions]); + + const modifyPermissions = useCallback((toAddArray, toRemoveArray) => { + const result = [...permissions, ...toAddArray].filter(p => !toRemoveArray.includes(p)); + setPermissions(result); + }, [setPermissions, permissions]); + + const getAllChildren = useCallback((root) => { + let children = [...root.children]; + root.children.forEach(child => children.push(...getAllChildren(child))) + return children; + }, []) + + const togglePermission = useCallback((permission) => { + markDirty(true); + setChanged(true); + const node = dfs(permissionTree, permission); + + const checked = node.toggled; + if (checked) { + removePermissions([permission]); + } else { + // Lookup parent that is checked + const checkedNode = isNodeChecked(node); + if (checkedNode) { + // We need to find the nodes that are currently checked by the parent node + // This way only the node that was clicked gets unchecked + const nodesToAdd = []; + const queue = [node.parent]; + let foundAll = false; + while (!foundAll) { + const next = queue.pop(); + if (!next) continue; + const otherChildren = next.children.filter(child => child.permission !== permission); + nodesToAdd.push(...otherChildren); + if (next.permission === checkedNode.permission) foundAll = true; + + if (!foundAll) queue.push(next.parent); + } + + // Then we need to remove the nodes that are no longer all checked + // This way all the parents of the node that was clicked are unchecked + const nodesToRemove = []; + let next = node; + while (next.parent != null) { + nodesToRemove.push(next.parent); + next = next.parent; + } + + modifyPermissions( + nodesToAdd.map(child => child.permission), + nodesToRemove.map(child => child.permission) + ); + } else { + // Is not checked, add the permission and remove all child permission from list (they are checked) + modifyPermissions( + [permission], + getAllChildren(node).map(child => child.permission) + ); + } + } + }, [markDirty, setChanged, permissionTree, dfs, getAllChildren, isNodeChecked, modifyPermissions, removePermissions]); + + const saveChanges = useCallback(async () => { + if (saveRequested > lastSave && changed) { + const {error} = await saveGroupPermissions(groupName, permissions); + if (error) { + addAlert({ + timeout: 15000, + color: "danger", + content: <> + + {" "} + + + }); + } else { + setChanged(false); + addAlert({ + timeout: 5000, + color: "success", + content: <>{" "}{t('html.label.managePage.alert.saveSuccess')} + }); + } + setLastSave(Date.now()); + } + }, [lastSave, changed, setChanged, saveRequested, setLastSave, permissions, groupName, addAlert, t]); + + useEffect(() => { + saveChanges(); + }, [saveChanges]); + + useEffect(() => { + if (discardRequested > lastDiscard) { + loadPermissions(); + } + }, [lastDiscard, discardRequested, loadPermissions]) + + const sharedState = useMemo(() => { + return { + changed, + permissionTree, + permissions, + isChecked, + isIndeterminate, + togglePermission, + groupName + } + }, [ + changed, + permissionTree, + permissions, + isChecked, + isIndeterminate, + togglePermission, + groupName + ]) + return ( + {children} + + ) +} + +export const useGroupEditContext = () => { + return useContext(GroupEditContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/dataFetchHook.js b/Plan/react/dashboard/src/hooks/dataFetchHook.js index 077b62593..e255518fd 100644 --- a/Plan/react/dashboard/src/hooks/dataFetchHook.js +++ b/Plan/react/dashboard/src/hooks/dataFetchHook.js @@ -4,7 +4,7 @@ import {useDataStore} from "./datastoreHook"; import {useMetadata} from "./metadataHook"; import {staticSite} from "../service/backendConfiguration"; -export const useDataRequest = (fetchMethod, parameters) => { +export const useDataRequest = (fetchMethod, parameters, shouldRequest) => { const [data, setData] = useState(undefined); const [loadingError, setLoadingError] = useState(undefined); const {updateRequested, finishUpdate} = useNavigation(); @@ -13,6 +13,10 @@ export const useDataRequest = (fetchMethod, parameters) => { /*eslint-disable react-hooks/exhaustive-deps */ useEffect(() => { + if (shouldRequest !== undefined && !shouldRequest) { + setData(undefined); + return; + } datastore.setAsUpdating(fetchMethod); const handleResponse = (json, error, skipOldData, timeout) => { if (json) { @@ -59,7 +63,7 @@ export const useDataRequest = (fetchMethod, parameters) => { fetchMethod(updateRequested, ...parameters).then(({data: json, error}) => { handleResponse(json, error, false, 1000); }); - }, [fetchMethod, parameters.length, ...parameters, updateRequested, refreshBarrierMs]) + }, [fetchMethod, parameters.length, ...parameters, updateRequested, refreshBarrierMs, shouldRequest]) /* eslint-enable react-hooks/exhaustive-deps */ return {data, loadingError}; diff --git a/Plan/react/dashboard/src/hooks/serverExtensionDataContext.js b/Plan/react/dashboard/src/hooks/serverExtensionDataContext.js index 9c4996926..3777b99be 100644 --- a/Plan/react/dashboard/src/hooks/serverExtensionDataContext.js +++ b/Plan/react/dashboard/src/hooks/serverExtensionDataContext.js @@ -1,14 +1,17 @@ import {createContext, useContext, useEffect, useMemo, useState} from "react"; import {useDataRequest} from "./dataFetchHook"; import {fetchExtensionData} from "../service/serverService"; +import {useAuth} from "./authenticationHook"; const ServerExtensionContext = createContext({}); -export const ServerExtensionContextProvider = ({identifier, children}) => { +export const ServerExtensionContextProvider = ({identifier, proxy, children}) => { + const {hasPermission} = useAuth(); const [extensionData, setExtensionData] = useState(undefined); const [extensionDataLoadingError, setExtensionDataLoadingError] = useState(undefined); - const {data, loadingError} = useDataRequest(fetchExtensionData, [identifier]); + const seePlugins = hasPermission(proxy ? 'page.network.plugins' : 'page.server.plugins'); + const {data, loadingError} = useDataRequest(fetchExtensionData, [identifier], seePlugins); useEffect(() => { setExtensionData(data); @@ -16,8 +19,8 @@ export const ServerExtensionContextProvider = ({identifier, children}) => { }, [data, loadingError, setExtensionData, setExtensionDataLoadingError]) const sharedState = useMemo(() => { - return {extensionData, extensionDataLoadingError}; - }, [extensionData, extensionDataLoadingError]); + return {extensionData, extensionDataLoadingError, proxy}; + }, [extensionData, extensionDataLoadingError, proxy]); return ( {children} diff --git a/Plan/react/dashboard/src/service/backendConfiguration.js b/Plan/react/dashboard/src/service/backendConfiguration.js index 61eaa3884..2f7bebd07 100644 --- a/Plan/react/dashboard/src/service/backendConfiguration.js +++ b/Plan/react/dashboard/src/service/backendConfiguration.js @@ -23,6 +23,10 @@ export const doSomePostRequest = async (url, statusOptions, body) => { return doSomeRequest(url, statusOptions, async () => axios.post(baseAddress + url, body)); } +export const doSomeDeleteRequest = async (url, statusOptions, body) => { + return doSomeRequest(url, statusOptions, async () => axios.delete(baseAddress + url, body)); +} + export const doSomeRequest = async (url, statusOptions, axiosFunction) => { let response = undefined; try { diff --git a/Plan/react/dashboard/src/service/localeService.js b/Plan/react/dashboard/src/service/localeService.js index 9b1930364..fb3daabc6 100644 --- a/Plan/react/dashboard/src/service/localeService.js +++ b/Plan/react/dashboard/src/service/localeService.js @@ -103,7 +103,8 @@ export const localeService = { }, getLanguages: function () { - let languages = Object.fromEntries(Object.entries(this.availableLanguages).sort()); + let languages = Object.fromEntries(Object.entries(this.availableLanguages) + .sort((a, b) => a[0].localeCompare(b[0]))); if ('CUSTOM' in languages) { // Move "Custom" to first in list delete languages["CUSTOM"] diff --git a/Plan/react/dashboard/src/service/manageService.js b/Plan/react/dashboard/src/service/manageService.js new file mode 100644 index 000000000..2a452e167 --- /dev/null +++ b/Plan/react/dashboard/src/service/manageService.js @@ -0,0 +1,31 @@ +import {doGetRequest, doSomeDeleteRequest, doSomePostRequest, standard200option} from "./backendConfiguration"; + +export const fetchGroups = async () => { + let url = `/v1/webGroups`; + return doGetRequest(url, new Date().getTime()); +} + +export const fetchGroupPermissions = async (groupName) => { + let url = `/v1/groupPermissions?group=${groupName}`; + return doGetRequest(url, new Date().getTime()); +} + +export const fetchAvailablePermissions = async () => { + let url = `/v1/permissions`; + return doGetRequest(url, new Date().getTime()); +} + +export const saveGroupPermissions = async (groupName, permissions) => { + let url = `/v1/saveGroupPermissions?group=${groupName}`; + return doSomePostRequest(url, [standard200option], JSON.stringify(permissions)); +} + +export const addGroup = async (groupName) => { + let url = `/v1/saveGroupPermissions?group=${groupName}`; + return doSomePostRequest(url, [standard200option], JSON.stringify([])); +} + +export const deleteGroup = async (groupName, moveTo) => { + let url = `/v1/deleteGroup?group=${groupName}&moveTo=${moveTo}`; + return doSomeDeleteRequest(url, [standard200option]); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/style/main.sass b/Plan/react/dashboard/src/style/main.sass index fff645191..b94c400a2 100644 --- a/Plan/react/dashboard/src/style/main.sass +++ b/Plan/react/dashboard/src/style/main.sass @@ -21,4 +21,21 @@ p, span, td, .h3, a, button h6 @extend .m-0 - @extend .fw-bold \ No newline at end of file + @extend .fw-bold + +.alert-popup-area + position: absolute + top: 0.66rem + left: 50% + transform: translateX(-50%) + display: flex + flex-direction: column + z-index: 999 + + .alert-timer + animation: alert-timer linear + background-color: rgba(0, 0, 0, 0.2) + height: 5px + position: absolute + bottom: 0 + margin-left: -1rem \ No newline at end of file diff --git a/Plan/react/dashboard/src/style/style.css b/Plan/react/dashboard/src/style/style.css index 64d9ad8a2..9d4f0c86c 100644 --- a/Plan/react/dashboard/src/style/style.css +++ b/Plan/react/dashboard/src/style/style.css @@ -1281,6 +1281,14 @@ div#navSrvContainer::-webkit-scrollbar-thumb { text-shadow: 0 0 8px #000; } +/* +Fix table colors not applying properly due to bootstrap. +*/ +.table > :not(caption) > * > * { + color: inherit; + background-color: inherit; +} + /* Adding a translation, even though it doesn't visually impact the page, causes child elements with a @@ -1448,4 +1456,25 @@ ul.filters { .extension-table-header { cursor: pointer; +} + +.opaque-text { + opacity: 0.4; +} + +.opaque-text:hover { + opacity: 1; +} + +@keyframes alert-timer { + from { + width: 100% + } + to { + width: 0 + } +} + +.group-help i { + color: var(--color-plan) } \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/colors.js b/Plan/react/dashboard/src/util/colors.js index 462ec8ef6..612a7c11a 100644 --- a/Plan/react/dashboard/src/util/colors.js +++ b/Plan/react/dashboard/src/util/colors.js @@ -270,7 +270,7 @@ const createNightModeColorCss = () => { export const createNightModeCss = () => { return `#content-wrapper {background-color:var(--color-night-black)!important;}` + `#wrapper {background-image: linear-gradient(to right, var(--color-night-dark-blue) 0%, var(--color-night-dark-blue) 14rem, var(--color-night-black) 14.01rem, var(--color-night-black) 100%);}` + - `body,.btn,.bg-transparent-light {color: var(--color-night-text-dark-bg);}` + + `body,.btn,.bg-transparent-light {color: var(--color-night-text-dark-bg) !important;}` + `.card,.bg-white,.modal-content,.page-loader,.nav-tabs .nav-link:hover,.nav-tabs,hr,form .btn, .btn-outline-secondary{background-color:var(--color-night-dark-blue)!important;border-color:var(--color-night-blue)!important;}` + `.bg-white.collapse-inner {border:1px solid;}` + `.card-header {background-color:var(--color-night-dark-blue);border-color:var(--color-night-blue);}` + @@ -284,5 +284,9 @@ export const createNightModeCss = () => { `.fc a{color:var(--color-night-text-dark-bg) !important;}` + `.fc-button{ background-color: ${withReducedSaturation(colorMap.PLAN.hex)} !important;}` + `.loader{border: 4px solid var(--color-plan); background-color: var(--color-plan);}` + + `.dropdown-item,.dropdown-header{color: var(--color-night-text-dark-bg) !important;}` + + `.dropdown-item:hover{background-color: var(--color-night-blue) !important;}` + + `.dropdown-menu{border-color:var(--color-night-blue);color: var(--color-night-blue) !important;}` + + `:root {--bs-heading-color:var(--color-night-text-dark-bg); --bs-card-color:var(--color-night-text-dark-bg); --bs-body-color:var(--color-night-text-dark-bg); --bs-body-bg:var(--color-night-dark-grey-blue); --bs-btn-active-border-color:var(--color-night-blue);}` + createNightModeColorCss() } \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/SwaggerView.js b/Plan/react/dashboard/src/views/SwaggerView.js index 88bc36d1e..5719a920f 100644 --- a/Plan/react/dashboard/src/views/SwaggerView.js +++ b/Plan/react/dashboard/src/views/SwaggerView.js @@ -3,18 +3,25 @@ import SwaggerUI from "swagger-ui" import "swagger-ui/dist/swagger-ui.css" import {baseAddress} from "../service/backendConfiguration" +import {useAuth} from "../hooks/authenticationHook"; const SwaggerView = () => { + const {hasPermission} = useAuth(); + const seeDocs = hasPermission('access.docs'); useEffect(() => { - SwaggerUI({ - dom_id: "#swagger-ui", - url: baseAddress + "/docs/swagger.json" - }); - }, []); + if (seeDocs) { + SwaggerUI({ + dom_id: "#swagger-ui", + url: baseAddress + "/docs/swagger.json" + }); + } + }, [seeDocs]); return ( -
    + <> + {seeDocs &&
    } + ) } diff --git a/Plan/react/dashboard/src/views/common/Geolocations.js b/Plan/react/dashboard/src/views/common/Geolocations.js index e1f2cfca5..261f3a954 100644 --- a/Plan/react/dashboard/src/views/common/Geolocations.js +++ b/Plan/react/dashboard/src/views/common/Geolocations.js @@ -6,15 +6,21 @@ import PingTableCard from "../../components/cards/common/PingTableCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; -const Geolocations = ({className, geolocationData, pingData, geolocationError, pingError}) => { +const Geolocations = ( + {className, geolocationData, pingData, geolocationError, pingError, seeGeolocations, seePing} +) => { return (
    - {geolocationError ? : - } - {pingError ? : } + {seeGeolocations && <> + {geolocationError ? + : } + } + {seePing && <> + {pingError ? : } + }
    diff --git a/Plan/react/dashboard/src/views/layout/ErrorsPage.js b/Plan/react/dashboard/src/views/layout/ErrorsPage.js index 49d1bc581..8ba0b64e7 100644 --- a/Plan/react/dashboard/src/views/layout/ErrorsPage.js +++ b/Plan/react/dashboard/src/views/layout/ErrorsPage.js @@ -9,16 +9,19 @@ import {fetchErrorLogs} from "../../service/metadataService"; import ErrorPage from "./ErrorPage"; import ErrorsAccordion from "../../components/accordion/ErrorsAccordion"; import {Card} from "react-bootstrap"; +import {useAuth} from "../../hooks/authenticationHook"; const ErrorsPage = () => { - const {data, loadingError} = useDataRequest(fetchErrorLogs, []); + const {hasPermission} = useAuth(); + const seeErrors = hasPermission('access.errors'); + const {data, loadingError} = useDataRequest(fetchErrorLogs, [], seeErrors); if (loadingError) return ; return ( <> - -
    + + {seeErrors &&
    Error Logs} hideUpdater/>
    @@ -30,7 +33,7 @@ const ErrorsPage = () => {
    -
    +
    } ) }; diff --git a/Plan/react/dashboard/src/views/layout/ManagePage.js b/Plan/react/dashboard/src/views/layout/ManagePage.js new file mode 100644 index 000000000..2ffee9964 --- /dev/null +++ b/Plan/react/dashboard/src/views/layout/ManagePage.js @@ -0,0 +1,65 @@ +import React, {useEffect, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {useMetadata} from "../../hooks/metadataHook"; +import {useNavigation} from "../../hooks/navigationHook"; +import {staticSite} from "../../service/backendConfiguration"; +import {faSearch, faUsersGear} from "@fortawesome/free-solid-svg-icons"; +import {useAuth} from "../../hooks/authenticationHook"; +import MainPageRedirect from "../../components/navigation/MainPageRedirect"; +import ErrorPage from "./ErrorPage"; +import Sidebar from "../../components/navigation/Sidebar"; +import Header from "../../components/navigation/Header"; +import {Outlet} from "react-router-dom"; +import ColorSelectorModal from "../../components/modal/ColorSelectorModal"; +import AlertPopupArea from "../../components/alert/AlertPopupArea"; + +const HelpModal = React.lazy(() => import("../../components/modal/HelpModal")); + +const ManagePage = () => { + const {t, i18n} = useTranslation(); + const {isProxy, networkName, serverName} = useMetadata(); + + const [error] = useState(undefined); + const {sidebarItems, setSidebarItems, currentTab, setCurrentTab} = useNavigation(); + + useEffect(() => { + const items = staticSite ? [] : [ + {name: 'html.label.manage'}, + {name: 'html.label.groupPermissions', icon: faUsersGear, href: "groups"}, + // {name: 'html.label.groupUsers', icon: faUserGroup, href: "groupUsers"}, + // {name: 'html.label.users', icon: faUser, href: "users"}, + {name: 'html.label.links'}, + {name: 'html.label.query', icon: faSearch, href: "/query"}, + ] + + setSidebarItems(items); + window.document.title = `Plan | Manage`; + setCurrentTab('html.label.manage') + }, [t, i18n, setCurrentTab, setSidebarItems]) + + const {authRequired, loggedIn} = useAuth(); + if (authRequired && !loggedIn) return ; + if (error) return ; + + const displayedServerName = isProxy ? networkName : (serverName?.startsWith('Server') ? "Plan" : serverName); + return ( + <> + +
    + +
    +
    +
    + +
    + +
    +
    + + ) +} + +export default ManagePage; \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/layout/NetworkPage.js b/Plan/react/dashboard/src/views/layout/NetworkPage.js index d291e16ae..795df9486 100644 --- a/Plan/react/dashboard/src/views/layout/NetworkPage.js +++ b/Plan/react/dashboard/src/views/layout/NetworkPage.js @@ -39,7 +39,12 @@ const NetworkSidebar = () => { useEffect(() => { const servers = networkMetadata?.servers || []; const items = [ - {name: 'html.label.networkOverview', icon: faInfoCircle, href: "overview"}, + { + name: 'html.label.networkOverview', + icon: faInfoCircle, + href: "overview", + permission: 'page.network.overview' + }, {}, {name: 'html.label.information'}, { @@ -50,10 +55,17 @@ const NetworkSidebar = () => { nameShort: 'html.label.overview', name: 'html.label.servers', icon: faNetworkWired, - href: "serversOverview" + href: "serversOverview", + permission: 'page.network.server.list' + }, + { + name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions", + permission: 'page.network.sessions' + }, + staticSite ? undefined : { + name: 'html.label.performance', icon: faCogs, href: "performance", + permission: 'page.network.performance' }, - {name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions"}, - staticSite ? undefined : {name: 'html.label.performance', icon: faCogs, href: "performance"}, {}, ...servers .filter(server => !server.proxy) @@ -62,7 +74,8 @@ const NetworkSidebar = () => { name: server.serverName, icon: faServer, href: "/server/" + server.serverUUID, - color: 'light-green' + color: 'light-green', + permission: 'access.server.' + server.serverUUID } }) ] @@ -75,17 +88,43 @@ const NetworkSidebar = () => { nameShort: 'html.label.overview', name: 'html.label.playerbaseOverview', icon: faChartLine, - href: "playerbase" + href: "playerbase", + permission: 'page.network.playerbase' + }, + { + name: 'html.label.joinAddresses', + icon: faLocationArrow, + href: "join-addresses", + permission: 'page.network.join.addresses' + }, + { + name: 'html.label.playerRetention', + icon: faUsersViewfinder, + href: "retention", + permission: 'page.network.retention' + }, + { + name: 'html.label.playerList', + icon: faUserGroup, + href: "players", + permission: 'page.network.players' + }, + { + name: 'html.label.geolocations', + icon: faGlobe, + href: "geolocations", + permission: 'page.network.geolocations' }, - {name: 'html.label.joinAddresses', icon: faLocationArrow, href: "join-addresses"}, - {name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"}, - {name: 'html.label.playerList', icon: faUserGroup, href: "players"}, - {name: 'html.label.geolocations', icon: faGlobe, href: "geolocations"}, ] }, {}, - {name: 'html.label.plugins'}, - {name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"} + {name: 'html.label.plugins', permission: 'page.network.plugins'}, + { + name: 'html.label.pluginsOverview', + icon: faCubes, + href: "plugins-overview", + permission: 'page.network.plugins' + } ] if (extensionData?.extensions) { @@ -95,7 +134,8 @@ const NetworkSidebar = () => { return { name: info.pluginName, icon: [iconTypeToFontAwesomeClass(info.icon.family), info.icon.iconName], - href: `plugins/${encodeURIComponent(info.pluginName)}` + href: `plugins/${encodeURIComponent(info.pluginName)}`, + permission: 'page.network.plugins' } }).forEach(item => items.push(item)) } @@ -103,8 +143,8 @@ const NetworkSidebar = () => { if (!staticSite) { items.push( {}, - {name: 'html.label.links'}, - {name: 'html.label.query', icon: faSearch, href: "/query"} + {name: 'html.label.links', permission: 'access.query'}, + {name: 'html.label.query', icon: faSearch, href: "/query", permission: 'access.query'} ); } @@ -129,7 +169,7 @@ const NetworkPage = () => { return ( <> - +
    diff --git a/Plan/react/dashboard/src/views/layout/PlayerPage.js b/Plan/react/dashboard/src/views/layout/PlayerPage.js index 1991f9672..219777a58 100644 --- a/Plan/react/dashboard/src/views/layout/PlayerPage.js +++ b/Plan/react/dashboard/src/views/layout/PlayerPage.js @@ -17,34 +17,42 @@ const HelpModal = React.lazy(() => import("../../components/modal/HelpModal")); const PlayerPage = () => { const {t, i18n} = useTranslation(); + const {hasChildPermission} = useAuth(); + const seePlayer = hasChildPermission('access.player') const {sidebarItems, setSidebarItems} = useNavigation(); const {identifier} = useParams(); const {currentTab, finishUpdate} = useNavigation(); - const {data: player, loadingError} = useDataRequest(fetchPlayer, [identifier]) + const {data: player, loadingError} = useDataRequest(fetchPlayer, [identifier], seePlayer) useEffect(() => { if (!player) return; const items = [ - {name: 'html.label.playerOverview', icon: faInfoCircle, href: "overview"}, - {name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions"}, - {name: 'html.label.pvpPve', icon: faCampground, href: "pvppve"}, - {name: 'html.label.servers', icon: faNetworkWired, href: "servers"} + { + name: 'html.label.playerOverview', + icon: faInfoCircle, + href: "overview", + permission: 'page.player.overview' + }, + {name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions", permission: 'page.player.sessions'}, + {name: 'html.label.pvpPve', icon: faCampground, href: "pvppve", permission: 'page.player.versus'}, + {name: 'html.label.servers', icon: faNetworkWired, href: "servers", permission: 'page.player.servers'} ] - player.extensions.map(extension => { + player?.extensions?.map(extension => { return { name: `${t('html.label.plugins')} (${extension.serverName})`, icon: faCubes, - href: `plugins/${encodeURIComponent(extension.serverName)}` + href: `plugins/${encodeURIComponent(extension.serverName)}`, + permission: 'page.player.plugins' } }).forEach(item => items.push(item)); setSidebarItems(items); - window.document.title = `Plan | ${player.info.name}`; + window.document.title = `Plan | ${player?.info?.name}`; finishUpdate(player.timestamp, player.timestamp_f); }, [player, t, i18n, finishUpdate, setSidebarItems]) @@ -55,9 +63,9 @@ const PlayerPage = () => { return player ? ( <> - +
    -
    +
    diff --git a/Plan/react/dashboard/src/views/layout/PlayersPage.js b/Plan/react/dashboard/src/views/layout/PlayersPage.js index 89dae90be..c776d8b33 100644 --- a/Plan/react/dashboard/src/views/layout/PlayersPage.js +++ b/Plan/react/dashboard/src/views/layout/PlayersPage.js @@ -36,7 +36,7 @@ const PlayersPage = () => { if (authRequired && !loggedIn) return ; if (error) return ; - const displayedServerName = isProxy ? networkName : (serverName && serverName.startsWith('Server') ? "Plan" : serverName); + const displayedServerName = isProxy ? networkName : (serverName?.startsWith('Server') ? "Plan" : serverName); return ( <> diff --git a/Plan/react/dashboard/src/views/layout/QueryPage.js b/Plan/react/dashboard/src/views/layout/QueryPage.js index de5c90955..e47d0358b 100644 --- a/Plan/react/dashboard/src/views/layout/QueryPage.js +++ b/Plan/react/dashboard/src/views/layout/QueryPage.js @@ -36,7 +36,7 @@ const QueryPage = () => { if (authRequired && !loggedIn) return ; if (error) return ; - const displayedServerName = isProxy ? networkName : (serverName && serverName.startsWith('Server') ? "Plan" : serverName); + const displayedServerName = isProxy ? networkName : (serverName?.startsWith('Server') ? "Plan" : serverName); return ( <> diff --git a/Plan/react/dashboard/src/views/layout/RegisterPage.js b/Plan/react/dashboard/src/views/layout/RegisterPage.js index a800ae2e6..7ef20d19b 100644 --- a/Plan/react/dashboard/src/views/layout/RegisterPage.js +++ b/Plan/react/dashboard/src/views/layout/RegisterPage.js @@ -138,10 +138,12 @@ const RegisterPage = () => { const {data, error} = await fetchRegisterCheck(code); if (error) { setFailMessage(t('html.register.error.checkFailed') + error) - } else if (data && data.success) { + } else if (data?.success) { navigate('/login?registerSuccess=true'); } else { - setTimeout(() => checkRegistration(code), 5000); + setTimeout(() => { + checkRegistration(code); + }, 5000); } } @@ -159,11 +161,13 @@ const RegisterPage = () => { const {data, error} = await postRegister(username, password); if (error) { - setFailMessage(t('html.register.error.failed') + (error.data && error.data.error ? error.data.error : error.message)); - } else if (data && data.code) { + setFailMessage(t('html.register.error.failed') + (error?.data.error ? error.data.error : error.message)); + } else if (data?.code) { setRegisterCode(data.code); setFinalizeRegistrationModalOpen(true); - setTimeout(() => checkRegistration(data.code), 10000); + setTimeout(() => { + checkRegistration(data.code); + }, 10000); } else { setFailMessage(t('html.register.error.failed') + data ? data.error : t('generic.noData')); } diff --git a/Plan/react/dashboard/src/views/layout/ServerPage.js b/Plan/react/dashboard/src/views/layout/ServerPage.js index aa1ff3463..11413c2a1 100644 --- a/Plan/react/dashboard/src/views/layout/ServerPage.js +++ b/Plan/react/dashboard/src/views/layout/ServerPage.js @@ -40,7 +40,12 @@ const ServerSidebar = () => { useEffect(() => { const items = [ - {name: 'html.label.serverOverview', icon: faInfoCircle, href: "overview"}, + { + name: 'html.label.serverOverview', + icon: faInfoCircle, + href: "overview", + permission: 'page.server.overview' + }, {}, {name: 'html.label.information'}, { @@ -51,11 +56,22 @@ const ServerSidebar = () => { nameShort: 'html.label.overview', name: 'html.label.playersOnlineOverview', icon: faChartArea, - href: "online-activity" + href: "online-activity", + permission: 'page.server.online.activity' }, - {name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions"}, - {name: 'html.label.pvpPve', icon: faCampground, href: "pvppve"} - ] + { + name: 'html.label.sessions', + icon: faCalendarCheck, + href: "sessions", + permission: 'page.server.sessions' + }, + { + name: 'html.label.pvpPve', + icon: faCampground, + href: "pvppve", + permission: 'page.server.player.versus' + } + ], }, { name: 'html.label.playerbase', @@ -65,18 +81,44 @@ const ServerSidebar = () => { nameShort: 'html.label.overview', name: 'html.label.playerbaseOverview', icon: faChartLine, - href: "playerbase" + href: "playerbase", + permission: 'page.server.playerbase' + }, + { + name: 'html.label.joinAddresses', + icon: faLocationArrow, + href: "join-addresses", + permission: 'page.server.join.addresses' + }, + { + name: 'html.label.playerRetention', + icon: faUsersViewfinder, + href: "retention", + permission: 'page.server.retention' + }, + { + name: 'html.label.playerList', + icon: faUserGroup, + href: "players", + permission: 'page.server.players' + }, + { + name: 'html.label.geolocations', + icon: faGlobe, + href: "geolocations", + permission: 'page.server.geolocations' }, - {name: 'html.label.joinAddresses', icon: faLocationArrow, href: "join-addresses"}, - {name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"}, - {name: 'html.label.playerList', icon: faUserGroup, href: "players"}, - {name: 'html.label.geolocations', icon: faGlobe, href: "geolocations"}, ] }, - {name: 'html.label.performance', icon: faCogs, href: "performance"}, + {name: 'html.label.performance', icon: faCogs, href: "performance", permission: 'page.server.performance'}, {}, - {name: 'html.label.plugins'}, - {name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"} + {name: 'html.label.plugins', permission: 'page.server.plugins'}, + { + name: 'html.label.pluginsOverview', + icon: faCubes, + href: "plugins-overview", + permission: 'page.server.plugins' + } ] if (extensionData?.extensions) { @@ -86,7 +128,8 @@ const ServerSidebar = () => { return { name: info.pluginName, icon: [iconTypeToFontAwesomeClass(info.icon.family), info.icon.iconName], - href: `plugins/${encodeURIComponent(info.pluginName)}` + href: `plugins/${encodeURIComponent(info.pluginName)}`, + permission: 'page.network.plugins' } }).forEach(item => items.push(item)) } @@ -94,8 +137,8 @@ const ServerSidebar = () => { if (!staticSite) { items.push( {}, - {name: 'html.label.links'}, - {name: 'html.label.query', icon: faSearch, href: "/query"} + {name: 'html.label.links', permission: 'access.query'}, + {name: 'html.label.query', icon: faSearch, href: "/query", permission: 'access.query'} ); } @@ -133,7 +176,7 @@ const ServerPage = () => { const fromMetadata = networkMetadata?.servers?.find(server => server.serverUUID === identifier); return fromMetadata ? fromMetadata.serverName : identifier; } else { - return serverName && serverName.startsWith('Server') ? "Plan" : serverName + return serverName?.startsWith('Server') ? "Plan" : serverName } } const displayedServerName = getDisplayedServerName(); diff --git a/Plan/react/dashboard/src/views/manage/GroupsView.js b/Plan/react/dashboard/src/views/manage/GroupsView.js new file mode 100644 index 000000000..a4f2fdd68 --- /dev/null +++ b/Plan/react/dashboard/src/views/manage/GroupsView.js @@ -0,0 +1,413 @@ +import ErrorView from "../ErrorView"; +import LoadIn from "../../components/animation/LoadIn"; +import {Card, Col, InputGroup, Row} from "react-bootstrap"; +import React, {useCallback, useEffect, useState} from "react"; +import CardHeader from "../../components/cards/CardHeader"; +import { + faCheck, + faExclamationTriangle, + faFloppyDisk, + faPlus, + faRotateLeft, + faTrash, + faUserGroup, + faUsersGear +} from "@fortawesome/free-solid-svg-icons"; +import {FontAwesomeIcon as Fa, FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {GroupEditContextProvider, useGroupEditContext} from "../../hooks/context/groupEditContextHook"; +import {addGroup, deleteGroup, fetchGroups} from "../../service/manageService"; +import {CardLoader, ChartLoader} from "../../components/navigation/Loader"; +import {Trans, useTranslation} from "react-i18next"; +import { + ConfigurationStorageContextProvider, + useConfigurationStorageContext +} from "../../hooks/context/configurationStorageContextHook"; +import SideNavTabs from "../../components/layout/SideNavTabs"; +import Select from "../../components/input/Select"; +import Scrollable from "../../components/Scrollable"; +import OpaqueText from "../../components/layout/OpaqueText"; +import {useAlertPopupContext} from "../../hooks/context/alertPopupContext"; +import {DropdownStatusContextProvider, useDropdownStatusContext} from "../../hooks/context/dropdownStatusContextHook"; +import {useNavigation} from "../../hooks/navigationHook"; +import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons"; +import {useAuth} from "../../hooks/authenticationHook"; + +const GroupsHeader = ({groupName, icon}) => { + return ( + {groupName} + ) +} + +const PermissionDropdown = ({permission, checked, indeterminate, togglePermission, children, childNodes, root}) => { + const {t} = useTranslation(); + const {toggle, toggled} = useDropdownStatusContext(); + + const translationKey = "html.manage.permission.description." + permission?.split('.').join("_"); + const translated = t(translationKey); + + if (childNodes.length) { + if (permission === undefined) { + return <>{children}; + } else { + return ( +
    + { + event.preventDefault(); + toggle(permission); + }}> + { + if (input) input.indeterminate = indeterminate + }} + onChange={() => togglePermission(permission)} + /> {permission} {permission && translated !== translationKey && + · {translated}} +
    +
    + + {children} +
    + ) + } + } else { + return ( +
  • + togglePermission(permission)} + /> {permission} {permission && translated !== translationKey && + · {translated}} +
  • + ) + } +} + +const PermissionTree = ({nodes, isIndeterminate, isChecked, togglePermission}) => { + if (!nodes.length) { + return <>; + } + return ( + <> + {nodes.map(node =>
    + + + +
    )} + + ) +} + +const GroupsBody = ({groups, reloadGroupNames}) => { + const { + changed, + groupName, + permissionTree, + togglePermission, + isChecked, + isIndeterminate + } = useGroupEditContext(); + + return ( + + +
    +

    + +

    + + +
    + + {permissionTree?.children?.length && } + {!permissionTree?.children?.length && } + + +
    + ); +} + +const SaveButton = () => { + const {t} = useTranslation(); + const {dirty, requestSave} = useConfigurationStorageContext(); + + return ( + + ) +} + +const DeleteGroupButton = ({groupName, groups, reloadGroupNames}) => { + const [clicked, setClicked] = useState(false); + const [moveToGroup, setMoveToGroup] = useState(0); + const {addAlert} = useAlertPopupContext(); + const {t} = useTranslation(); + + if (clicked) { + const groupOptions = groups.filter(g => g.name !== groupName).map(g => g.name); + return ( + + }/> + + +
    + {t('html.label.managePage.deleteGroup.moveToSelect')} +
    + + {invalid &&
    + {t('html.label.managePage.addGroup.invalidName')} +
    } +
    + +
    +
    + ) +} + +const GroupsCard = ({groups, reloadGroupNames}) => { + const {t} = useTranslation(); + const {setHelpModalTopic} = useNavigation(); + const openHelp = useCallback(() => setHelpModalTopic('group-permissions'), [setHelpModalTopic]); + + const slices = groups.map(group => { + return { + body: + + , + header: , + color: 'light-green', + outline: false + } + }) + + slices.push({ + body: , + header: , + color: 'light-green', + outline: false + }) + + return ( + + + + + + + + + + + + ) +} + +const GroupsView = () => { + const {hasPermission} = useAuth(); + const seeManageGroups = hasPermission('manage.groups'); + + const [updateRequested, setUpdateRequested] = useState(Date.now()); + const [data, setData] = useState(undefined); + const [loadingError, setLoadingError] = useState(undefined); + const loadGroupNames = useCallback(() => { + if (!seeManageGroups) return; + fetchGroups().then(({data, error}) => { + setData(data); + setLoadingError(error); + }); + }, [setData, setLoadingError, seeManageGroups]); + useEffect(() => { + loadGroupNames(); + }, [updateRequested, loadGroupNames]); + + const reloadGroupNames = useCallback(() => { + setTimeout(() => setUpdateRequested(Date.now()), 1000); + }, [setUpdateRequested]) + + if (loadingError) return ; + if (!data) return ; + + return ( + + {seeManageGroups && + + + + + + + + } + + ) +}; + +export default GroupsView \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/network/NetworkGeolocations.js b/Plan/react/dashboard/src/views/network/NetworkGeolocations.js index 2fec08487..0487f7cf6 100644 --- a/Plan/react/dashboard/src/views/network/NetworkGeolocations.js +++ b/Plan/react/dashboard/src/views/network/NetworkGeolocations.js @@ -3,15 +3,20 @@ import {useDataRequest} from "../../hooks/dataFetchHook"; import Geolocations from "../common/Geolocations"; import {fetchNetworkPingTable} from "../../service/networkService"; import {fetchGeolocations} from "../../service/serverService"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkGeolocations = () => { - const {data, loadingError} = useDataRequest(fetchGeolocations, []); - const {data: pingData, loadingError: pingLoadingError} = useDataRequest(fetchNetworkPingTable, []); + const {hasPermission} = useAuth(); + + const seeGeolocations = hasPermission('page.network.geolocations.map'); + const seePing = hasPermission('page.network.geolocations.ping.per.country'); + const {data, loadingError} = useDataRequest(fetchGeolocations, [], seeGeolocations); + const {data: pingData, loadingError: pingLoadingError} = useDataRequest(fetchNetworkPingTable, [], seePing); return ( ) }; diff --git a/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.js b/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.js index 7557964cd..70637364c 100644 --- a/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.js +++ b/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.js @@ -4,18 +4,23 @@ import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddre import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkJoinAddresses = () => { + const {hasPermission} = useAuth(); + + const seeTime = hasPermission('page.network.join.addresses.graphs.time'); + const seeLatest = hasPermission('page.network.join.addresses.graphs.pie'); return (
    - + {seeTime && - - + } + {seeLatest && - + }
    diff --git a/Plan/react/dashboard/src/views/network/NetworkOverview.js b/Plan/react/dashboard/src/views/network/NetworkOverview.js index 835d16ab9..16c76e946 100644 --- a/Plan/react/dashboard/src/views/network/NetworkOverview.js +++ b/Plan/react/dashboard/src/views/network/NetworkOverview.js @@ -13,6 +13,7 @@ import {faUsers} from "@fortawesome/free-solid-svg-icons"; import NetworkOnlineActivityGraphsCard from "../../components/cards/server/graphs/NetworkOnlineActivityGraphsCard"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; import ExtendableCardBody from "../../components/layout/extension/ExtendableCardBody"; +import {useAuth} from "../../hooks/authenticationHook"; const RecentPlayersCard = ({data}) => { @@ -21,7 +22,7 @@ const RecentPlayersCard = ({data}) => { if (!data) return ; return ( - +
    {t('html.label.players')} @@ -49,7 +50,10 @@ const RecentPlayersCard = ({data}) => { } const NetworkOverview = () => { - const {data, loadingError} = useDataRequest(fetchNetworkOverview, []) + const {hasPermission, hasChildPermission} = useAuth(); + const seeOverview = hasPermission('page.network.overview.numbers'); + const seeGraphs = hasChildPermission('page.network.overview.graphs'); + const {data, loadingError} = useDataRequest(fetchNetworkOverview, [], seeOverview) if (loadingError) { return @@ -59,21 +63,21 @@ const NetworkOverview = () => {
    - + {seeGraphs && - - + } + {seeOverview && - + } - + {seeOverview && - + }
    ) diff --git a/Plan/react/dashboard/src/views/network/NetworkPerformance.js b/Plan/react/dashboard/src/views/network/NetworkPerformance.js index d6fb6d957..ada54bfe0 100644 --- a/Plan/react/dashboard/src/views/network/NetworkPerformance.js +++ b/Plan/react/dashboard/src/views/network/NetworkPerformance.js @@ -13,8 +13,10 @@ import {useNavigation} from "../../hooks/navigationHook"; import {mapPerformanceDataToSeries} from "../../util/graphs"; import PerformanceGraphsCard from "../../components/cards/network/PerformanceGraphsCard"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkPerformance = () => { + const {hasPermission} = useAuth(); const {t} = useTranslation(); const {networkMetadata} = useMetadata(); const {updateRequested} = useNavigation(); @@ -23,6 +25,8 @@ const NetworkPerformance = () => { const [selectedOptions, setSelectedOptions] = useState([]); const [visualizedServers, setVisualizedServers] = useState([]); + const seePerformance = hasPermission('page.network.performance'); + useEffect(() => { if (networkMetadata) { const options = networkMetadata.servers; @@ -42,6 +46,7 @@ const NetworkPerformance = () => { const [performanceData, setPerformanceData] = useState({}); const loadPerformanceData = useCallback(async () => { + if (!seePerformance) return; const loaded = { servers: [], data: [], @@ -78,7 +83,7 @@ const NetworkPerformance = () => { if (error) loaded.errors.push(error); setPerformanceData({...loaded, overview: data}); - }, [visualizedServers, serverOptions, setPerformanceData]) + }, [visualizedServers, serverOptions, setPerformanceData, seePerformance]) useEffect(() => { loadPerformanceData(); @@ -88,7 +93,7 @@ const NetworkPerformance = () => { (s, i) => s === visualizedServers[i]); return ( -
    + {seePerformance &&
    @@ -112,7 +117,7 @@ const NetworkPerformance = () => { -
    +
    }
    ) }; diff --git a/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.js b/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.js index b308c52ae..3a79f379d 100644 --- a/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.js +++ b/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.js @@ -3,17 +3,21 @@ import ExtendableRow from "../../components/layout/extension/ExtendableRow"; import {Col} from "react-bootstrap"; import LoadIn from "../../components/animation/LoadIn"; import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkPlayerRetention = () => { + const {hasPermission} = useAuth(); + + const seeRetention = hasPermission('page.network.retention'); return ( -
    + {seeRetention &&
    -
    +
    }
    ) }; diff --git a/Plan/react/dashboard/src/views/network/NetworkPlayerbaseOverview.js b/Plan/react/dashboard/src/views/network/NetworkPlayerbaseOverview.js index a65fb3f61..803b5af6a 100644 --- a/Plan/react/dashboard/src/views/network/NetworkPlayerbaseOverview.js +++ b/Plan/react/dashboard/src/views/network/NetworkPlayerbaseOverview.js @@ -9,22 +9,27 @@ import PlayerbaseInsightsCard from "../../components/cards/server/insights/Playe import LoadIn from "../../components/animation/LoadIn"; import {fetchNetworkPlayerbaseOverview} from "../../service/networkService"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkPlayerbaseOverview = () => { - const {data, loadingError} = useDataRequest(fetchNetworkPlayerbaseOverview, []); + const {hasPermission} = useAuth(); + + const seeOverview = hasPermission('page.network.playerbase.overview'); + const seeGraphs = hasPermission('page.network.playerbase.graphs'); + const {data, loadingError} = useDataRequest(fetchNetworkPlayerbaseOverview, [], seeOverview); return (
    - + {seeGraphs && - - + } + {seeOverview && {loadingError && } {!loadingError && <> @@ -34,7 +39,7 @@ const NetworkPlayerbaseOverview = () => { } - + }
    ) diff --git a/Plan/react/dashboard/src/views/network/NetworkServers.js b/Plan/react/dashboard/src/views/network/NetworkServers.js index c694db4ec..28ff10541 100644 --- a/Plan/react/dashboard/src/views/network/NetworkServers.js +++ b/Plan/react/dashboard/src/views/network/NetworkServers.js @@ -7,18 +7,21 @@ import ServersTableCard from "../../components/cards/network/ServersTableCard"; import QuickViewGraphCard from "../../components/cards/network/QuickViewGraphCard"; import QuickViewDataCard from "../../components/cards/network/QuickViewDataCard"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkServers = () => { + const {hasPermission} = useAuth(); const [selectedServer, setSelectedServer] = useState(0); - const {data, loadingError} = useDataRequest(fetchServersOverview, []) + const seeServers = hasPermission('page.network.server.list'); + const {data, loadingError} = useDataRequest(fetchServersOverview, [], seeServers); if (loadingError) { return } return ( - + <>{seeServers && setSelectedServer(index)}/> @@ -27,7 +30,7 @@ const NetworkServers = () => { {data?.servers.length && } {data?.servers.length && } - + } ) }; diff --git a/Plan/react/dashboard/src/views/network/NetworkSessions.js b/Plan/react/dashboard/src/views/network/NetworkSessions.js index 4beb5e420..e06ad14df 100644 --- a/Plan/react/dashboard/src/views/network/NetworkSessions.js +++ b/Plan/react/dashboard/src/views/network/NetworkSessions.js @@ -5,18 +5,23 @@ import SessionInsightsCard from "../../components/cards/server/insights/SessionI import LoadIn from "../../components/animation/LoadIn"; import ServerPieCard from "../../components/cards/common/ServerPieCard"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const NetworkSessions = () => { + const {hasPermission} = useAuth(); + const seeSessionList = hasPermission('page.network.sessions.list') + const seeServerPie = hasPermission('page.network.sessions.server.pie') + const seeInsights = hasPermission('page.network.sessions.overview') return (
    - + {seeSessionList && - + } - - + {seeServerPie && } + {seeInsights && }
    diff --git a/Plan/react/dashboard/src/views/player/PlayerOverview.js b/Plan/react/dashboard/src/views/player/PlayerOverview.js index cc4b38d8a..b6f52275e 100644 --- a/Plan/react/dashboard/src/views/player/PlayerOverview.js +++ b/Plan/react/dashboard/src/views/player/PlayerOverview.js @@ -2,11 +2,9 @@ import React from "react"; import {Card, Col} from "react-bootstrap"; import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons"; -import {faBookOpen, faBraille, faCrosshairs, faGlobe, faSkull, faWifi} from "@fortawesome/free-solid-svg-icons"; -import Scrollable from "../../components/Scrollable"; +import {faBookOpen, faBraille, faCrosshairs, faSkull} from "@fortawesome/free-solid-svg-icons"; import PunchCard from "../../components/graphs/PunchCard"; import AsNumbersTable from "../../components/table/AsNumbersTable"; -import {useTheme} from "../../hooks/themeHook"; import {usePlayer} from "../layout/PlayerPage"; import {useTranslation} from "react-i18next"; import PlayerOverviewCard from "../../components/cards/player/PlayerOverviewCard"; @@ -14,36 +12,8 @@ import NicknamesCard from "../../components/cards/player/NicknamesCard"; import {TableRow} from "../../components/table/TableRow"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; - -const ConnectionsCard = ({player}) => { - const {t} = useTranslation(); - const {nightModeEnabled} = useTheme(); - return ( - - -
    - {t('html.label.connectionInfo')} -
    -
    - - - - - - - - - - {player.connections.map((connection, i) => ( - - - ))} - -
    {t('html.label.country')} {t('html.label.lastConnected')}
    {connection.geolocation}{connection.date}
    -
    -
    - ) -} +import ConnectionsCard from "../../components/cards/player/ConnectionsCard"; +import {useAuth} from "../../hooks/authenticationHook"; const PunchCardCard = ({player}) => { const {t} = useTranslation(); @@ -91,23 +61,24 @@ const OnlineActivityCard = ({player}) => { } const PlayerOverview = () => { + const {hasPermission} = useAuth(); const {player} = usePlayer(); return ( -
    + {hasPermission('page.player.overview') &&
    - - + + -
    +
    }
    ) } diff --git a/Plan/react/dashboard/src/views/player/PlayerPluginData.js b/Plan/react/dashboard/src/views/player/PlayerPluginData.js index 9882e2db9..849d68a75 100644 --- a/Plan/react/dashboard/src/views/player/PlayerPluginData.js +++ b/Plan/react/dashboard/src/views/player/PlayerPluginData.js @@ -5,8 +5,10 @@ import {useParams} from "react-router-dom"; import Masonry from "masonry-layout"; import {usePlayer} from "../layout/PlayerPage"; import LoadIn from "../../components/animation/LoadIn"; +import {useAuth} from "../../hooks/authenticationHook"; const PlayerPluginData = () => { + const {hasPermission} = useAuth(); const {player} = usePlayer(); const {serverName} = useParams(); @@ -24,10 +26,14 @@ const PlayerPluginData = () => { } }, [serverName]) + if (!hasPermission('page.player.plugins')) { + return <>; + } + if (!extensions?.extensionData?.length) { return ( -
    +
    @@ -44,7 +50,7 @@ const PlayerPluginData = () => { return ( -
    +
    diff --git a/Plan/react/dashboard/src/views/player/PlayerPvpPve.js b/Plan/react/dashboard/src/views/player/PlayerPvpPve.js index 548029442..2310e6e97 100644 --- a/Plan/react/dashboard/src/views/player/PlayerPvpPve.js +++ b/Plan/react/dashboard/src/views/player/PlayerPvpPve.js @@ -11,6 +11,7 @@ import PvpPveAsNumbersCard from "../../components/cards/player/PvpPveAsNumbersCa import PvpKillsTableCard from "../../components/cards/common/PvpKillsTableCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const InsightsCard = ({player}) => { const {t} = useTranslation(); @@ -48,10 +49,11 @@ const PvpDeathsTableCard = ({player}) => { } const PlayerPvpPve = () => { + const {hasPermission} = useAuth(); const {player} = usePlayer(); return ( -
    + {hasPermission('page.player.versus') &&
    @@ -68,7 +70,7 @@ const PlayerPvpPve = () => { -
    +
    }
    ) } diff --git a/Plan/react/dashboard/src/views/player/PlayerServers.js b/Plan/react/dashboard/src/views/player/PlayerServers.js index 38031f0d6..444839a33 100644 --- a/Plan/react/dashboard/src/views/player/PlayerServers.js +++ b/Plan/react/dashboard/src/views/player/PlayerServers.js @@ -11,6 +11,7 @@ import {useTranslation} from "react-i18next"; import PlayerPingGraph from "../../components/graphs/PlayerPingGraph"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const PingGraphCard = ({player}) => { const {t} = useTranslation(); @@ -68,10 +69,11 @@ const ServerPieCard = ({player}) => { const PlayerServers = () => { + const {hasPermission} = useAuth(); const {player} = usePlayer(); return ( -
    + {hasPermission('page.player.servers') &&
    @@ -85,7 +87,7 @@ const PlayerServers = () => { -
    +
    }
    ) } diff --git a/Plan/react/dashboard/src/views/player/PlayerSessions.js b/Plan/react/dashboard/src/views/player/PlayerSessions.js index 8397248d0..5129d6538 100644 --- a/Plan/react/dashboard/src/views/player/PlayerSessions.js +++ b/Plan/react/dashboard/src/views/player/PlayerSessions.js @@ -9,6 +9,7 @@ import PlayerWorldPieCard from "../../components/cards/player/PlayerWorldPieCard import PlayerRecentSessionsCard from "../../components/cards/player/PlayerRecentSessionsCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const SessionCalendarCard = ({player}) => { const {t} = useTranslation(); @@ -25,10 +26,11 @@ const SessionCalendarCard = ({player}) => { } const PlayerSessions = () => { + const {hasPermission} = useAuth(); const {player} = usePlayer(); return ( -
    + {hasPermission('page.player.sessions') &&
    @@ -38,7 +40,7 @@ const PlayerSessions = () => { -
    +
    }
    ) } diff --git a/Plan/react/dashboard/src/views/players/AllPlayers.js b/Plan/react/dashboard/src/views/players/AllPlayers.js index 0fd00fd23..0168ac4ad 100644 --- a/Plan/react/dashboard/src/views/players/AllPlayers.js +++ b/Plan/react/dashboard/src/views/players/AllPlayers.js @@ -7,19 +7,22 @@ import PlayerListCard from "../../components/cards/common/PlayerListCard"; import LoadIn from "../../components/animation/LoadIn"; import {CardLoader} from "../../components/navigation/Loader"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const AllPlayers = () => { - const {data, loadingError} = useDataRequest(fetchPlayers, [null]); + const {hasPermission} = useAuth(); + const seePlayers = hasPermission('page.network.players') || hasPermission('access.players') + const {data, loadingError} = useDataRequest(fetchPlayers, [null], seePlayers); if (loadingError) return return ( - + {seePlayers && {data ? : } - + } ) }; diff --git a/Plan/react/dashboard/src/views/query/NewQueryView.js b/Plan/react/dashboard/src/views/query/NewQueryView.js index 9f869cc8f..e82cb75f7 100644 --- a/Plan/react/dashboard/src/views/query/NewQueryView.js +++ b/Plan/react/dashboard/src/views/query/NewQueryView.js @@ -3,18 +3,20 @@ import LoadIn from "../../components/animation/LoadIn"; import {Col, Row} from "react-bootstrap"; import QueryOptionsCard from "../../components/cards/query/QueryOptionsCard"; import QueryPath from "../../components/alert/QueryPath"; +import {useAuth} from "../../hooks/authenticationHook"; const NewQueryView = () => { + const {hasPermission} = useAuth(); return ( -
    + {hasPermission('access.query') &&
    -
    +
    }
    ) }; diff --git a/Plan/react/dashboard/src/views/server/OnlineActivity.js b/Plan/react/dashboard/src/views/server/OnlineActivity.js index 0f85ab127..c987df850 100644 --- a/Plan/react/dashboard/src/views/server/OnlineActivity.js +++ b/Plan/react/dashboard/src/views/server/OnlineActivity.js @@ -9,30 +9,34 @@ import ErrorView from "../ErrorView"; import OnlineActivityInsightsCard from "../../components/cards/server/insights/OnlineActivityInsightsCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const OnlineActivity = () => { + const {hasPermission, hasChildPermission} = useAuth(); const {identifier} = useParams(); - const {data, loadingError} = useDataRequest(fetchOnlineActivityOverview, [identifier]) + const seeOverview = hasPermission('page.server.online.activity.overview'); + const seeGraphs = hasChildPermission('page.server.online.activity.graphs'); + const {data, loadingError} = useDataRequest(fetchOnlineActivityOverview, [identifier], seeOverview) if (loadingError) return return (
    - + {seeGraphs && - - + } + {seeOverview && - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/PlayerbaseOverview.js b/Plan/react/dashboard/src/views/server/PlayerbaseOverview.js index 58a425a81..2e4835dae 100644 --- a/Plan/react/dashboard/src/views/server/PlayerbaseOverview.js +++ b/Plan/react/dashboard/src/views/server/PlayerbaseOverview.js @@ -10,24 +10,28 @@ import PlayerbaseTrendsCard from "../../components/cards/server/tables/Playerbas import PlayerbaseInsightsCard from "../../components/cards/server/insights/PlayerbaseInsightsCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const PlayerbaseOverview = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); - const {data, loadingError} = useDataRequest(fetchPlayerbaseOverview, [identifier]); + const seeOverview = hasPermission('page.server.playerbase.overview'); + const seeGraphs = hasPermission('page.server.playerbase.graphs'); + const {data, loadingError} = useDataRequest(fetchPlayerbaseOverview, [identifier], seeOverview); return (
    - + {seeGraphs && - - + } + {seeOverview && {loadingError && } {!loadingError && <> @@ -37,7 +41,7 @@ const PlayerbaseOverview = () => { } - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/ServerGeolocations.js b/Plan/react/dashboard/src/views/server/ServerGeolocations.js index 6f336027e..70f9c13c3 100644 --- a/Plan/react/dashboard/src/views/server/ServerGeolocations.js +++ b/Plan/react/dashboard/src/views/server/ServerGeolocations.js @@ -3,17 +3,21 @@ import {useParams} from "react-router-dom"; import {useDataRequest} from "../../hooks/dataFetchHook"; import {fetchGeolocations, fetchPingTable} from "../../service/serverService"; import Geolocations from "../common/Geolocations"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerGeolocations = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); - const {data, loadingError} = useDataRequest(fetchGeolocations, [identifier]); - const {pingData, pingLoadingError} = useDataRequest(fetchPingTable, [identifier]); + const seeGeolocations = hasPermission('page.server.geolocations.map'); + const seePing = hasPermission('page.server.geolocations.ping.per.country'); + const {data, loadingError} = useDataRequest(fetchGeolocations, [identifier], seeGeolocations); + const {pingData, pingLoadingError} = useDataRequest(fetchPingTable, [identifier], seePing); return ( ) }; diff --git a/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js b/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js index 58bb5c139..6164c3974 100644 --- a/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js +++ b/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js @@ -5,19 +5,24 @@ import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddre import {useParams} from "react-router-dom"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerJoinAddresses = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); + + const seeTime = hasPermission('page.server.join.addresses.graphs.time'); + const seeLatest = hasPermission('page.server.join.addresses.graphs.pie'); return (
    - + {seeTime && - - + } + {seeLatest && - + }
    diff --git a/Plan/react/dashboard/src/views/server/ServerOverview.js b/Plan/react/dashboard/src/views/server/ServerOverview.js index b465e0501..c763dd9c0 100644 --- a/Plan/react/dashboard/src/views/server/ServerOverview.js +++ b/Plan/react/dashboard/src/views/server/ServerOverview.js @@ -14,6 +14,7 @@ import ServerWeekComparisonCard from "../../components/cards/server/tables/Serve import LoadIn from "../../components/animation/LoadIn"; import {CardLoader} from "../../components/navigation/Loader"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const Last7DaysCard = ({data}) => { const {t} = useTranslation(); @@ -21,7 +22,7 @@ const Last7DaysCard = ({data}) => { if (!data) return ; return ( - +
    {t('html.label.last7days')} @@ -57,11 +58,15 @@ const Last7DaysCard = ({data}) => { } const ServerOverview = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); + const seeOverview = hasPermission('page.server.overview.numbers'); + const seeOnlineGraph = hasPermission('page.server.overview.players.online.graph') const {data, loadingError} = useDataRequest( fetchServerOverview, - [identifier]) + [identifier], + seeOverview) if (loadingError) { return @@ -71,21 +76,21 @@ const ServerOverview = () => {
    - + {seeOnlineGraph && - - + } + {seeOverview && - + } - + {seeOverview && - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/ServerPerformance.js b/Plan/react/dashboard/src/views/server/ServerPerformance.js index 51c1a8dcc..b378bcdeb 100644 --- a/Plan/react/dashboard/src/views/server/ServerPerformance.js +++ b/Plan/react/dashboard/src/views/server/ServerPerformance.js @@ -9,21 +9,25 @@ import PerformanceInsightsCard from "../../components/cards/server/insights/Perf import {ErrorViewCard} from "../ErrorView"; import PerformanceAsNumbersCard from "../../components/cards/server/tables/PerformanceAsNumbersCard"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerPerformance = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); - const {data, loadingError} = useDataRequest(fetchPerformanceOverview, [identifier]); + const seeGraphs = hasPermission('page.server.performance.graphs'); + const seeOverview = hasPermission('page.server.performance.overview'); + const {data, loadingError} = useDataRequest(fetchPerformanceOverview, [identifier], seeOverview); return (
    - + {seeGraphs && - - + } + {seeOverview && {loadingError ? : } @@ -32,7 +36,7 @@ const ServerPerformance = () => { {loadingError ? : } - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/ServerPlayerRetention.js b/Plan/react/dashboard/src/views/server/ServerPlayerRetention.js index 3caed8241..9ed35ceb6 100644 --- a/Plan/react/dashboard/src/views/server/ServerPlayerRetention.js +++ b/Plan/react/dashboard/src/views/server/ServerPlayerRetention.js @@ -4,17 +4,21 @@ import {Col} from "react-bootstrap"; import LoadIn from "../../components/animation/LoadIn"; import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard"; import {useParams} from "react-router-dom"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerPlayerRetention = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); + + const seeRetention = hasPermission('page.server.retention'); return (
    - + {seeRetention && - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/ServerPlayers.js b/Plan/react/dashboard/src/views/server/ServerPlayers.js index df2b0010d..8a8b3d9fe 100644 --- a/Plan/react/dashboard/src/views/server/ServerPlayers.js +++ b/Plan/react/dashboard/src/views/server/ServerPlayers.js @@ -7,22 +7,25 @@ import {Col} from "react-bootstrap"; import PlayerListCard from "../../components/cards/common/PlayerListCard"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerPlayers = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); - const {data, loadingError} = useDataRequest(fetchPlayers, [identifier]); + const seePlayers = hasPermission('page.server.players') + const {data, loadingError} = useDataRequest(fetchPlayers, [identifier], seePlayers); if (loadingError) return return (
    - + {seePlayers && - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/ServerPluginData.js b/Plan/react/dashboard/src/views/server/ServerPluginData.js index e694f7d4b..5ae1f16b3 100644 --- a/Plan/react/dashboard/src/views/server/ServerPluginData.js +++ b/Plan/react/dashboard/src/views/server/ServerPluginData.js @@ -7,11 +7,14 @@ import Loader from "../../components/navigation/Loader"; import {useTranslation} from "react-i18next"; import {useServerExtensionContext} from "../../hooks/serverExtensionDataContext"; import ErrorView from "../ErrorView"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerPluginData = () => { + const {hasPermission} = useAuth(); const {t} = useTranslation(); - const {extensionData, extensionDataLoadingError} = useServerExtensionContext(); + const {extensionData, extensionDataLoadingError, proxy} = useServerExtensionContext(); const extensions = useMemo(() => extensionData?.extensions ? extensionData.extensions.filter(extension => !extension.wide) : [], [extensionData]); + const seePlugins = hasPermission(proxy ? 'page.network.plugins' : 'page.server.plugins'); useEffect(() => { const masonryRow = document.getElementById('extension-masonry-row'); @@ -28,10 +31,14 @@ const ServerPluginData = () => { if (extensionDataLoadingError) return ; + if (!seePlugins) { + return <> + } + if (!extensions?.length) { return ( -
    +
    diff --git a/Plan/react/dashboard/src/views/server/ServerPvpPve.js b/Plan/react/dashboard/src/views/server/ServerPvpPve.js index 9168a56fe..df3f2946a 100644 --- a/Plan/react/dashboard/src/views/server/ServerPvpPve.js +++ b/Plan/react/dashboard/src/views/server/ServerPvpPve.js @@ -9,32 +9,37 @@ import {fetchKills, fetchPvpPve} from "../../service/serverService"; import ErrorView from "../ErrorView"; import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerPvpPve = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); - const {data, loadingError} = useDataRequest(fetchPvpPve, [identifier]); - const {data: killsData, loadingError: killsLoadingError} = useDataRequest(fetchKills, [identifier]); + const seeKillNumbers = hasPermission('page.server.player.versus.overview'); + const seeKills = hasPermission('page.server.player.versus.kill.list'); + + const {data, loadingError} = useDataRequest(fetchPvpPve, [identifier], seeKillNumbers); + const {data: killsData, loadingError: killsLoadingError} = useDataRequest(fetchKills, [identifier], seeKills); if (loadingError) return if (killsLoadingError) return return ( - +
    - + {seeKillNumbers && - - + } + {seeKills && - + }
    ) diff --git a/Plan/react/dashboard/src/views/server/ServerSessions.js b/Plan/react/dashboard/src/views/server/ServerSessions.js index 4641d4b5f..952e6d6d3 100644 --- a/Plan/react/dashboard/src/views/server/ServerSessions.js +++ b/Plan/react/dashboard/src/views/server/ServerSessions.js @@ -6,19 +6,25 @@ import SessionInsightsCard from "../../components/cards/server/insights/SessionI import LoadIn from "../../components/animation/LoadIn"; import {useParams} from "react-router-dom"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; +import {useAuth} from "../../hooks/authenticationHook"; const ServerSessions = () => { + const {hasPermission} = useAuth(); const {identifier} = useParams(); + + const seeSessionList = hasPermission('page.server.sessions.list'); + const seeSessionInsights = hasPermission('page.server.sessions.overview'); + const seeWorldPie = hasPermission('page.server.sessions.world.pie'); return (
    - + {seeSessionList && - + } - - + {seeWorldPie && } + {seeSessionInsights && }