Added Login & Registration pages (#1422)

- Added login.html and register.html
- Added .bg-gradient
- Added logonsine.js for a decoration.
- Added /login, /register, /auth/login, /auth/logout and /auth/register endpoints
- Redirects to /login if cookie not present with auth enabled.
- Basic login functionality using cookies
- Registration page allows new kind of registration that doesn't log passwords on console.
- Fixes a bug with stippets that blocked any cross-plugin modifications (PageExtension API)
- Fixes a typo with css snippet code that made the css not apply (PageExtension API)
This commit is contained in:
Risto Lahtela 2020-05-02 23:31:29 +03:00 committed by GitHub
parent 3d85a23eb3
commit 3903a266a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1292 additions and 33 deletions

View File

@ -68,4 +68,20 @@ public final class URIQuery {
public Optional<String> get(String key) {
return Optional.ofNullable(byKey.get(key));
}
public String asString() {
StringBuilder builder = new StringBuilder("?");
int i = 0;
int max = byKey.size();
for (Map.Entry<String, String> entry : byKey.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
builder.append(key).append('=').append(value);
if (i < max - 1) {
builder.append('&');
}
i++;
}
return builder.toString();
}
}

View File

@ -18,6 +18,8 @@ package com.djrapitops.plan.commands.subcommands;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.auth.RegistrationBin;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.Permissions;
@ -42,6 +44,8 @@ import com.djrapitops.plugin.utilities.Verify;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
/**
@ -104,10 +108,23 @@ public class RegisterCommand extends CommandNode {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 1, Arrays.toString(getArguments())));
}
if (CommandUtils.isPlayer(sender)) {
playerRegister(args, sender);
List<String> argumentList = Arrays.asList(args);
boolean newRegister = argumentList.contains("--code");
if (newRegister) {
if (CommandUtils.isPlayer(sender)) {
register(argumentList, getPermissionLevel(sender), sender);
} else if (argumentList.contains("superuser")) {
register(argumentList, 0, sender);
} else {
sender.sendMessage("§cInvalid arguments.");
}
} else {
consoleRegister(args, sender, notEnoughArgsMsg);
// Legacy support
if (CommandUtils.isPlayer(sender)) {
playerRegister(args, sender);
} else {
consoleRegister(args, sender, notEnoughArgsMsg);
}
}
} catch (PassEncryptUtil.CannotPerformOperationException e) {
errorHandler.log(L.WARN, this.getClass(), e);
@ -131,6 +148,22 @@ public class RegisterCommand extends CommandNode {
registerUser(new WebUser(userName, passHash, permLevel), sender);
}
private void register(List<String> args, int permissionLevel, Sender sender) {
String code = "";
for (String arg : args) {
if (arg.length() == 12) code = arg;
}
if (code.isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 1, "--code !<code>!"));
}
Optional<WebUser> user = RegistrationBin.register(code, permissionLevel);
if (!user.isPresent()) {
sender.sendMessage("§c" + locale.getString(FailReason.USER_DOES_NOT_EXIST));
} else {
registerUser(user.get(), sender);
}
}
private void playerRegister(String[] args, Sender sender) throws PassEncryptUtil.CannotPerformOperationException {
boolean registerSenderAsUser = args.length == 1;
if (registerSenderAsUser) {

View File

@ -0,0 +1,54 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plugin.api.Check;
/**
* Html String generator for /login and /register page.
*
* @author Rsl1122
*/
public class LoginPage implements Page {
private final String template;
private final ServerInfo serverInfo;
LoginPage(
String htmlTemplate,
ServerInfo serverInfo
) {
this.template = htmlTemplate;
this.serverInfo = serverInfo;
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholders.put("command", getCommand());
return placeholders.apply(template);
}
private String getCommand() {
if (serverInfo.getServer().isNotProxy()) return "plan";
if (Check.isBungeeAvailable()) return "planbungee";
if (Check.isVelocityAvailable()) return "planvelocity";
return "plan";
}
}

View File

@ -214,4 +214,12 @@ public class PageFactory {
throw readFail.getCause();
}
}
public Page loginPage() throws IOException {
return new LoginPage(getResource("login.html"), serverInfo.get());
}
public Page registerPage() throws IOException {
return new LoginPage(getResource("register.html"), serverInfo.get());
}
}

View File

