diff --git a/build.gradle b/build.gradle
index 740ca4b..200faef 100644
--- a/build.gradle
+++ b/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 {
diff --git a/config/example.fleet.properties b/config/example.fleet.properties
index 016c0f3..3796ad7 100644
--- a/config/example.fleet.properties
+++ b/config/example.fleet.properties
@@ -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
diff --git a/src/main/java/io/linuxserver/fleet/auth/UserCredentials.java b/src/main/java/io/linuxserver/fleet/auth/UserCredentials.java
new file mode 100644
index 0000000..2f4546c
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/UserCredentials.java
@@ -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 .
+ */
+
+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;
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/auth/authenticator/AuthenticatorFactory.java b/src/main/java/io/linuxserver/fleet/auth/authenticator/AuthenticatorFactory.java
new file mode 100644
index 0000000..4404a4e
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/authenticator/AuthenticatorFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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 .
+ */
+
+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 PROPERTIES:
+
+ LOGGER.info("Configuring new authenticator: PropertyLoadedUserAuthenticator");
+ return new PropertyLoadedUserAuthenticator(properties.getAppUsername(), properties.getAppPassword());
+
+ case DATABASE:
+
+ LOGGER.info("Configuring new authenticator: DatabaseStoredUserAuthenticator");
+ return new DatabaseStoredUserAuthenticator(beans.getPasswordEncoder(), beans.getUserDelegate());
+ }
+
+ throw new RuntimeException("Provided authentication type is not supported");
+ }
+
+ public enum AuthenticationType {
+ PROPERTIES, DATABASE
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/auth/authenticator/DatabaseStoredUserAuthenticator.java b/src/main/java/io/linuxserver/fleet/auth/authenticator/DatabaseStoredUserAuthenticator.java
new file mode 100644
index 0000000..e409c3f
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/authenticator/DatabaseStoredUserAuthenticator.java
@@ -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 .
+ */
+
+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();
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/delegate/PropertiesAuthenticationDelegate.java b/src/main/java/io/linuxserver/fleet/auth/authenticator/PropertyLoadedUserAuthenticator.java
similarity index 63%
rename from src/main/java/io/linuxserver/fleet/delegate/PropertiesAuthenticationDelegate.java
rename to src/main/java/io/linuxserver/fleet/auth/authenticator/PropertyLoadedUserAuthenticator.java
index f35a345..3795e8f 100644
--- a/src/main/java/io/linuxserver/fleet/delegate/PropertiesAuthenticationDelegate.java
+++ b/src/main/java/io/linuxserver/fleet/auth/authenticator/PropertyLoadedUserAuthenticator.java
@@ -15,27 +15,28 @@
* along with this program. If not, see .
*/
-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();
}
diff --git a/src/main/java/io/linuxserver/fleet/auth/authenticator/UserAuthenticator.java b/src/main/java/io/linuxserver/fleet/auth/authenticator/UserAuthenticator.java
new file mode 100644
index 0000000..bec0401
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/authenticator/UserAuthenticator.java
@@ -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 .
+ */
+
+package io.linuxserver.fleet.auth.authenticator;
+
+import io.linuxserver.fleet.auth.AuthenticationResult;
+import io.linuxserver.fleet.auth.UserCredentials;
+
+/**
+ *
+ * Provides a mechanism for the application to authenticate a login request
+ * from a user.
+ *
+ */
+public interface UserAuthenticator {
+
+ /**
+ *
+ * Performs an authentication check against the provided credentials and the repository
+ * of currently stored users.
+ *
+ */
+ AuthenticationResult authenticate(UserCredentials userCredentials);
+}
diff --git a/src/main/java/io/linuxserver/fleet/auth/security/PKCS5S2PasswordEncoder.java b/src/main/java/io/linuxserver/fleet/auth/security/PKCS5S2PasswordEncoder.java
new file mode 100644
index 0000000..44051aa
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/security/PKCS5S2PasswordEncoder.java
@@ -0,0 +1,168 @@
+/*
+ * 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 .
+ */
+
+package io.linuxserver.fleet.auth.security;
+
+import io.linuxserver.fleet.auth.security.util.SaltGenerator;
+import org.bouncycastle.crypto.PBEParametersGenerator;
+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;
+
+/**
+ *
+ * Uses the PKCS5S2 crypto algorithm to encode and verify hashed passwords.
+ *
+ */
+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);
+ }
+
+ /**
+ *
+ * Compares the two byte arrays by performing a bitwise equality check against each individual
+ * element of both arrays.
+ *
+ *
+ * @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;
+ }
+
+ /**
+ *
+ * Performs the cryptographic hash against the raw password and the randomly generated salt value. This
+ * also concatenates the provided secret into the salt.
+ *
+ */
+ private byte[] encode(String rawPassword, byte[] salt) {
+
+ PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator();
+ generator.init(
+ PBEParametersGenerator.PKCS5PasswordToBytes(rawPassword.toCharArray()),
+ joinArrays(salt, secret),
+ iterations
+ );
+
+ return joinArrays(salt, ((KeyParameter) generator.generateDerivedMacParameters(hashWidth)).getKey());
+ }
+
+ /**
+ *
+ * Converts a byte array into a base-64 encoded string.
+ *
+ */
+ private String toBase64(byte[] bytes) {
+ return Base64.getEncoder().encodeToString(bytes);
+ }
+
+ /**
+ *
+ * Converts a base64-encoded string into its raw byte value
+ *
+ */
+ private byte[] fromBase64(String input) {
+ return Base64.getDecoder().decode(input);
+ }
+
+ /**
+ *
+ * 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.
+ *
+ */
+ private byte[] extractSalt(byte[] decodedHash) {
+ return extractFromArray(decodedHash, 0, saltGenerator.getKeyLength());
+ }
+
+ /**
+ *
+ * Combines two byte arrays together in order.
+ *
+ */
+ 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;
+ }
+
+ /**
+ *
+ * Extracts a sub-array from the provided array.
+ *
+ */
+ 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;
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/auth/security/PasswordEncoder.java b/src/main/java/io/linuxserver/fleet/auth/security/PasswordEncoder.java
new file mode 100644
index 0000000..a275dd2
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/security/PasswordEncoder.java
@@ -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 .
+ */
+
+package io.linuxserver.fleet.auth.security;
+
+/**
+ *
+ * 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.
+ * PasswordEncoder
+ *
+ */
+public interface PasswordEncoder {
+
+ /**
+ *
+ * Encodes the raw password into a one-way encrypted hash. The result of which should be stored.
+ *
+ *
+ * @param rawPassword
+ * The raw unencrypted password.
+ *
+ * @return
+ * The hashed result.
+ */
+ String encode(String rawPassword);
+
+ /**
+ *
+ * Determines if the provided raw password, when encoded, matches the stored encoded password.
+ *
+ *
+ * @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);
+}
diff --git a/src/main/java/io/linuxserver/fleet/auth/security/util/SaltGenerator.java b/src/main/java/io/linuxserver/fleet/auth/security/util/SaltGenerator.java
new file mode 100644
index 0000000..4f8ee30
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/auth/security/util/SaltGenerator.java
@@ -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 .
+ */
+
+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;
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/core/FleetApp.java b/src/main/java/io/linuxserver/fleet/core/FleetApp.java
index 3a0bc56..3cacdb1 100644
--- a/src/main/java/io/linuxserver/fleet/core/FleetApp.java
+++ b/src/main/java/io/linuxserver/fleet/core/FleetApp.java
@@ -20,6 +20,7 @@ package io.linuxserver.fleet.core;
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;
@@ -56,11 +57,14 @@ class FleetApp {
beans.getSynchronisationDelegate().registerListener(synchronisationWebSocket);
beans.getWebServer().addWebSocket("/admin/ws/sync", synchronisationWebSocket);
+ beans.getWebServer().addFilter( "*", new InitialUserFilterRoute(beans.getProperties().getAuthenticationType(), beans.getUserDelegate()));
beans.getWebServer().start();
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/setup", new SetupPage());
+ beans.getWebServer().addPostRoute( "/admin/setup", new RegisterInitialUserRoute(beans.getUserDelegate()));
beans.getWebServer().addPage( "/admin/login", new LoginPage());
beans.getWebServer().addPostRoute( "/admin/login", new LoginRoute(beans.getAuthenticationDelegate()));
beans.getWebServer().addPostRoute( "/admin/logout", new LogoutRoute());
diff --git a/src/main/java/io/linuxserver/fleet/core/FleetBeans.java b/src/main/java/io/linuxserver/fleet/core/FleetBeans.java
index de6d705..032f112 100644
--- a/src/main/java/io/linuxserver/fleet/core/FleetBeans.java
+++ b/src/main/java/io/linuxserver/fleet/core/FleetBeans.java
@@ -17,9 +17,13 @@
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;
@@ -42,6 +46,8 @@ public class FleetBeans {
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 +60,17 @@ 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() {
@@ -105,4 +112,12 @@ public class FleetBeans {
public TaskDelegate getTaskDelegate() {
return taskDelegate;
}
+
+ public UserDelegate getUserDelegate() {
+ return userDelegate;
+ }
+
+ public PasswordEncoder getPasswordEncoder() {
+ return passwordEncoder;
+ }
}
diff --git a/src/main/java/io/linuxserver/fleet/core/FleetProperties.java b/src/main/java/io/linuxserver/fleet/core/FleetProperties.java
index b7f604c..6f5b2b4 100644
--- a/src/main/java/io/linuxserver/fleet/core/FleetProperties.java
+++ b/src/main/java/io/linuxserver/fleet/core/FleetProperties.java
@@ -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(".", "_"));
}
}
diff --git a/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java
new file mode 100644
index 0000000..b9c3efe
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java
@@ -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 .
+ */
+
+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 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 fetchAllUsers() {
+
+ List 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());
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/db/dao/UserDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/UserDAO.java
new file mode 100644
index 0000000..7b255a8
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/db/dao/UserDAO.java
@@ -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 .
+ */
+
+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 saveUser(User user);
+
+ List fetchAllUsers();
+
+ void removeUser(User user);
+}
diff --git a/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java
new file mode 100644
index 0000000..775dca6
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java
@@ -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 .
+ */
+
+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));
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/delegate/UserDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/UserDelegate.java
new file mode 100644
index 0000000..ea59721
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/delegate/UserDelegate.java
@@ -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 .
+ */
+
+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 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);
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/model/User.java b/src/main/java/io/linuxserver/fleet/model/User.java
new file mode 100644
index 0000000..864a111
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/model/User.java
@@ -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 .
+ */
+
+package io.linuxserver.fleet.model;
+
+public class User extends PersistableItem {
+
+ 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;
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/web/WebServer.java b/src/main/java/io/linuxserver/fleet/web/WebServer.java
index 5673ae2..1908997 100644
--- a/src/main/java/io/linuxserver/fleet/web/WebServer.java
+++ b/src/main/java/io/linuxserver/fleet/web/WebServer.java
@@ -20,10 +20,7 @@ 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.*;
import spark.template.freemarker.FreeMarkerEngine;
import static spark.Spark.*;
@@ -45,7 +42,6 @@ public class WebServer {
started = true;
path("/admin", configureAuthorisationRoute(""));
- path("/admin", configureAuthorisationRoute("/repositories"));
path("/admin", configureAuthorisationRoute("/images"));
path("/admin", configureAuthorisationRoute("/manageImage"));
path("/admin", configureAuthorisationRoute("/manageRepository"));
@@ -71,6 +67,10 @@ public class WebServer {
});
}
+ public void addFilter(String path, Filter route) {
+ before(path, route);
+ }
+
public void addWebSocket(String path, Object object) {
if (started) {
diff --git a/src/main/java/io/linuxserver/fleet/web/pages/SetupPage.java b/src/main/java/io/linuxserver/fleet/web/pages/SetupPage.java
new file mode 100644
index 0000000..e6d1140
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/web/pages/SetupPage.java
@@ -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 .
+ */
+
+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");
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/web/routes/InitialUserFilterRoute.java b/src/main/java/io/linuxserver/fleet/web/routes/InitialUserFilterRoute.java
new file mode 100644
index 0000000..faf5e7f
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/web/routes/InitialUserFilterRoute.java
@@ -0,0 +1,84 @@
+/*
+ * 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 .
+ */
+
+package io.linuxserver.fleet.web.routes;
+
+import io.linuxserver.fleet.auth.authenticator.AuthenticatorFactory.AuthenticationType;
+import io.linuxserver.fleet.delegate.UserDelegate;
+import spark.Filter;
+import spark.Request;
+import spark.Response;
+
+/**
+ *
+ * Forces the maintainer of Fleet to set up an initial user before running Fleet.
+ *
+ */
+public class InitialUserFilterRoute implements Filter {
+
+ private static final String FLEET_USER_UNDEFINED = "fleet.user.undefined";
+
+ private static final String[] PATH_EXEMPTIONS = {
+ "/assets", "/admin/setup"
+ };
+
+ private final UserDelegate userDelegate;
+ private final boolean databaseAuthenticationEnabled;
+
+ public InitialUserFilterRoute(String authenticationType, UserDelegate userDelegate) {
+
+ this.userDelegate = userDelegate;
+ this.databaseAuthenticationEnabled = AuthenticationType.DATABASE.equals(AuthenticationType.valueOf(authenticationType.toUpperCase()));
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+
+ if (databaseAuthenticationEnabled) {
+
+ if (!initialUserNeedsConfiguring() && "/admin/setup".equals(request.pathInfo())) {
+ response.redirect("/admin");
+ }
+
+ else if (initialUserNeedsConfiguring() && !pathIsExempted(request.pathInfo())) {
+ response.redirect("/admin/setup");
+ }
+ }
+ }
+
+ private boolean initialUserNeedsConfiguring() {
+
+ String configured = System.getProperty(FLEET_USER_UNDEFINED);
+ if (null == configured || "true".equalsIgnoreCase(configured)) {
+ System.setProperty(FLEET_USER_UNDEFINED, String.valueOf(userDelegate.isUserRepositoryEmpty()));
+ }
+
+ return "true".equalsIgnoreCase(System.getProperty(FLEET_USER_UNDEFINED));
+ }
+
+ private boolean pathIsExempted(String path) {
+
+ for (String exemption : PATH_EXEMPTIONS) {
+
+ if (path.startsWith(exemption)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/io/linuxserver/fleet/web/routes/RegisterInitialUserRoute.java b/src/main/java/io/linuxserver/fleet/web/routes/RegisterInitialUserRoute.java
new file mode 100644
index 0000000..7e09f13
--- /dev/null
+++ b/src/main/java/io/linuxserver/fleet/web/routes/RegisterInitialUserRoute.java
@@ -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 .
+ */
+
+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("/admin/setup?passwordMismatch=true");
+ return null;
+ }
+
+ try {
+
+ userDelegate.createNewUser(username, password);
+ response.redirect("/admin");
+
+ } catch (SaveException e) {
+
+ response.redirect("/admin/setup?createUserError=true");
+ LOGGER.error("Unable to create new user", e);
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/resources/assets/js/fleet.js b/src/main/resources/assets/js/fleet.js
index 6559d05..1b37b09 100644
--- a/src/main/resources/assets/js/fleet.js
+++ b/src/main/resources/assets/js/fleet.js
@@ -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));
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V1.2__CreateUserTable.sql b/src/main/resources/db/migration/V1.2__CreateUserTable.sql
new file mode 100644
index 0000000..ae01284
--- /dev/null
+++ b/src/main/resources/db/migration/V1.2__CreateUserTable.sql
@@ -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;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V1.3__CreateUserSprocs.sql b/src/main/resources/db/migration/V1.3__CreateUserSprocs.sql
new file mode 100644
index 0000000..62919d5
--- /dev/null
+++ b/src/main/resources/db/migration/V1.3__CreateUserSprocs.sql
@@ -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 ;
diff --git a/src/main/resources/spark/template/freemarker/base.ftl b/src/main/resources/spark/template/freemarker/base.ftl
index 40bd0e2..da4557b 100644
--- a/src/main/resources/spark/template/freemarker/base.ftl
+++ b/src/main/resources/spark/template/freemarker/base.ftl
@@ -69,15 +69,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>
+