mirror of
https://github.com/linuxserver/fleet.git
synced 2026-02-20 05:11:08 +08:00
Merge pull request #1 from linuxserver/1.1.0_database_auth
1.1.0 database auth
This commit is contained in:
commit
b7aa227d09
25
build.gradle
25
build.gradle
@ -11,7 +11,24 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
version = '1.0.1'
|
||||
version = '1.1.0'
|
||||
|
||||
sourceSets {
|
||||
|
||||
main {
|
||||
|
||||
java {
|
||||
srcDir 'src/main/java'
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
java {
|
||||
srcDir 'src/test/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@ -37,11 +54,11 @@ dependencies {
|
||||
|
||||
// MISC
|
||||
compile 'org.apache.commons:commons-lang3:3.7'
|
||||
|
||||
compile 'org.bouncycastle:bcprov-jdk15on:1.61'
|
||||
|
||||
// Unit Testing
|
||||
testCompile 'junit:junit:4.11'
|
||||
testCompile 'org.mockito:mockito-all:1.10.19'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
}
|
||||
|
||||
jar {
|
||||
@ -82,7 +99,7 @@ minifyJs {
|
||||
combineCss {
|
||||
|
||||
source = [
|
||||
'src/main/resources/assets/css/bootstrap.min.css',
|
||||
'src/main/resources/assets/css/bootstrap.css',
|
||||
'src/main/resources/assets/css/fleet.css',
|
||||
'src/main/resources/assets/css/fontawesome-all.css'
|
||||
]
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
fleet.app.port=8080
|
||||
fleet.refresh.interval=60
|
||||
|
||||
# If set to DATABASE, fleet.admin.username and fleet.admin.password are not used. They can be omitted.
|
||||
fleet.admin.authentication.type=PROPERTIES|DATABASE
|
||||
|
||||
# User for management of images and repositories
|
||||
# CHANGE THESE!!
|
||||
fleet.admin.username=test
|
||||
|
||||
38
src/main/java/io/linuxserver/fleet/auth/UserCredentials.java
Normal file
38
src/main/java/io/linuxserver/fleet/auth/UserCredentials.java
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth;
|
||||
|
||||
public class UserCredentials {
|
||||
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public UserCredentials(String username, String password) {
|
||||
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.authenticator;
|
||||
|
||||
import io.linuxserver.fleet.auth.security.PKCS5S2PasswordEncoder;
|
||||
import io.linuxserver.fleet.core.FleetBeans;
|
||||
import io.linuxserver.fleet.core.FleetProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AuthenticatorFactory {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticatorFactory.class);
|
||||
|
||||
public static UserAuthenticator getAuthenticator(FleetBeans beans) {
|
||||
|
||||
FleetProperties properties = beans.getProperties();
|
||||
|
||||
AuthenticationType authType = AuthenticationType.valueOf(properties.getAuthenticationType().toUpperCase());
|
||||
switch (authType) {
|
||||
|
||||
case DATABASE:
|
||||
|
||||
LOGGER.info("Configuring new authenticator: DatabaseStoredUserAuthenticator");
|
||||
return new DatabaseStoredUserAuthenticator(beans.getPasswordEncoder(), beans.getUserDelegate());
|
||||
|
||||
case PROPERTIES:
|
||||
default:
|
||||
|
||||
LOGGER.info("Configuring new authenticator: PropertyLoadedUserAuthenticator");
|
||||
return new PropertyLoadedUserAuthenticator(properties.getAppUsername(), properties.getAppPassword());
|
||||
}
|
||||
}
|
||||
|
||||
public enum AuthenticationType {
|
||||
PROPERTIES, DATABASE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.authenticator;
|
||||
|
||||
import io.linuxserver.fleet.auth.AuthenticatedUser;
|
||||
import io.linuxserver.fleet.auth.AuthenticationResult;
|
||||
import io.linuxserver.fleet.auth.UserCredentials;
|
||||
import io.linuxserver.fleet.auth.security.PasswordEncoder;
|
||||
import io.linuxserver.fleet.delegate.UserDelegate;
|
||||
import io.linuxserver.fleet.model.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DatabaseStoredUserAuthenticator implements UserAuthenticator {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseStoredUserAuthenticator.class);
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserDelegate userDelegate;
|
||||
|
||||
public DatabaseStoredUserAuthenticator(PasswordEncoder passwordEncoder, UserDelegate userDelegate) {
|
||||
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.userDelegate = userDelegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResult authenticate(UserCredentials userCredentials) {
|
||||
|
||||
User user = userDelegate.fetchUserByUsername(userCredentials.getUsername());
|
||||
if (null == user) {
|
||||
|
||||
LOGGER.warn("Attempt to log in with user '{}' failed. Not found.", userCredentials.getUsername());
|
||||
return AuthenticationResult.notAuthenticated();
|
||||
}
|
||||
|
||||
boolean authenticated = passwordEncoder.matches(userCredentials.getPassword(), user.getPassword());
|
||||
if (authenticated) {
|
||||
return new AuthenticationResult(authenticated, new AuthenticatedUser(user.getUsername()));
|
||||
}
|
||||
|
||||
LOGGER.warn("Unable to verify user credentials for user {}", userCredentials.getUsername());
|
||||
return AuthenticationResult.notAuthenticated();
|
||||
}
|
||||
}
|
||||
@ -15,27 +15,28 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.delegate;
|
||||
package io.linuxserver.fleet.auth.authenticator;
|
||||
|
||||
import io.linuxserver.fleet.auth.AuthenticatedUser;
|
||||
import io.linuxserver.fleet.auth.AuthenticationResult;
|
||||
import io.linuxserver.fleet.auth.UserCredentials;
|
||||
|
||||
public class PropertiesAuthenticationDelegate implements AuthenticationDelegate {
|
||||
public class PropertyLoadedUserAuthenticator implements UserAuthenticator {
|
||||
|
||||
private final String adminUsername;
|
||||
private final String adminPassword;
|
||||
|
||||
public PropertiesAuthenticationDelegate(String username, String password) {
|
||||
public PropertyLoadedUserAuthenticator(String adminUsername, String adminPassword) {
|
||||
|
||||
this.adminUsername = username;
|
||||
this.adminPassword = password;
|
||||
this.adminUsername = adminUsername;
|
||||
this.adminPassword = adminPassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResult authenticate(String username, String password) {
|
||||
public AuthenticationResult authenticate(UserCredentials userCredentials) {
|
||||
|
||||
if (adminUsername.equals(username) && adminPassword.equals(password))
|
||||
return new AuthenticationResult(true, new AuthenticatedUser(username));
|
||||
if (adminUsername.equals(userCredentials.getUsername()) && adminPassword.equals(userCredentials.getPassword()))
|
||||
return new AuthenticationResult(true, new AuthenticatedUser(userCredentials.getUsername()));
|
||||
|
||||
return AuthenticationResult.notAuthenticated();
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.authenticator;
|
||||
|
||||
import io.linuxserver.fleet.auth.AuthenticationResult;
|
||||
import io.linuxserver.fleet.auth.UserCredentials;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Provides a mechanism for the application to authenticate a login request
|
||||
* from a user.
|
||||
* </p>
|
||||
*/
|
||||
public interface UserAuthenticator {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Performs an authentication check against the provided credentials and the repository
|
||||
* of currently stored users.
|
||||
* </p>
|
||||
*/
|
||||
AuthenticationResult authenticate(UserCredentials userCredentials);
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.security;
|
||||
|
||||
import io.linuxserver.fleet.auth.security.util.SaltGenerator;
|
||||
import org.bouncycastle.crypto.PBEParametersGenerator;
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Uses the PKCS5S2 crypto algorithm to encode and verify hashed passwords.
|
||||
* </p>
|
||||
*/
|
||||
public class PKCS5S2PasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final int DEFAULT_HASH_WIDTH = 512;
|
||||
private static final int DEFAULT_ITERATIONS = 150051;
|
||||
|
||||
private final SaltGenerator saltGenerator = new SaltGenerator();
|
||||
|
||||
private final byte[] secret;
|
||||
private final int hashWidth;
|
||||
private final int iterations;
|
||||
|
||||
public PKCS5S2PasswordEncoder(String secret) {
|
||||
this(secret, DEFAULT_HASH_WIDTH, DEFAULT_ITERATIONS);
|
||||
}
|
||||
|
||||
public PKCS5S2PasswordEncoder(String secret, int hashWidth, int iterations) {
|
||||
|
||||
this.secret = secret.getBytes(StandardCharsets.UTF_8);
|
||||
this.hashWidth = hashWidth;
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encode(String rawPassword) {
|
||||
|
||||
if (null == rawPassword) {
|
||||
throw new IllegalArgumentException("Password must not be null");
|
||||
}
|
||||
|
||||
return toBase64(encode(rawPassword, saltGenerator.generateSalt()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String rawPassword, String encodedPassword) {
|
||||
|
||||
byte[] decodedHash = fromBase64(encodedPassword);
|
||||
byte[] saltInHash = extractSalt(decodedHash);
|
||||
byte[] hashToVerify = encode(rawPassword, saltInHash);
|
||||
|
||||
return passwordsMatch(decodedHash, hashToVerify);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Compares the two byte arrays by performing a bitwise equality check against each individual
|
||||
* element of both arrays.
|
||||
* </p>
|
||||
*
|
||||
* @implNote I looked at a couple of implementations for doing this, and I preferred how Spring had implemented it.
|
||||
*/
|
||||
private boolean passwordsMatch(byte[] originalPassword, byte[] providedPassword) {
|
||||
|
||||
if (originalPassword.length != providedPassword.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
for (int i = 0; i < originalPassword.length; i++) {
|
||||
result |= originalPassword[i] ^ providedPassword[i];
|
||||
}
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Performs the cryptographic hash against the raw password and the randomly generated salt value. This
|
||||
* also concatenates the provided secret into the salt.
|
||||
* </p>
|
||||
*/
|
||||
private byte[] encode(String rawPassword, byte[] salt) {
|
||||
|
||||
PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA256Digest());
|
||||
generator.init(
|
||||
PBEParametersGenerator.PKCS5PasswordToBytes(rawPassword.toCharArray()),
|
||||
joinArrays(salt, secret),
|
||||
iterations
|
||||
);
|
||||
|
||||
return joinArrays(salt, ((KeyParameter) generator.generateDerivedMacParameters(hashWidth)).getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Converts a byte array into a base-64 encoded string.
|
||||
* </p>
|
||||
*/
|
||||
private String toBase64(byte[] bytes) {
|
||||
return Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Converts a base64-encoded string into its raw byte value
|
||||
* </p>
|
||||
*/
|
||||
private byte[] fromBase64(String input) {
|
||||
return Base64.getDecoder().decode(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Obtains the specific bytes which represent the salt used in a previous hashed password. This is to
|
||||
* enable the comparision between the existing and new password.
|
||||
* </p>
|
||||
*/
|
||||
private byte[] extractSalt(byte[] decodedHash) {
|
||||
return extractFromArray(decodedHash, 0, saltGenerator.getKeyLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Combines two byte arrays together in order.
|
||||
* </p>
|
||||
*/
|
||||
private byte[] joinArrays(byte[] first, byte[] second) {
|
||||
|
||||
byte[] result = Arrays.copyOf(first, first.length + second.length);
|
||||
System.arraycopy(second, 0, result, first.length, second.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Extracts a sub-array from the provided array.
|
||||
* </p>
|
||||
*/
|
||||
private byte[] extractFromArray(byte[] array, int begin, int end) {
|
||||
|
||||
int length = end - begin;
|
||||
byte[] subarray = new byte[length];
|
||||
System.arraycopy(array, begin, subarray, 0, length);
|
||||
return subarray;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.security;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Provides a mechanism for a password to be encoded using a strong cryptographic algorithm.
|
||||
* The general idea of this interface has been taken from Spring's own implementation of this.
|
||||
* <a href="https://docs.spring.io/spring-security/site/docs/4.2.4.RELEASE/apidocs/org/springframework/security/crypto/password/PasswordEncoder.html">PasswordEncoder</a>
|
||||
* </p>
|
||||
*/
|
||||
public interface PasswordEncoder {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Encodes the raw password into a one-way encrypted hash. The result of which should be stored.
|
||||
* </p>
|
||||
*
|
||||
* @param rawPassword
|
||||
* The raw unencrypted password.
|
||||
*
|
||||
* @return
|
||||
* The hashed result.
|
||||
*/
|
||||
String encode(String rawPassword);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if the provided raw password, when encoded, matches the stored encoded password.
|
||||
* </p>
|
||||
*
|
||||
* @param rawPassword
|
||||
* The raw password to check
|
||||
* @param encodedPassword
|
||||
* The originally encoded and stored password
|
||||
* @return
|
||||
* true if the passwords match, false if not.
|
||||
*/
|
||||
boolean matches(String rawPassword, String encodedPassword);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.security.util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class SaltGenerator {
|
||||
|
||||
private static final int KEY_LENGTH = 16;
|
||||
|
||||
public byte[] generateSalt() {
|
||||
|
||||
SecureRandom sr = new SecureRandom();
|
||||
|
||||
byte[] salt = new byte[KEY_LENGTH];
|
||||
sr.nextBytes(salt);
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
public int getKeyLength() {
|
||||
return KEY_LENGTH;
|
||||
}
|
||||
}
|
||||
@ -17,25 +17,54 @@
|
||||
|
||||
package io.linuxserver.fleet.core;
|
||||
|
||||
import io.linuxserver.fleet.auth.AuthenticatedUser;
|
||||
import io.linuxserver.fleet.auth.authenticator.AuthenticatorFactory.AuthenticationType;
|
||||
import io.linuxserver.fleet.model.api.ApiResponse;
|
||||
import io.linuxserver.fleet.model.api.FleetApiException;
|
||||
import io.linuxserver.fleet.web.JsonTransformer;
|
||||
import io.linuxserver.fleet.web.SessionAttribute;
|
||||
import io.linuxserver.fleet.web.pages.HomePage;
|
||||
import io.linuxserver.fleet.web.pages.LoginPage;
|
||||
import io.linuxserver.fleet.web.pages.ManageRepositoriesPage;
|
||||
import io.linuxserver.fleet.web.pages.SetupPage;
|
||||
import io.linuxserver.fleet.web.routes.*;
|
||||
import io.linuxserver.fleet.web.websocket.SynchronisationWebSocket;
|
||||
import spark.Session;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static spark.Spark.*;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Primary entry point for the application. All contexts and resources are loaded
|
||||
* through this class.
|
||||
* </p>
|
||||
*/
|
||||
class FleetApp {
|
||||
public class FleetApp {
|
||||
|
||||
private static final String FLEET_USER_UNDEFINED = "fleet.user.undefined";
|
||||
|
||||
private static FleetApp instance;
|
||||
|
||||
public static FleetApp instance() {
|
||||
|
||||
if (null == instance) {
|
||||
|
||||
synchronized (FleetApp.class) {
|
||||
|
||||
if (null == instance) {
|
||||
instance = new FleetApp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final FleetBeans beans;
|
||||
|
||||
FleetApp() {
|
||||
private FleetApp() {
|
||||
beans = new FleetBeans();
|
||||
}
|
||||
|
||||
@ -52,25 +81,138 @@ class FleetApp {
|
||||
|
||||
private void configureWeb() {
|
||||
|
||||
port(beans.getProperties().getAppPort());
|
||||
|
||||
staticFiles.location("/assets");
|
||||
staticFiles.expireTime(600);
|
||||
|
||||
SynchronisationWebSocket synchronisationWebSocket = new SynchronisationWebSocket();
|
||||
beans.getSynchronisationDelegate().registerListener(synchronisationWebSocket);
|
||||
|
||||
beans.getWebServer().addWebSocket("/admin/ws/sync", synchronisationWebSocket);
|
||||
beans.getWebServer().start();
|
||||
webSocket("/admin/ws/sync", synchronisationWebSocket);
|
||||
init();
|
||||
|
||||
beans.getWebServer().addPage( "/", new HomePage(beans.getRepositoryDelegate(), beans.getImageDelegate()));
|
||||
beans.getWebServer().addGetApi( "/api/v1/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()));
|
||||
beans.getWebServer().addPage( "/admin", new ManageRepositoriesPage(beans.getRepositoryDelegate()));
|
||||
beans.getWebServer().addPage( "/admin/login", new LoginPage());
|
||||
beans.getWebServer().addPostRoute( "/admin/login", new LoginRoute(beans.getAuthenticationDelegate()));
|
||||
beans.getWebServer().addPostRoute( "/admin/logout", new LogoutRoute());
|
||||
beans.getWebServer().addPostApi( "/admin/manageImage", new ManageImageApi(beans.getImageDelegate()));
|
||||
beans.getWebServer().addGetApi( "/admin/getImage", new GetImageApi(beans.getImageDelegate()));
|
||||
beans.getWebServer().addPostApi( "/admin/manageRepository", new ManageRepositoryApi(beans.getRepositoryDelegate()));
|
||||
beans.getWebServer().addPostApi( "/admin/forceSync", new ForceSyncApi(beans.getTaskDelegate()));
|
||||
/* -----------------------
|
||||
* Set Up
|
||||
* -----------------------
|
||||
*/
|
||||
if (initialUserNeedsConfiguring()) {
|
||||
|
||||
path("/setup", () -> {
|
||||
|
||||
before("", (request, response) -> {
|
||||
|
||||
if (!initialUserNeedsConfiguring()) {
|
||||
halt(401);
|
||||
}
|
||||
});
|
||||
|
||||
get("", new SetupPage());
|
||||
post("", new RegisterInitialUserRoute(beans.getUserDelegate()));
|
||||
});
|
||||
}
|
||||
|
||||
/* -----------------------
|
||||
* Image List and Log In
|
||||
* -----------------------
|
||||
*/
|
||||
path("/", () -> {
|
||||
|
||||
get("", new HomePage(beans.getRepositoryDelegate(), beans.getImageDelegate()));
|
||||
get("/login", new LoginPage());
|
||||
post("/login", new LoginRoute(beans.getAuthenticationDelegate()));
|
||||
post("/logout", new LogoutRoute());
|
||||
});
|
||||
|
||||
/* -----------------------
|
||||
* API
|
||||
* -----------------------
|
||||
*/
|
||||
path("/api/v1", () -> {
|
||||
|
||||
get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer());
|
||||
|
||||
after("/*", (request, response) -> {
|
||||
|
||||
response.header("Access-Control-Allow-Origin", "*");
|
||||
response.header("Access-Control-Allow-Methods", "GET");
|
||||
response.header("Content-Type","application/json");
|
||||
});
|
||||
});
|
||||
|
||||
/* -----------------------
|
||||
* Admin
|
||||
* -----------------------
|
||||
*/
|
||||
path("/admin", () -> {
|
||||
|
||||
before("", (request, response) -> {
|
||||
|
||||
Session session = request.session(false);
|
||||
|
||||
if (null == session)
|
||||
response.redirect("/login");
|
||||
|
||||
else {
|
||||
|
||||
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
|
||||
if (null == user)
|
||||
response.redirect("/login");
|
||||
}
|
||||
});
|
||||
|
||||
before("/*", (request, response) -> {
|
||||
|
||||
Session session = request.session(false);
|
||||
|
||||
if (null == session)
|
||||
response.redirect("/login");
|
||||
|
||||
else {
|
||||
|
||||
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
|
||||
if (null == user)
|
||||
response.redirect("/login");
|
||||
}
|
||||
});
|
||||
|
||||
get("", new ManageRepositoriesPage(beans.getRepositoryDelegate()));
|
||||
|
||||
get("/api/getImage", new GetImageApi(beans.getImageDelegate()), new JsonTransformer());
|
||||
post("/api/manageImage", new ManageImageApi(beans.getImageDelegate()), new JsonTransformer());
|
||||
post("/api/manageRepository", new ManageRepositoryApi(beans.getRepositoryDelegate()), new JsonTransformer());
|
||||
post("/api/forceSync", new ForceSyncApi(beans.getTaskDelegate()), new JsonTransformer());
|
||||
|
||||
after("/api/*", (request, response) -> response.header("Content-Type", "application/json"));
|
||||
});
|
||||
|
||||
/* -----------------------
|
||||
* API Error Handling
|
||||
* -----------------------
|
||||
*/
|
||||
exception(FleetApiException.class, (exception, request, response) -> {
|
||||
|
||||
response.body(new JsonTransformer().render(new ApiResponse<>("ERROR", exception.getMessage())));
|
||||
response.header("Content-Type", "application/json");
|
||||
response.status(exception.getStatusCode());
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleSync() {
|
||||
beans.getTaskDelegate().scheduleSynchronisationTask(beans.getProperties().getRefreshIntervalInMinutes(), TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private boolean initialUserNeedsConfiguring() {
|
||||
|
||||
String configured = System.getProperty(FLEET_USER_UNDEFINED);
|
||||
if (null == configured || "true".equalsIgnoreCase(configured)) {
|
||||
System.setProperty(FLEET_USER_UNDEFINED, String.valueOf(beans.getUserDelegate().isUserRepositoryEmpty()));
|
||||
}
|
||||
|
||||
return "true".equalsIgnoreCase(System.getProperty(FLEET_USER_UNDEFINED)) && databaseAuthenticationEnabled();
|
||||
}
|
||||
|
||||
private boolean databaseAuthenticationEnabled() {
|
||||
return AuthenticationType.DATABASE == AuthenticationType.valueOf(beans.getProperties().getAuthenticationType());
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,14 +17,17 @@
|
||||
|
||||
package io.linuxserver.fleet.core;
|
||||
|
||||
import io.linuxserver.fleet.auth.authenticator.AuthenticatorFactory;
|
||||
import io.linuxserver.fleet.auth.security.PKCS5S2PasswordEncoder;
|
||||
import io.linuxserver.fleet.auth.security.PasswordEncoder;
|
||||
import io.linuxserver.fleet.db.DefaultDatabaseConnection;
|
||||
import io.linuxserver.fleet.db.dao.DefaultImageDAO;
|
||||
import io.linuxserver.fleet.db.dao.DefaultRepositoryDAO;
|
||||
import io.linuxserver.fleet.db.dao.DefaultUserDAO;
|
||||
import io.linuxserver.fleet.db.migration.DatabaseVersion;
|
||||
import io.linuxserver.fleet.delegate.*;
|
||||
import io.linuxserver.fleet.dockerhub.DockerHubV2Client;
|
||||
import io.linuxserver.fleet.thread.TaskManager;
|
||||
import io.linuxserver.fleet.web.WebServer;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -39,9 +42,10 @@ public class FleetBeans {
|
||||
private final AuthenticationDelegate authenticationDelegate;
|
||||
private final DockerHubDelegate dockerHubDelegate;
|
||||
private final SynchronisationDelegate synchronisationDelegate;
|
||||
private final WebServer webServer;
|
||||
private final TaskManager taskManager;
|
||||
private final TaskDelegate taskDelegate;
|
||||
private final UserDelegate userDelegate;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* Ensures the database is kept up to date.
|
||||
@ -54,16 +58,16 @@ public class FleetBeans {
|
||||
|
||||
final DefaultDatabaseConnection databaseConnection = new DefaultDatabaseConnection(properties);
|
||||
|
||||
passwordEncoder = new PKCS5S2PasswordEncoder(properties.getAppSecret());
|
||||
databaseVersion = new DatabaseVersion(databaseConnection);
|
||||
imageDelegate = new ImageDelegate(new DefaultImageDAO(databaseConnection));
|
||||
repositoryDelegate = new RepositoryDelegate(new DefaultRepositoryDAO(databaseConnection));
|
||||
dockerHubDelegate = new DockerHubDelegate(new DockerHubV2Client(properties.getDockerHubCredentials()));
|
||||
authenticationDelegate = new PropertiesAuthenticationDelegate(properties.getAppUsername(), properties.getAppPassword());
|
||||
webServer = new WebServer(properties.getAppPort());
|
||||
taskManager = new TaskManager();
|
||||
synchronisationDelegate = new SynchronisationDelegate(imageDelegate, repositoryDelegate, dockerHubDelegate);
|
||||
|
||||
userDelegate = new UserDelegate(passwordEncoder, new DefaultUserDAO(databaseConnection));
|
||||
taskDelegate = new TaskDelegate(this);
|
||||
authenticationDelegate = new DefaultAuthenticationDelegate(AuthenticatorFactory.getAuthenticator(this));
|
||||
}
|
||||
|
||||
public FleetProperties getProperties() {
|
||||
@ -94,10 +98,6 @@ public class FleetBeans {
|
||||
return databaseVersion;
|
||||
}
|
||||
|
||||
public WebServer getWebServer() {
|
||||
return webServer;
|
||||
}
|
||||
|
||||
public TaskManager getTaskManager() {
|
||||
return taskManager;
|
||||
}
|
||||
@ -105,4 +105,12 @@ public class FleetBeans {
|
||||
public TaskDelegate getTaskDelegate() {
|
||||
return taskDelegate;
|
||||
}
|
||||
|
||||
public UserDelegate getUserDelegate() {
|
||||
return userDelegate;
|
||||
}
|
||||
|
||||
public PasswordEncoder getPasswordEncoder() {
|
||||
return passwordEncoder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,16 @@ public class FleetProperties {
|
||||
return getStringProperty("fleet.database.password");
|
||||
}
|
||||
|
||||
public String getAuthenticationType() {
|
||||
return getStringProperty("fleet.admin.authentication.type");
|
||||
}
|
||||
|
||||
public String getAppSecret() {
|
||||
|
||||
String secret = getStringProperty("fleet.admin.secret");
|
||||
return null == secret ? "" : secret;
|
||||
}
|
||||
|
||||
public String getAppUsername() {
|
||||
return getStringProperty("fleet.admin.username");
|
||||
}
|
||||
@ -82,7 +92,7 @@ public class FleetProperties {
|
||||
|
||||
property = System.getProperty(propertyKey);
|
||||
if (null == property) {
|
||||
property = System.getenv(propertyKey);
|
||||
property = System.getenv(propertyKey.replace(".", "_"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,8 +20,6 @@ package io.linuxserver.fleet.core;
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
FleetApp app = new FleetApp();
|
||||
app.run();
|
||||
FleetApp.instance().run();
|
||||
}
|
||||
}
|
||||
|
||||
157
src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java
Normal file
157
src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.db.dao;
|
||||
|
||||
import io.linuxserver.fleet.db.PoolingDatabaseConnection;
|
||||
import io.linuxserver.fleet.db.query.InsertUpdateResult;
|
||||
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
|
||||
import io.linuxserver.fleet.model.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
|
||||
import static io.linuxserver.fleet.db.dao.Utils.setNullableString;
|
||||
|
||||
public class DefaultUserDAO implements UserDAO {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUserDAO.class);
|
||||
|
||||
private final PoolingDatabaseConnection databaseConnection;
|
||||
|
||||
public DefaultUserDAO(PoolingDatabaseConnection databaseConnection) {
|
||||
this.databaseConnection = databaseConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User fetchUser(int id) {
|
||||
|
||||
try (Connection connection = databaseConnection.getConnection()) {
|
||||
|
||||
CallableStatement call = connection.prepareCall("{CALL User_Get(?)}");
|
||||
call.setInt(1, id);
|
||||
|
||||
ResultSet results = call.executeQuery();
|
||||
if (results.next())
|
||||
return parseUserFromResultSet(results);
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Unable to retrieve user", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User fetchUserByUsername(String username) {
|
||||
|
||||
try (Connection connection = databaseConnection.getConnection()) {
|
||||
|
||||
CallableStatement call = connection.prepareCall("{CALL User_GetByName(?)}");
|
||||
call.setString(1, username);
|
||||
|
||||
ResultSet results = call.executeQuery();
|
||||
if (results.next())
|
||||
return parseUserFromResultSet(results);
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Unable to retrieve user", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertUpdateResult<User> saveUser(User user) {
|
||||
|
||||
try (Connection connection = databaseConnection.getConnection()) {
|
||||
|
||||
CallableStatement call = connection.prepareCall("{CALL User_Save(?,?,?,?,?,?)}");
|
||||
setNullableInt(call, 1, user.getId());
|
||||
call.setString(2, user.getUsername());
|
||||
setNullableString(call, 3, user.getPassword());
|
||||
|
||||
call.registerOutParameter(4, Types.INTEGER);
|
||||
call.registerOutParameter(5, Types.INTEGER);
|
||||
call.registerOutParameter(6, Types.VARCHAR);
|
||||
|
||||
call.executeUpdate();
|
||||
|
||||
int userId = call.getInt(4);
|
||||
int status = call.getInt(5);
|
||||
String statusMessage = call.getString(6);
|
||||
|
||||
if (InsertUpdateStatus.OK == status)
|
||||
return new InsertUpdateResult<>(fetchUser(userId), status, statusMessage);
|
||||
|
||||
return new InsertUpdateResult<>(status, statusMessage);
|
||||
|
||||
} catch (SQLException e) {
|
||||
|
||||
LOGGER.error("Unable to save user", e);
|
||||
return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save user");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> fetchAllUsers() {
|
||||
|
||||
List<User> repositories = new ArrayList<>();
|
||||
|
||||
try (Connection connection = databaseConnection.getConnection()) {
|
||||
|
||||
CallableStatement call = connection.prepareCall("{CALL User_GetAll()}");
|
||||
|
||||
ResultSet results = call.executeQuery();
|
||||
while (results.next())
|
||||
repositories.add(parseUserFromResultSet(results));
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Unable to get all users", e);
|
||||
}
|
||||
|
||||
return repositories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUser(User user) {
|
||||
|
||||
try (Connection connection = databaseConnection.getConnection()) {
|
||||
|
||||
CallableStatement call = connection.prepareCall("{CALL User_Delete(?)}");
|
||||
call.setInt(1, user.getId());
|
||||
|
||||
call.executeUpdate();
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error when removing user", e);
|
||||
}
|
||||
}
|
||||
|
||||
private User parseUserFromResultSet(ResultSet results) throws SQLException {
|
||||
|
||||
return new User(
|
||||
results.getInt("UserId"),
|
||||
results.getString("UserName"),
|
||||
results.getString("UserPassword")
|
||||
).withModifiedTime(results.getTimestamp("ModifiedTime").toLocalDateTime());
|
||||
}
|
||||
}
|
||||
36
src/main/java/io/linuxserver/fleet/db/dao/UserDAO.java
Normal file
36
src/main/java/io/linuxserver/fleet/db/dao/UserDAO.java
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.db.dao;
|
||||
|
||||
import io.linuxserver.fleet.db.query.InsertUpdateResult;
|
||||
import io.linuxserver.fleet.model.User;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface UserDAO {
|
||||
|
||||
User fetchUser(int id);
|
||||
|
||||
User fetchUserByUsername(String username);
|
||||
|
||||
InsertUpdateResult<User> saveUser(User user);
|
||||
|
||||
List<User> fetchAllUsers();
|
||||
|
||||
void removeUser(User user);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.delegate;
|
||||
|
||||
import io.linuxserver.fleet.auth.AuthenticationResult;
|
||||
import io.linuxserver.fleet.auth.UserCredentials;
|
||||
import io.linuxserver.fleet.auth.authenticator.UserAuthenticator;
|
||||
|
||||
public class DefaultAuthenticationDelegate implements AuthenticationDelegate {
|
||||
|
||||
private final UserAuthenticator authenticator;
|
||||
|
||||
public DefaultAuthenticationDelegate(UserAuthenticator authenticator) {
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResult authenticate(String username, String password) {
|
||||
return authenticator.authenticate(new UserCredentials(username, password));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.delegate;
|
||||
|
||||
import io.linuxserver.fleet.auth.security.PasswordEncoder;
|
||||
import io.linuxserver.fleet.db.dao.UserDAO;
|
||||
import io.linuxserver.fleet.db.query.InsertUpdateResult;
|
||||
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
|
||||
import io.linuxserver.fleet.exception.SaveException;
|
||||
import io.linuxserver.fleet.model.User;
|
||||
|
||||
public class UserDelegate {
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserDAO userDAO;
|
||||
|
||||
public UserDelegate(PasswordEncoder passwordEncoder, UserDAO userDAO) {
|
||||
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.userDAO = userDAO;
|
||||
}
|
||||
|
||||
public User fetchUser(int id) {
|
||||
return userDAO.fetchUser(id);
|
||||
}
|
||||
|
||||
public User fetchUserByUsername(String username) {
|
||||
return userDAO.fetchUserByUsername(username);
|
||||
}
|
||||
|
||||
public boolean isUserRepositoryEmpty() {
|
||||
return userDAO.fetchAllUsers().isEmpty();
|
||||
}
|
||||
|
||||
public User createNewUser(String username, String password) throws SaveException {
|
||||
|
||||
InsertUpdateResult<User> result = userDAO.saveUser(new User(username, passwordEncoder.encode(password)));
|
||||
|
||||
if (result.getStatus() == InsertUpdateStatus.OK)
|
||||
return result.getResult();
|
||||
|
||||
throw new SaveException(result.getStatusMessage());
|
||||
}
|
||||
|
||||
public void removeUser(User user) {
|
||||
userDAO.removeUser(user);
|
||||
}
|
||||
}
|
||||
46
src/main/java/io/linuxserver/fleet/model/User.java
Normal file
46
src/main/java/io/linuxserver/fleet/model/User.java
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.model;
|
||||
|
||||
public class User extends PersistableItem<User> {
|
||||
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public User(String username, String password) {
|
||||
super();
|
||||
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public User(Integer id, String username, String password) {
|
||||
super(id);
|
||||
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@ -240,10 +240,20 @@ public class DefaultSynchronisationState implements SynchronisationState {
|
||||
}
|
||||
|
||||
private void onStart(SynchronisationContext context) {
|
||||
|
||||
synchronized (running) {
|
||||
running.set(true);
|
||||
}
|
||||
|
||||
context.getListeners().forEach(SynchronisationListener::onSynchronisationStart);
|
||||
}
|
||||
|
||||
private void onFinish(SynchronisationContext context) {
|
||||
|
||||
synchronized (running) {
|
||||
running.set(false);
|
||||
}
|
||||
|
||||
context.getListeners().forEach(SynchronisationListener::onSynchronisationFinish);
|
||||
}
|
||||
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.web;
|
||||
|
||||
import io.linuxserver.fleet.auth.AuthenticatedUser;
|
||||
import io.linuxserver.fleet.model.api.ApiResponse;
|
||||
import io.linuxserver.fleet.model.api.FleetApiException;
|
||||
import spark.Route;
|
||||
import spark.RouteGroup;
|
||||
import spark.Session;
|
||||
import spark.TemplateViewRoute;
|
||||
import spark.template.freemarker.FreeMarkerEngine;
|
||||
|
||||
import static spark.Spark.*;
|
||||
|
||||
public class WebServer {
|
||||
|
||||
private boolean started;
|
||||
|
||||
public WebServer(int appPort) {
|
||||
|
||||
port(appPort);
|
||||
|
||||
staticFiles.location("/assets");
|
||||
staticFiles.expireTime(600);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
|
||||
started = true;
|
||||
|
||||
path("/admin", configureAuthorisationRoute(""));
|
||||
path("/admin", configureAuthorisationRoute("/repositories"));
|
||||
path("/admin", configureAuthorisationRoute("/images"));
|
||||
path("/admin", configureAuthorisationRoute("/manageImage"));
|
||||
path("/admin", configureAuthorisationRoute("/manageRepository"));
|
||||
path("/admin", configureAuthorisationRoute("/forceSync"));
|
||||
path("/admin", configureAuthorisationRoute("/getImage"));
|
||||
|
||||
after("/api/v1/*", (request, response) -> {
|
||||
|
||||
response.header("Access-Control-Allow-Origin", "*");
|
||||
response.header("Access-Control-Allow-Methods", "GET");
|
||||
response.header("Content-Type", "application/json");
|
||||
});
|
||||
|
||||
after("/admin/getImage", (request, response) -> {
|
||||
response.header("Content-Type", "application/json");
|
||||
});
|
||||
|
||||
exception(FleetApiException.class, (exception, request, response) -> {
|
||||
|
||||
response.body(new JsonTransformer().render(new ApiResponse<>("ERROR", exception.getMessage())));
|
||||
response.header("Content-Type", "application/json");
|
||||
response.status(exception.getStatusCode());
|
||||
});
|
||||
}
|
||||
|
||||
public void addWebSocket(String path, Object object) {
|
||||
|
||||
if (started) {
|
||||
throw new IllegalStateException("Server has already started! Add a web socket before starting");
|
||||
}
|
||||
|
||||
webSocket(path, object);
|
||||
}
|
||||
|
||||
public void addPage(String path, TemplateViewRoute page) {
|
||||
get(path, page, new FreeMarkerEngine());
|
||||
}
|
||||
|
||||
public void addPostRoute(String path, Route route) {
|
||||
post(path, route);
|
||||
}
|
||||
|
||||
public void addGetApi(String path, Route route) {
|
||||
get(path, route, new JsonTransformer());
|
||||
}
|
||||
|
||||
public void addPostApi(String path, Route route) {
|
||||
post(path, route, new JsonTransformer());
|
||||
}
|
||||
|
||||
private RouteGroup configureAuthorisationRoute(String path) {
|
||||
|
||||
return () -> before(path, (request, response) -> {
|
||||
|
||||
Session session = request.session(false);
|
||||
|
||||
if (null == session)
|
||||
response.redirect("/admin/login");
|
||||
|
||||
else {
|
||||
|
||||
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
|
||||
if (null == user)
|
||||
response.redirect("/admin/login");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
31
src/main/java/io/linuxserver/fleet/web/pages/SetupPage.java
Normal file
31
src/main/java/io/linuxserver/fleet/web/pages/SetupPage.java
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.web.pages;
|
||||
|
||||
import spark.ModelAndView;
|
||||
import spark.Request;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class SetupPage extends WebPage {
|
||||
|
||||
@Override
|
||||
protected ModelAndView handle(Request request) {
|
||||
return new ModelAndView(new HashMap<>(), "setup.ftl");
|
||||
}
|
||||
}
|
||||
@ -19,14 +19,15 @@ package io.linuxserver.fleet.web.pages;
|
||||
|
||||
import io.linuxserver.fleet.web.SessionAttribute;
|
||||
import spark.*;
|
||||
import spark.template.freemarker.FreeMarkerEngine;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class WebPage implements TemplateViewRoute {
|
||||
public abstract class WebPage implements Route {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ModelAndView handle(Request request, Response response) {
|
||||
public Object handle(Request request, Response response) {
|
||||
|
||||
ModelAndView modelAndView = handle(request);
|
||||
|
||||
@ -34,7 +35,7 @@ public abstract class WebPage implements TemplateViewRoute {
|
||||
if (null != session && null != session.attribute(SessionAttribute.USER))
|
||||
((Map<String, Object>) modelAndView.getModel()).put("__AUTHENTICATED_USER", session.attribute(SessionAttribute.USER));
|
||||
|
||||
return modelAndView;
|
||||
return new FreeMarkerEngine().render(modelAndView);
|
||||
}
|
||||
|
||||
protected abstract ModelAndView handle(Request request);
|
||||
|
||||
@ -49,7 +49,7 @@ public class LoginRoute implements Route {
|
||||
AuthenticationResult authResult = authenticationDelegate.authenticate(username, password);
|
||||
if (!authResult.isAuthenticated()) {
|
||||
|
||||
response.redirect("/admin/login?fail=true");
|
||||
response.redirect("/login?fail=true");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ public class ManageImageApi implements Route {
|
||||
if (null == image) {
|
||||
|
||||
response.status(404);
|
||||
response.header("Content-Type", "application/json");
|
||||
return new ApiResponse<>("Error", "Image not found.");
|
||||
}
|
||||
|
||||
@ -67,7 +66,6 @@ public class ManageImageApi implements Route {
|
||||
|
||||
imageDelegate.saveImage(image);
|
||||
|
||||
response.header("Content-Type", "application/json");
|
||||
return new ApiResponse<>("OK", "Image updated.");
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -44,7 +44,6 @@ public class ManageRepositoryApi implements Route {
|
||||
if (null == repository) {
|
||||
|
||||
response.status(404);
|
||||
response.header("Content-Type", "application/json");
|
||||
return new ApiResponse<>("Error", "Repository not found.");
|
||||
}
|
||||
|
||||
@ -64,7 +63,6 @@ public class ManageRepositoryApi implements Route {
|
||||
|
||||
repositoryDelegate.saveRepository(repository);
|
||||
|
||||
response.header("Content-Type", "application/json");
|
||||
return new ApiResponse<>("OK", "Repository updated.");
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.web.routes;
|
||||
|
||||
import io.linuxserver.fleet.delegate.UserDelegate;
|
||||
import io.linuxserver.fleet.exception.SaveException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Route;
|
||||
|
||||
public class RegisterInitialUserRoute implements Route {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RegisterInitialUserRoute.class);
|
||||
|
||||
private UserDelegate userDelegate;
|
||||
|
||||
public RegisterInitialUserRoute(UserDelegate userDelegate) {
|
||||
this.userDelegate = userDelegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handle(Request request, Response response) {
|
||||
|
||||
String username = request.queryParams("username");
|
||||
String password = request.queryParams("password");
|
||||
String verifyPassword = request.queryParams("verify-password");
|
||||
|
||||
if (!verifyPassword.equals(password)) {
|
||||
|
||||
response.redirect("/setup?passwordMismatch=true");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
userDelegate.createNewUser(username, password);
|
||||
response.redirect("/login");
|
||||
|
||||
} catch (SaveException e) {
|
||||
|
||||
response.redirect("/setup?createUserError=true");
|
||||
LOGGER.error("Unable to create new user", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
9030
src/main/resources/assets/css/bootstrap.css
vendored
Normal file
9030
src/main/resources/assets/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -22,6 +22,14 @@ h2 {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navbar-darkish {
|
||||
|
||||
background-color: #cdcdcd;
|
||||
@ -75,6 +83,14 @@ h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.table tbody tr td.image-name span.image-name--repository {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
code {
|
||||
|
||||
background-color: #efefef;
|
||||
@ -96,7 +112,7 @@ code {
|
||||
-moz-box-shadow: 0px 0px 5px 0px rgba(82,63,105,0.05);
|
||||
box-shadow: 0px 0px 5px 0px rgba(82,63,105,0.05);
|
||||
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
border: 1px solid #d4dadf;
|
||||
|
||||
font-size: 0.9rem;
|
||||
|
||||
@ -43,7 +43,7 @@ var repositoryManager = (function($) {
|
||||
var syncEnabled = syncSwitch.is(':checked');
|
||||
|
||||
var request = {
|
||||
url: '/admin/manageRepository',
|
||||
url: '/admin/api/manageRepository',
|
||||
method: 'POST',
|
||||
data: {
|
||||
repositoryId: repositoryId,
|
||||
@ -62,7 +62,7 @@ var repositoryManager = (function($) {
|
||||
var maskValue = versionMask.val();
|
||||
|
||||
var request = {
|
||||
url: '/admin/manageRepository',
|
||||
url: '/admin/api/manageRepository',
|
||||
method: 'POST',
|
||||
data: {
|
||||
repositoryId: repositoryId,
|
||||
@ -174,7 +174,7 @@ var imageListManager = (function($) {
|
||||
var buildRequest = function(action, imageId) {
|
||||
|
||||
return {
|
||||
url: '/admin/manageImage',
|
||||
url: '/admin/api/manageImage',
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: action,
|
||||
@ -204,7 +204,7 @@ var imageListManager = (function($) {
|
||||
};
|
||||
|
||||
var createStableIcon = function() {
|
||||
return $('<i class="fas fa-check-circle text-success" title="No issues reported"></i>');
|
||||
return $('<i class="fas fa-check text-success" title="No issues reported"></i>');
|
||||
};
|
||||
|
||||
var getImageRow = function(item) {
|
||||
@ -222,7 +222,7 @@ var imageListManager = (function($) {
|
||||
var getImageMask = function(imageId, callback) {
|
||||
|
||||
var request = {
|
||||
url: '/admin/getImage?imageId=' + imageId,
|
||||
url: '/admin/api/getImage?imageId=' + imageId,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
@ -277,7 +277,7 @@ var synchronisationManager = (function($) {
|
||||
var startSynchronisation = function() {
|
||||
|
||||
var request = {
|
||||
url: '/admin/forceSync',
|
||||
url: '/admin/api/forceSync',
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
@ -310,4 +310,35 @@ var synchronisationManager = (function($) {
|
||||
init: init
|
||||
}
|
||||
|
||||
}(jQuery));
|
||||
|
||||
var passwordValidationManager = (function($) {
|
||||
|
||||
var comparePasswords = function(password, verifyPassword) {
|
||||
|
||||
if (password.val() !== verifyPassword.val()) {
|
||||
verifyPassword.get(0).setCustomValidity('Mismatch');
|
||||
} else {
|
||||
verifyPassword.get(0).setCustomValidity('');
|
||||
}
|
||||
};
|
||||
|
||||
var init = function() {
|
||||
|
||||
var password = $('#password');
|
||||
var verifyPassword = $('#verify-password');
|
||||
|
||||
password.on('keyup', function() {
|
||||
comparePasswords(password, verifyPassword);
|
||||
});
|
||||
|
||||
verifyPassword.on('keyup', function() {
|
||||
comparePasswords(password, verifyPassword);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
|
||||
}(jQuery));
|
||||
@ -0,0 +1,7 @@
|
||||
CREATE TABLE Users (
|
||||
`id` INT NOT NULL auto_increment PRIMARY KEY,
|
||||
`username` VARCHAR(255) NOT NULL,
|
||||
`password` VARCHAR(255) DEFAULT NULL,
|
||||
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY (`username`)
|
||||
) ENGINE=InnoDB;
|
||||
111
src/main/resources/db/migration/V1.3__CreateUserSprocs.sql
Normal file
111
src/main/resources/db/migration/V1.3__CreateUserSprocs.sql
Normal file
@ -0,0 +1,111 @@
|
||||
DELIMITER //
|
||||
|
||||
CREATE PROCEDURE `User_Save`
|
||||
(
|
||||
in_id INT,
|
||||
in_username VARCHAR(255),
|
||||
in_password VARCHAR(255),
|
||||
|
||||
OUT out_id INT,
|
||||
OUT out_status INT,
|
||||
OUT out_message VARCHAR(100)
|
||||
)
|
||||
BEGIN
|
||||
|
||||
IF in_id IS NULL THEN
|
||||
|
||||
INSERT INTO Users
|
||||
(
|
||||
`username`,
|
||||
`password`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
in_username,
|
||||
in_password
|
||||
);
|
||||
|
||||
SET out_id = LAST_INSERT_ID();
|
||||
|
||||
ELSE
|
||||
|
||||
UPDATE Repositories
|
||||
SET
|
||||
`username` = in_username,
|
||||
`password` = in_password
|
||||
WHERE
|
||||
`id` = in_id;
|
||||
|
||||
SET out_id = in_id;
|
||||
|
||||
END IF;
|
||||
|
||||
SET out_status = 0;
|
||||
SET out_message = 'OK';
|
||||
|
||||
END;
|
||||
//
|
||||
|
||||
CREATE PROCEDURE `User_GetAll` ()
|
||||
BEGIN
|
||||
|
||||
SELECT
|
||||
`id` AS `UserId`,
|
||||
`username` AS `UserName`,
|
||||
`password` AS `UserPassword`,
|
||||
`modified` AS `ModifiedTime`
|
||||
FROM
|
||||
Users;
|
||||
|
||||
END;
|
||||
//
|
||||
|
||||
CREATE PROCEDURE `User_Get`
|
||||
(
|
||||
in_id INT
|
||||
)
|
||||
BEGIN
|
||||
|
||||
SELECT
|
||||
`id` AS `UserId`,
|
||||
`username` AS `UserName`,
|
||||
`password` AS `UserPassword`,
|
||||
`modified` AS `ModifiedTime`
|
||||
FROM
|
||||
Users
|
||||
WHERE
|
||||
`id` = in_id;
|
||||
|
||||
END;
|
||||
//
|
||||
|
||||
CREATE PROCEDURE `User_GetByName`
|
||||
(
|
||||
in_username VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
|
||||
SELECT
|
||||
`id` AS `UserId`,
|
||||
`username` AS `UserName`,
|
||||
`password` AS `UserPassword`,
|
||||
`modified` AS `ModifiedTime`
|
||||
FROM
|
||||
Users
|
||||
WHERE
|
||||
`username` = in_username;
|
||||
END;
|
||||
//
|
||||
|
||||
CREATE PROCEDURE `User_Delete`
|
||||
(
|
||||
in_id INT
|
||||
)
|
||||
BEGIN
|
||||
|
||||
DELETE FROM Users WHERE `id` = in_id;
|
||||
|
||||
END;
|
||||
//
|
||||
|
||||
DELIMITER ;
|
||||
@ -7,6 +7,7 @@
|
||||
<title>${title}</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/images/favicon-32x32.png"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Pacifico|Nunito" rel="stylesheet" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/all.min.css" />
|
||||
@ -21,7 +22,9 @@
|
||||
|
||||
<#if __AUTHENTICATED_USER?has_content>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
<span class="navbar-toggler-icon align-middle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
@ -32,7 +35,7 @@
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="admin">Manage Repositories</a>
|
||||
<form action="/admin/logout" method="POST">
|
||||
<form action="/logout" method="POST">
|
||||
<button class="dropdown-item">Log Out</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -69,15 +72,37 @@
|
||||
$("table.table--sortable").tablesorter();
|
||||
});
|
||||
|
||||
<#if context='admin'>
|
||||
<#if context="admin">
|
||||
repositoryManager.init();
|
||||
synchronisationManager.init();
|
||||
</#if>
|
||||
|
||||
<#if context='home'>
|
||||
<#if context="home">
|
||||
imageListManager.init();
|
||||
</#if>
|
||||
|
||||
<#if context="setup" || context="login">
|
||||
(function() {
|
||||
'use strict';
|
||||
window.addEventListener('load', function() {
|
||||
|
||||
var forms = document.getElementsByClassName('needs-validation');
|
||||
|
||||
var validation = Array.prototype.filter.call(forms, function(form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (form.checkValidity() === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
}, false);
|
||||
});
|
||||
}, false);
|
||||
})();
|
||||
|
||||
passwordValidationManager.init();
|
||||
</#if>
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<#if !image.hidden || __AUTHENTICATED_USER?has_content>
|
||||
<tr <#if image.hidden>class="hidden-image"</#if> data-image-id="#{image.id}" data-image-name="${image.name}">
|
||||
<td class="image-name">
|
||||
<a target="_blank" href="https://hub.docker.com/r/${populatedRepository.repository.name}/${image.name}">${image.name}</a>
|
||||
<span class="image-name--repository">${populatedRepository.repository.name} /</span> <a target="_blank" href="https://hub.docker.com/r/${populatedRepository.repository.name}/${image.name}">${image.name}</a>
|
||||
</td>
|
||||
<td>
|
||||
<#if image.version?has_content>
|
||||
@ -79,7 +79,7 @@
|
||||
<#if image.unstable>
|
||||
<i class="fas fa-exclamation-triangle text-warning" title="Potentially unstable"></i>
|
||||
<#else>
|
||||
<i class="fas fa-check-circle text-success" title="No issues reported"></i>
|
||||
<i class="fas fa-check text-success" title="No issues reported"></i>
|
||||
</#if>
|
||||
|
||||
</td>
|
||||
|
||||
@ -5,22 +5,31 @@
|
||||
<div class="container container--white mt-3">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 p-3">
|
||||
<h2>Log In</h2>
|
||||
<div class="col-sm-12 col-md-3"></div>
|
||||
<div class="col-sm-12 col-md-6">
|
||||
|
||||
<form action="/admin/login" method="POST">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Log In</h2>
|
||||
<form action="/login" method="POST">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" name="username" id="username" />
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control form-control-sm" name="username" id="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control form-control-sm" name="password" id="password" />
|
||||
</div>
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-primary">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="password" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Log In</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-3"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
46
src/main/resources/spark/template/freemarker/setup.ftl
Normal file
46
src/main/resources/spark/template/freemarker/setup.ftl
Normal file
@ -0,0 +1,46 @@
|
||||
<#import "./base.ftl" as base>
|
||||
|
||||
<@base.base title="Set Up" context="setup">
|
||||
|
||||
<div class="container container--white mt-3">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h2>Set Up Fleet</h2>
|
||||
|
||||
<p>
|
||||
It looks like this is the first time you're running Fleet. In order to get started, you need to create
|
||||
and initial user which will have access to the management pages of the application.
|
||||
</p>
|
||||
|
||||
<h3 class="mb-3">Create a user</h3>
|
||||
|
||||
<form action="/setup" method="POST" class="needs-validation" novalidate>
|
||||
<div class="form-group row">
|
||||
<label for="username" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control form-control-sm" name="username" id="username" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control form-control-sm" name="password" id="password" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="verify-password" class="col-sm-2 col-form-label">Verify Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control form-control-sm" name="verify-password" id="verify-password" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-primary">Continue</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</@base.base>
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2019 LinuxServer.io
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.linuxserver.fleet.auth.security;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
public class PKCS5S2PasswordEncoderTest {
|
||||
|
||||
private static final String HASH_FOR_PASSWORD = "8SLrEokFZQQrRk1MeDrpgINOHNXKXPMs2r56DMWBCjLXs9oTHsLzEmnwb68oQAc9+1YdKPTdWahAjjqtvO9M8FtQxCC+8yd71+J1VoWizow=";
|
||||
|
||||
private PKCS5S2PasswordEncoder encoder = new PKCS5S2PasswordEncoder("superSecret");
|
||||
|
||||
@Test
|
||||
public void shouldGenerateHash() {
|
||||
assertThat(encoder.matches("password", HASH_FOR_PASSWORD), is(equalTo(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
System.out.println(encoder.encode("password").length());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user