@ -94,7 +94,7 @@ public class ResourceSvc implements ResourceService {
}
private WebResource applySnippets(String pluginName, String fileName, WebResource resource) {
Map<Position, StringBuilder> byPosition = calculateSnippets(pluginName, fileName);
Map<Position, StringBuilder> byPosition = calculateSnippets(fileName);
if (byPosition.isEmpty()) return resource;
String html = applySnippets(resource, byPosition);
@ -129,10 +129,10 @@ public class ResourceSvc implements ResourceService {
return html;
}
private Map<Position, StringBuilder> calculateSnippets(String pluginName, String fileName) {
private Map<Position, StringBuilder> calculateSnippets(String fileName) {
Map<Position, StringBuilder> byPosition = new EnumMap<>(Position.class);
for (Snippet snippet : snippets) {
if (snippet.matches(pluginName, fileName)) {
if (snippet.matches(fileName)) {
byPosition.computeIfAbsent(snippet.position, k -> new StringBuilder()).append(snippet.content);
}
}
@ -186,7 +186,9 @@ public class ResourceSvc implements ResourceService {
.appendWithSeparators(jsSrcs, "\"></script><script src=\"")
.append("\"></script>").build();
snippets.add(new Snippet(pluginName, fileName, position, snippet));
logger.info(locale.getString(PluginLang.API_ADD_RESOURCE_JS, pluginName, fileName, position.cleanName()));
if (!"Plan".equals(pluginName)) {
logger.info(locale.getString(PluginLang.API_ADD_RESOURCE_JS, pluginName, fileName, position.cleanName()));
}
}
public void checkParams(String pluginName, String fileName, Position position, String[] jsSrcs) {
@ -212,10 +214,12 @@ public class ResourceSvc implements ResourceService {
checkParams(pluginName, fileName, position, cssSrcs);
String snippet = new TextStringBuilder("<link href=\"")
.appendWithSeparators(cssSrcs, "\" ref=\"stylesheet\"></link><link href=\"")
.append("\" ref=\"stylesheet\"></link>").build();
.appendWithSeparators(cssSrcs, "\" rel=\"stylesheet\"></link><link href=\"")
.append("\" rel=\"stylesheet\">").build();
snippets.add(new Snippet(pluginName, fileName, position, snippet));
logger.info(locale.getString(PluginLang.API_ADD_RESOURCE_CSS, pluginName, fileName, position.cleanName()));
if (!"Plan".equals(pluginName)) {
logger.info(locale.getString(PluginLang.API_ADD_RESOURCE_CSS, pluginName, fileName, position.cleanName()));
}
}
private static class Snippet {
@ -231,8 +235,8 @@ public class ResourceSvc implements ResourceService {
this.content = content;
}
public boolean matches(String pluginName, String fileName) {
return pluginName.equals(this.pluginName) && fileName.equals(this.fileName);
public boolean matches(String fileName) {
return fileName.equals(this.fileName);
}
@Override

View File

@ -23,6 +23,7 @@ import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.Authentication;
import com.djrapitops.plan.delivery.webserver.auth.BasicAuthentication;
import com.djrapitops.plan.delivery.webserver.auth.CookieAuthentication;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.settings.config.PlanConfig;
@ -91,6 +92,7 @@ public class RequestHandler implements HttpHandler {
Response response = getResponse(exchange);
response.getHeaders().putIfAbsent("Access-Control-Allow-Origin", config.get(WebserverSettings.CORS_ALLOW_ORIGIN));
response.getHeaders().putIfAbsent("Access-Control-Allow-Methods", "GET, OPTIONS");
response.getHeaders().putIfAbsent("Access-Control-Allow-Credentials", "true");
ResponseSender sender = new ResponseSender(addresses, exchange, response);
sender.send();
} catch (Exception e) {
@ -115,10 +117,17 @@ public class RequestHandler implements HttpHandler {
response = responseResolver.getResponse(request);
}
} catch (WebUserAuthException thrownByAuthentication) {
if (thrownByAuthentication.getFailReason() != FailReason.USER_AND_PASS_NOT_SPECIFIED) {
FailReason failReason = thrownByAuthentication.getFailReason();
if (failReason == FailReason.USER_PASS_MISMATCH) {
bruteForceGuard.increaseAttemptCountOnFailedLogin(accessor);
response = responseFactory.badRequest(failReason.getReason(), "/auth/login");
} else {
String from = exchange.getRequestURI().toASCIIString();
response = Response.builder()
.redirectTo(StringUtils.startsWithAny(from, "/auth/", "/login") ? "/login" : "/login?from=" + from)
.setHeader("Set-Cookie", "auth=expired; Path=/; Max-Age=1")
.build();
}
response = responseFactory.basicAuthFail(thrownByAuthentication);
}
if (bruteForceGuard.shouldPreventRequest(accessor)) {
@ -143,7 +152,7 @@ public class RequestHandler implements HttpHandler {
}
private WebUser getWebUser(HttpExchange exchange) {
return getAuthorization(exchange.getRequestHeaders())
return getAuthentication(exchange.getRequestHeaders())
.map(Authentication::getWebUser) // Can throw WebUserAuthException
.map(com.djrapitops.plan.delivery.domain.WebUser::toNewWebUser)
.orElse(null);
@ -158,11 +167,23 @@ public class RequestHandler implements HttpHandler {
return headers;
}
private Optional<Authentication> getAuthorization(Headers requestHeaders) {
private Optional<Authentication> getAuthentication(Headers requestHeaders) {
if (config.isTrue(WebserverSettings.DISABLED_AUTHENTICATION)) {
return Optional.empty();
}
List<String> cookies = requestHeaders.get("Cookie");
if (cookies != null && !cookies.isEmpty()) {
for (String cookie : new TextStringBuilder().appendWithSeparators(cookies, ";").build().split(";")) {
String[] split = cookie.trim().split("=", 2);
String name = split[0];
String value = split[1];
if ("auth".equals(name)) {
return Optional.of(new CookieAuthentication(value));
}
}
}
List<String> authorization = requestHeaders.get("Authorization");
if (Verify.isEmpty(authorization)) return Optional.empty();

View File

@ -338,7 +338,9 @@ public class ResponseFactory {
return Response.builder()
.setMimeType(MimeType.HTML)
.setContent("<h1>403 Forbidden</h1>" +
"<p>You have too many failed login attempts. Please wait 2 minutes until attempting again.</p>")
"<p>You have too many failed login attempts. Please wait 2 minutes until attempting again.</p>" +
"<script>setTimeout(() => location.reload(), 120500);\" +\n" +
"</script>")
.setStatus(403)
.build();
}
@ -381,4 +383,20 @@ public class ResponseFactory {
return forInternalError(e, "Failed to generate player page");
}
}
public Response loginPageResponse() {
try {
return forPage(pageFactory.loginPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate player page");
}
}
public Response registerPageResponse() {
try {
return forPage(pageFactory.registerPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate player page");
}
}
}

View File

@ -25,8 +25,11 @@ 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.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.resolver.*;
import com.djrapitops.plan.delivery.webserver.resolver.auth.*;
import com.djrapitops.plan.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.connection.ForbiddenException;
import com.djrapitops.plugin.logging.L;
import com.djrapitops.plugin.logging.error.ErrorHandler;
@ -56,6 +59,11 @@ public class ResponseResolver {
private final RootPageResolver rootPageResolver;
private final RootJSONResolver rootJSONResolver;
private final StaticResourceResolver staticResourceResolver;
private LoginPageResolver loginPageResolver;
private RegisterPageResolver registerPageResolver;
private LoginResolver loginResolver;
private LogoutResolver logoutResolver;
private RegisterResolver registerResolver;
private final ErrorHandler errorHandler;
private final ResolverService resolverService;
@ -76,6 +84,12 @@ public class ResponseResolver {
RootJSONResolver rootJSONResolver,
StaticResourceResolver staticResourceResolver,
LoginPageResolver loginPageResolver,
RegisterPageResolver registerPageResolver,
LoginResolver loginResolver,
LogoutResolver logoutResolver,
RegisterResolver registerResolver,
ErrorHandler errorHandler
) {
this.resolverService = resolverService;
@ -88,6 +102,11 @@ public class ResponseResolver {
this.rootPageResolver = rootPageResolver;
this.rootJSONResolver = rootJSONResolver;
this.staticResourceResolver = staticResourceResolver;
this.loginPageResolver = loginPageResolver;
this.registerPageResolver = registerPageResolver;
this.loginResolver = loginResolver;
this.logoutResolver = logoutResolver;
this.registerResolver = registerResolver;
this.errorHandler = errorHandler;
}
@ -99,6 +118,13 @@ public class ResponseResolver {
resolverService.registerResolver(plugin, "/favicon.ico", (NoAuthResolver) request -> Optional.of(responseFactory.faviconResponse()));
resolverService.registerResolver(plugin, "/network", serverPageResolver);
resolverService.registerResolver(plugin, "/server", serverPageResolver);
resolverService.registerResolver(plugin, "/login", loginPageResolver);
resolverService.registerResolver(plugin, "/register", registerPageResolver);
resolverService.registerResolver(plugin, "/auth/login", loginResolver);
resolverService.registerResolver(plugin, "/auth/logout", logoutResolver);
resolverService.registerResolver(plugin, "/auth/register", registerResolver);
resolverService.registerResolverForMatches(plugin, Pattern.compile("^/$"), rootPageResolver);
resolverService.registerResolverForMatches(plugin, Pattern.compile("^.*/(vendor|css|js|img)/.*"), staticResourceResolver);
@ -114,6 +140,8 @@ public class ResponseResolver {
return responseFactory.forbidden403(e.getMessage());
} catch (BadRequestException e) {
return responseFactory.badRequest(e.getMessage(), request.getPath().asString());
} catch (WebUserAuthException e) {
throw e; // Pass along
} catch (Exception e) {
errorHandler.log(L.ERROR, this.getClass(), e);
return responseFactory.internalErrorResponse(e, request.getPath().asString());
@ -141,7 +169,7 @@ public class ResponseResolver {
if (isAuthRequired) {
if (!user.isPresent()) {
if (webServer.get().isUsingHTTPS()) {
return responseFactory.basicAuth();
throw new WebUserAuthException(FailReason.NO_USER_PRESENT);
} else {
return responseFactory.forbidden403();
}

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.webserver.cache.JSONCache;
import javax.inject.Inject;
@ -45,6 +46,13 @@ public class WebServerSystem implements SubSystem {
@Override
public void enable() {
webServer.enable();
if (!webServer.isAuthRequired()) {
ResourceService.getInstance().addStylesToResource("Plan", "error.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "server.html", ResourceService.Position.PRE_CONTENT, "../css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "player.html", ResourceService.Position.PRE_CONTENT, "../css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "players.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "network.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
}
}
@Override

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
public class ActiveCookieStore {
private static final Map<String, WebUser> USERS_BY_COOKIE = new HashMap<>();
public static Optional<WebUser> checkCookie(String cookie) {
return Optional.ofNullable(USERS_BY_COOKIE.get(cookie));
}
public static String generateNewCookie(WebUser user) {
String cookie = DigestUtils.sha256Hex(user.getName() + UUID.randomUUID() + System.currentTimeMillis());
USERS_BY_COOKIE.put(cookie, user);
return cookie;
}
public static void removeCookie(String cookie) {
USERS_BY_COOKIE.remove(cookie);
}
public static void removeCookie(WebUser user) {
USERS_BY_COOKIE.entrySet().stream().filter(entry -> entry.getValue().getName().equals(user.getName()))
.findAny()
.map(Map.Entry::getKey)
.ifPresent(ActiveCookieStore::removeCookie);
}
}

View File

@ -0,0 +1,33 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
public class CookieAuthentication implements Authentication {
private final String cookie;
public CookieAuthentication(String cookie) {
this.cookie = cookie;
}
@Override
public WebUser getWebUser() {
return ActiveCookieStore.checkCookie(cookie).orElse(null);
}
}

View File

@ -25,6 +25,8 @@ import com.djrapitops.plan.settings.locale.lang.Lang;
* @see com.djrapitops.plan.exceptions.WebUserAuthException
*/
public enum FailReason implements Lang {
NO_USER_PRESENT("User cookie not present"),
EXPIRED_COOKIE("User cookie has expired"),
USER_AND_PASS_NOT_SPECIFIED("User and Password not specified"),
USER_DOES_NOT_EXIST("User does not exist"),
USER_PASS_MISMATCH("User and Password did not match"),

View File

@ -0,0 +1,46 @@
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class RegistrationBin {
private static final Map<String, AwaitingForRegistration> REGISTRATION_BIN = new HashMap<>();
public static String addInfoForRegistration(String username, String password) throws PassEncryptUtil.CannotPerformOperationException {
String hash = PassEncryptUtil.createHash(password);
String code = DigestUtils.sha256Hex(username + password + System.currentTimeMillis()).substring(0, 12);
REGISTRATION_BIN.put(code, new AwaitingForRegistration(username, hash));
return code;
}
public static Optional<WebUser> register(String code, int permissionLevel) {
AwaitingForRegistration found = REGISTRATION_BIN.get(code);
if (found == null) return Optional.empty();
REGISTRATION_BIN.remove(code);
return Optional.of(found.toWebUser(permissionLevel));
}
public static boolean contains(String code) {
return REGISTRATION_BIN.containsKey(code);
}
private static class AwaitingForRegistration {
private final String username;
private final String passwordHash;
public AwaitingForRegistration(String username, String passwordHash) {
this.username = username;
this.passwordHash = passwordHash;
}
public WebUser toWebUser(int permissionLevel) {
return new WebUser(username, passwordHash, permissionLevel);
}
}
}

View File

@ -23,6 +23,8 @@ import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.WebServer;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import dagger.Lazy;
@ -62,12 +64,8 @@ public class RootPageResolver implements NoAuthResolver {
return responseFactory.redirectResponse(redirectTo);
}
Optional<WebUser> webUser = request.getUser();
if (!webUser.isPresent()) {
return responseFactory.basicAuth();
}
WebUser user = webUser.get();
WebUser user = request.getUser()
.orElseThrow(() -> new WebUserAuthException(FailReason.NO_USER_PRESENT));
if (user.hasPermission("page.server")) {
return responseFactory.redirectResponse(server.isProxy() ? "network" : "server/" + Html.encodeToURL(server.getIdentifiableName()));

View File

@ -0,0 +1,55 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
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.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.WebServer;
import dagger.Lazy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class LoginPageResolver implements NoAuthResolver {
private ResponseFactory responseFactory;
private Lazy<WebServer> webServer;
@Inject
public LoginPageResolver(
ResponseFactory responseFactory,
Lazy<WebServer> webServer
) {
this.responseFactory = responseFactory;
this.webServer = webServer;
}
@Override
public Optional<Response> resolve(Request request) {
Optional<WebUser> user = request.getUser();
if (user.isPresent() || !webServer.get().isAuthRequired()) {
Optional<String> from = request.getQuery().get("from");
return Optional.of(responseFactory.redirectResponse(from.orElse("/")));
}
return Optional.of(responseFactory.loginPageResponse());
}
}

View File

@ -0,0 +1,83 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
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.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.exceptions.PassEncryptException;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Singleton
public class LoginResolver implements NoAuthResolver {
private DBSystem dbSystem;
@Inject
public LoginResolver(
DBSystem dbSystem
) {
this.dbSystem = dbSystem;
}
@Override
public Optional<Response> resolve(Request request) {
try {
String cookie = ActiveCookieStore.generateNewCookie(getWebUser(request));
return Optional.of(getResponse(cookie));
} catch (DBOpException | PassEncryptException e) {
throw new WebUserAuthException(e);
}
}
public Response getResponse(String cookie) {
return Response.builder()
.setStatus(200)
.setHeader("Set-Cookie", "auth=" + cookie + "; Path=/; Max-Age=" + TimeUnit.HOURS.toSeconds(2L))
.setJSONContent(Collections.singletonMap("success", true))
.build();
}
public WebUser getWebUser(Request request) throws PassEncryptUtil.CannotPerformOperationException, PassEncryptUtil.InvalidHashException {
URIQuery query = request.getQuery();
String username = query.get("user").orElseThrow(() -> new BadRequestException("'user' parameter not defined"));
String password = query.get("password").orElseThrow(() -> new BadRequestException("'password' parameter not defined"));
WebUser webUser = dbSystem.getDatabase().query(WebUserQueries.fetchWebUser(username))
.orElseThrow(() -> new BadRequestException(FailReason.USER_DOES_NOT_EXIST.getReason() + ": " + username));
boolean correctPass = PassEncryptUtil.verifyPassword(password, webUser.getSaltedPassHash());
if (!correctPass) {
throw new WebUserAuthException(FailReason.USER_PASS_MISMATCH);
}
return webUser;
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
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.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class LogoutResolver implements NoAuthResolver {
@Inject
public LogoutResolver() {}
@Override
public Optional<Response> resolve(Request request) {
String cookies = request.getHeader("Cookie").orElse("");
String foundCookie = null;
for (String cookie : cookies.split(";")) {
if (cookie.isEmpty()) continue;
String[] split = cookie.split("=");
String name = split[0];
String value = split[1];
if ("auth".equals(name)) {
foundCookie = value;
ActiveCookieStore.removeCookie(value);
}
}
if (foundCookie == null) {
throw new WebUserAuthException(FailReason.NO_USER_PRESENT);
}
return Optional.of(getResponse(foundCookie));
}
public Response getResponse(String cookie) {
return Response.builder()
.setStatus(200)
.setHeader("Set-Cookie", "auth=" + cookie + "; Max-Age=1")
.setMimeType(MimeType.HTML)
.setContent(
"<p>Logging out..</p><script>const urlParams = new URLSearchParams(window.location.search);" +
"const cause = urlParams.get('cause');" +
"setTimeout(() => window.location.href = cause ? '../login?cause=' + cause : '../login', 1000);" +
"</script>"
)
.build();
}
}

View File

@ -0,0 +1,54 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
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.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.WebServer;
import dagger.Lazy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class RegisterPageResolver implements NoAuthResolver {
private ResponseFactory responseFactory;
private Lazy<WebServer> webServer;
@Inject
public RegisterPageResolver(
ResponseFactory responseFactory,
Lazy<WebServer> webServer
) {
this.responseFactory = responseFactory;
this.webServer = webServer;
}
@Override
public Optional<Response> resolve(Request request) {
Optional<WebUser> user = request.getUser();
if (user.isPresent() || !webServer.get().isAuthRequired()) {
return Optional.of(responseFactory.redirectResponse("/"));
}
return Optional.of(responseFactory.registerPageResponse());
}
}

View File

@ -0,0 +1,78 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
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.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.webserver.auth.RegistrationBin;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import com.djrapitops.plan.utilities.java.Maps;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.Optional;
@Singleton
public class RegisterResolver implements NoAuthResolver {
private DBSystem dbSystem;
@Inject
public RegisterResolver(DBSystem dbSystem) {this.dbSystem = dbSystem;}
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));
}
public Response getResponse(Request request) {
URIQuery query = request.getQuery();
Optional<String> checkCode = query.get("code");
if (checkCode.isPresent()) {
return Response.builder()
.setStatus(200)
.setJSONContent(Collections.singletonMap("success", !RegistrationBin.contains(checkCode.get())))
.build();
}
String username = query.get("user").orElseThrow(() -> new BadRequestException("'user' parameter not defined"));
boolean alreadyExists = dbSystem.getDatabase().query(WebUserQueries.fetchWebUser(username)).isPresent();
if (alreadyExists) throw new BadRequestException("User '" + username + "' already exists!");
String password = query.get("password").orElseThrow(() -> new BadRequestException("'password' parameter not defined"));
try {
String code = RegistrationBin.addInfoForRegistration(username, password);
return Response.builder()
.setStatus(200)
.setJSONContent(Maps.builder(String.class, Object.class)
.put("success", true)
.put("code", code)
.build())
.build();
} catch (PassEncryptUtil.CannotPerformOperationException e) {
throw new IllegalStateException(e);
}
}
}

View File

@ -1,3 +1,19 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.modules;
import com.djrapitops.plan.placeholder.*;

View File

@ -1,3 +1,19 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.placeholder;
public interface Placeholders {

View File

@ -0,0 +1,3 @@
#logout-button {
display: none;
}

View File

@ -8604,6 +8604,10 @@ button.bg-dark:focus {
box-shadow: none !important;
}
.w-15 {
width: 15% !important;
}
.w-22 {
width: 22% !important;
}
@ -11707,6 +11711,7 @@ a:focus {
.hidden {
width: 0 !important;
display: none;
}
.hidden .sidebar .sidebar-brand img {

View File

@ -755,6 +755,10 @@ div.scrollbar {
background-color: rgba(0, 0, 0, 0.08);
}
.bg-gradient {
background: rgba(0, 0, 0, 0) linear-gradient(180deg, rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 0.2) 100%);
}
.bg-red {
background-color: #F44336;
color: #fff;

View File

@ -32,7 +32,7 @@
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center">
<img class="w-22" src="img/Flaticon_circle.png">
<img alt="logo" class="w-22" src="img/Flaticon_circle.png">
</a>
<!-- Divider -->
@ -54,6 +54,9 @@
<button class="btn bg-plan" data-target="#informationModal" data-toggle="modal" type="button">
<i class="fa fa-fw fa-question-circle"></i>
</button>
<a class="btn bg-plan" href="/auth/logout" id="logout-button">
<i class="fa fa-fw fa-door-open"></i> Logout
</a>
</div>
<div class="ml-3 align-items-center justify-content-between">
${version}

View File

@ -1,6 +1,6 @@
(function ($) {
var bgElements = ['.sidebar', '.btn'];
var textElements = [];
var bgElements = ['.sidebar', '.btn', 'body'];
var textElements = ['a', 'button'];
var colors = ['plan',
'red', 'pink', 'purple', 'deep-purple',

View File

@ -0,0 +1,71 @@
function drawSine(canvasId) {
// https://gist.github.com/gkhays/e264009c0832c73d5345847e673a64ab
function drawPoint(ctx, x, y) {
var radius = 2;
ctx.beginPath();
// Hold x constant at 4 so the point only moves up and down.
ctx.arc(x - 5, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
}
function plotSine(ctx, xOffset, yOffset) {
var width = ctx.canvas.width;
var height = ctx.canvas.height;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#fff";
// console.log("Drawing point...");
var x = -2;
var y = 0;
var amplitude = 50;
var frequency = 50;
// ctx.moveTo(x, y);
ctx.moveTo(x, 50);
while (x <= width) {
y = height / 2 + amplitude * Math.sin((x + xOffset) / frequency) * Math.cos((x + xOffset) / (frequency * 0.54515978463));
ctx.lineTo(x, y);
x += 5;
// console.log("x="+x+" y="+y);
}
ctx.stroke();
ctx.save();
drawPoint(ctx, x, y);
ctx.stroke();
ctx.restore();
}
function draw() {
var canvas = document.getElementById(canvasId);
var context = canvas.getContext("2d");
context.clearRect(0, 0, 1000, 150);
context.save();
plotSine(context, step, 100);
context.restore();
step += 0.5;
window.requestAnimationFrame(draw);
}
function fix_dpi() {
var canvas = document.getElementById(canvasId);
let dpi = window.devicePixelRatio;//get canvas
let ctx = canvas.getContext('2d');
let style_width = +getComputedStyle(canvas).getPropertyValue("width").slice(0, -2);//scale the canvascanvas.setAttribute('height', style_height * dpi);
canvas.setAttribute('width', style_width * dpi);
}
fix_dpi();
var step = -1;
window.requestAnimationFrame(draw);
}

View File

@ -6,6 +6,7 @@
function jsonRequest(address, callback) {
setTimeout(function () {
var xhttp = new XMLHttpRequest();
xhttp.withCredentials = true;
xhttp.onreadystatechange = function () {
if (this.readyState === 4) {
try {
@ -17,9 +18,10 @@ function jsonRequest(address, callback) {
} else if (this.status === 404 || this.status === 403 || this.status === 500) {
callback(null, "HTTP " + this.status + " (See " + address + ")")
} else if (this.status === 400) {
callback(null, this.responseText + " (See " + address + ")")
const json = JSON.parse(this.responseText);
callback(json, json.error)
} else if (this.status === 0) {
callback(null, "Request was blocked. (Adblocker maybe?)")
callback(null, "Request did not reach the server. (Server offline / Adblocker?)")
}
} catch (e) {
callback(null, e.message + " (See " + address + ")")

View File

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
<meta content="Player Analytics, login page" name="description">
<meta content="Rsl1122" name="author">
<title>Plan | Login</title>
<!-- Custom fonts for this template-->
<link href="vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link crossorigin="anonymous" href="https://fonts.googleapis.com/css?family=Nunito:300,400,600,700,800,900"
rel="stylesheet">
<!-- Custom styles for this template-->
<link href="css/sb-admin-2.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body class="bg-plan bg-gradient">
<div class="container">
<!-- Outer Row -->
<div class="row justify-content-center">
<img alt="logo" class="w-15 mt-5" src="img/Flaticon_circle.png">
</div>
<div class="row justify-content-center">
<div class="col-xl-6 col-lg-7 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-12">
<div class="p-5">
<div class="hidden alert alert-danger" id="fail-msg">
Failed
</div>
<form class="user">
<div class="form-group">
<input autocomplete="username" class="form-control form-control-user"
id="inputUser"
placeholder="Username" type="text">
</div>
<div class="form-group">
<input autocomplete="current-password" class="form-control form-control-user"
id="inputPassword" placeholder="Password"
type="password">
</div>
<a class="btn bg-plan btn-user btn-block" href="#" id="login-button">
Login
</a>
</form>
<hr>
<div class="text-center">
<a class="col-plan small" href="#" id="forgot-button">Forgot Password?</a>
</div>
<div class="text-center">
<a class="col-plan small" href="./register">Create an Account!</a>
</div>
<div class="text-center">
<button class="btn col-plan" data-target="#colorChooserModal" data-toggle="modal"
type="button">
<i class="fa fa-palette"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<canvas class="col-xl-3 col-lg-3 col-md-5" id="decoration" style="height: 100px"></canvas>
</div>
</div>
<!-- Forgot Password Modal -->
<div aria-hidden="true" aria-labelledby="forgotModalLabel" class="modal fade" id="forgotModal"
role="dialog" tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-white">
<h5 class="modal-title" id="forgotModalLabel"><i class="far fa-fw fa-hand-point-right"></i>
Forgot password? Unregister and register again.
</h5>
<button aria-label="Close" class="close" data-dismiss="modal" type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body bg-white">
<p>Use the following command in game to remove your current user:</p>
<p><code>/${command} unregister</code></p>
<p>Or using console:</p>
<p><code>${command} unregister [username]</code></p>
<p>After using the command, <a class="col-plan" href="./register">Create an Account!</a></p>
</div>
</div>
</div>
</div>
<!-- Color Chooser Modal -->
<div aria-hidden="true" aria-labelledby="colorChooserModalLabel" class="modal fade" id="colorChooserModal"
role="dialog" tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="colorChooserModalLabel"><i class="fa fa-palette"></i> Theme Select
</h5>
<button aria-label="Close" class="close" data-dismiss="modal" type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<button class="btn color-chooser" id="choose-plan" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-red" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-pink" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-purple" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-deep-purple" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-indigo" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-blue" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-light-blue" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-cyan" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-teal" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-green" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-light-green" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-lime" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-yellow" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-amber" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-orange" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-deep-orange" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-brown" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-grey" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-blue-grey" type="button"><i
class="fa fa-palette"></i></button>
</div>
<div class="modal-footer">
<button class="btn" id="night-mode-toggle" type="button"><i class="fa fa-fw fa-cloud-moon"></i>
Night
Mode
</button>
<button class="btn bg-plan" data-dismiss="modal" type="button">OK</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="vendor/jquery/jquery.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="js/sb-admin-2.min.js"></script>
<script src="js/xmlhttprequests.js"></script>
<script src="js/color-selector.js"></script>
<script src="js/logonsine.js"></script>
<script id="mainScript">
drawSine("decoration");
const msg = $(`#fail-msg`);
function displayError(message) {
msg.text(message);
msg.removeClass('hidden');
}
$('#forgot-button').click(() => $('#forgotModal').modal());
$('#login-button').click(() => {
msg.addClass('hidden');
const user = $('#inputUser').val();
if (!user || user.length < 1) {
return displayError('You need to specify a Username');
}
if (user.length > 50) {
return displayError('Username can be up to 50 characters, yours is ' + user.length);
}
const password = $('#inputPassword').val();
if (!password || password.length < 1) {
return displayError('You need to specify a Password');
}
jsonRequest(`./auth/login?user=${encodeURI(user)}&password=${encodeURI(password)}`, (json, error) => {
if (error) {
if (error.includes("HTTP 403")) {
location.reload();
}
$('#inputPassword').val("");
return displayError('Login failed: ' + error);
}
if (json && json.success) {
const urlParams = new URLSearchParams(window.location.search);
const cameFrom = urlParams.get('from');
window.location.href = cameFrom ? cameFrom : './';
} else {
return displayError('Login failed: ' + json.error);
}
});
})
</script>
</body>
</html>

View File

@ -41,7 +41,7 @@
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center">
<img class="w-22" src="./img/Flaticon_circle.png">
<img alt="logo" class="w-22" src="./img/Flaticon_circle.png">
</a>
<!-- Divider -->
@ -120,6 +120,9 @@
<button class="btn bg-plan" data-target="#informationModal" data-toggle="modal" type="button">
<i class="fa fa-fw fa-question-circle"></i>
</button>
<a class="btn bg-plan" href="/auth/logout" id="logout-button">
<i class="fa fa-fw fa-door-open"></i> Logout
</a>
</div>
<div class="ml-3 align-items-center justify-content-between">
${version}

View File

@ -41,7 +41,7 @@
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center">
<img class="w-22" src="../img/Flaticon_circle.png">
<img alt="logo" class="w-22" src="../img/Flaticon_circle.png">
</a>
<!-- Divider -->
@ -108,6 +108,9 @@
<button class="btn bg-plan" data-target="#informationModal" data-toggle="modal" type="button">
<i class="fa fa-fw fa-question-circle"></i>
</button>
<a class="btn bg-plan" href="/auth/logout" id="logout-button">
<i class="fa fa-fw fa-door-open"></i> Logout
</a>
</div>
<div class="ml-3 align-items-center justify-content-between">
${version}

View File

@ -32,7 +32,7 @@
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center">
<img class="w-22" src="img/Flaticon_circle.png">
<img alt="logo" class="w-22" src="img/Flaticon_circle.png">
</a>
<!-- Divider -->
@ -54,6 +54,9 @@
<button class="btn bg-plan" data-target="#informationModal" data-toggle="modal" type="button">
<i class="fa fa-fw fa-question-circle"></i>
</button>
<a class="btn bg-plan" href="/auth/logout" id="logout-button">
<i class="fa fa-fw fa-door-open"></i> Logout
</a>
</div>
<div class="ml-3 align-items-center justify-content-between">
${version}

View File

@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
<meta content="Player Analytics, login page" name="description">
<meta content="Rsl1122" name="author">
<title>Plan | Register</title>
<!-- Custom fonts for this template-->
<link href="vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link crossorigin="anonymous" href="https://fonts.googleapis.com/css?family=Nunito:300,400,600,700,800,900"
rel="stylesheet">
<!-- Custom styles for this template-->
<link href="css/sb-admin-2.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body class="bg-plan bg-gradient">
<div class="container">
<!-- Outer Row -->
<div class="row justify-content-center">
<img alt="logo" class="w-15 mt-5" src="img/Flaticon_circle.png">
</div>
<div class="row justify-content-center">
<div class="col-xl-6 col-lg-7 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-12">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Create a new user</h1>
</div>
<div class="hidden alert alert-danger" id="fail-msg">
Failed
</div>
<form class="user">
<div class="form-group">
<input autocomplete="username" class="form-control form-control-user"
id="inputUser"
placeholder="Username" type="text">
<small class="form-text text-muted" id="usernameHelpBlock">
Username can be up to 50 characters.
</small>
</div>
<div class="form-group">
<input autocomplete="new-password" class="form-control form-control-user"
id="inputPassword" placeholder="Password" type="password">
<small class="form-text text-muted" id="passwordHelpBlock">
Password should be more than 8 characters, but there are no limitations.
</small>
</div>
<a class="btn bg-plan btn-user btn-block" href="#" id="register-button">
Register
</a>
</form>
<hr>
<div class="text-center">
<a class="col-plan small" href="./login">Already have an account? Login!</a>
</div>
<div class="text-center">
<button class="btn col-plan" data-target="#colorChooserModal" data-toggle="modal"
type="button">
<i class="fa fa-palette"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Finalize Register Modal -->
<div aria-hidden="true" aria-labelledby="finalizeModalLabel" class="modal fade" id="finalizeModal"
role="dialog" tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-white">
<h5 class="modal-title" id="finalizeModalLabel"><i class="far fa-fw fa-hand-point-right"></i>
Complete Registration
</h5>
<button aria-label="Close" class="close" data-dismiss="modal" type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body bg-white">
<p>You can now finish registering the user. The registration should be completed before next
restart.</p>
<p>Use the following command in game to finish registration:</p>
<p><code>/${command} register --code <span class="register-code"></span></code></p>
<p>Or using console:</p>
<p><code>${command} register superuser --code <span class="register-code"></span></code></p>
</div>
</div>
</div>
</div>
<!-- Color Chooser Modal -->
<div aria-hidden="true" aria-labelledby="colorChooserModalLabel" class="modal fade" id="colorChooserModal"
role="dialog" tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="colorChooserModalLabel"><i class="fa fa-palette"></i> Theme Select
</h5>
<button aria-label="Close" class="close" data-dismiss="modal" type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<button class="btn color-chooser" id="choose-plan" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-red" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-pink" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-purple" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-deep-purple" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-indigo" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-blue" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-light-blue" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-cyan" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-teal" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-green" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-light-green" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-lime" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-yellow" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-amber" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-orange" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-deep-orange" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-brown" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-grey" type="button"><i
class="fa fa-palette"></i></button>
<button class="btn color-chooser" id="choose-blue-grey" type="button"><i
class="fa fa-palette"></i></button>
</div>
<div class="modal-footer">
<button class="btn" id="night-mode-toggle" type="button"><i class="fa fa-fw fa-cloud-moon"></i>
Night
Mode
</button>
<button class="btn bg-plan" data-dismiss="modal" type="button">OK</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="vendor/jquery/jquery.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="js/sb-admin-2.min.js"></script>
<script src="js/xmlhttprequests.js"></script>
<script src="js/color-selector.js"></script>
<script id="mainScript">
const msg = $(`#fail-msg`);
function displayError(message) {
msg.text(message);
msg.removeClass('hidden');
}
function checkIfRegistered(code) {
jsonRequest(`./auth/register?code=${encodeURI(code)}`, (json, error) => {
if (error) {
displayError('Checking registration status failed: ' + error)
}
if (json && json.success) {
window.location.href = "./login";
} else {
setTimeout(() => checkIfRegistered(code), 5000);
}
})
}
$('#register-button').click(() => {
msg.addClass('hidden');
const user = $('#inputUser').val();
if (!user || user.length < 1) {
return displayError('You need to specify a Username');
}
if (user.length > 50) {
return displayError('Username can be up to 50 characters, yours is ' + user.length);
}
const password = $('#inputPassword').val();
if (!password || password.length < 1) {
return displayError('You need to specify a Password');
}
jsonRequest(`./auth/register?user=${encodeURI(user)}&password=${encodeURI(password)}`, (json, error) => {
if (error) {
return displayError('Registration failed: ' + error);
}
const code = json.code;
$('.register-code').text(code);
$('#finalizeModal').modal();
setTimeout(() => checkIfRegistered(code), 10000);
});
})
</script>
</body>
</html>

View File

@ -39,7 +39,7 @@
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center">
<img class="w-22" src="../img/Flaticon_circle.png">
<img alt="logo" class="w-22" src="../img/Flaticon_circle.png">
</a>
<!-- Divider -->
@ -126,6 +126,9 @@
<button class="btn bg-plan" data-target="#informationModal" data-toggle="modal" type="button">
<i class="fa fa-fw fa-question-circle"></i>
</button>
<a class="btn bg-plan" href="/auth/logout" id="logout-button">
<i class="fa fa-fw fa-door-open"></i> Logout
</a>
</div>
<div class="ml-3 align-items-center justify-content-between">
${version}