commit ba2c76ff063a1d8f5957d8f8ec6b6ab5c53debf5 Author: Takiguchi Date: Sat Sep 29 18:33:56 2018 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a08031d --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +.mvn +**/node_modules \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..92be9b2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.minager + minager + 0.0.1-SNAPSHOT + jar + + Minager + Minecraft server managing application + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mindrot + jbcrypt + 0.4 + + + org.postgresql + postgresql + runtime + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + + diff --git a/src/main/bash/minager-service b/src/main/bash/minager-service new file mode 100644 index 0000000..6a91d09 --- /dev/null +++ b/src/main/bash/minager-service @@ -0,0 +1,28 @@ +#!/bin/sh + +USER=minecraft +MINECRAFT_SERVER_PATH=/home/minecraft/minager/ +MINECRAFT_SHELL=minager.sh + +case "$1" in + 'start') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL start" + ;; + 'stop') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL stop" + ;; + 'status') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL status" + ;; + 'restart') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL restart" + ;; + *) + # If no argument, we launch the app in case of server startup + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL start &>/dev/null" + # And show the script usage + echo "Usage: /etc/init.d/minecraft {start|stop|status|restart}\n" >&2 + exit 3 + ;; +esac +exit 0 diff --git a/src/main/bash/minager.sh b/src/main/bash/minager.sh new file mode 100644 index 0000000..9ceb59a --- /dev/null +++ b/src/main/bash/minager.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# kFreeBSD do not accept scripts as interpreters, using #!/bin/sh and sourcing. +if [ true != "$INIT_D_SCRIPT_SOURCED" ] ; then + set "$0" "$@"; INIT_D_SCRIPT_SOURCED=true . /lib/init/init-d-script +fi + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +DESC="The Minager web app server" +NAME='minager' +MINECRAFT_HOME='/home/minecraft' +MINAGER_HOME="$MINECRAFT_HOME/minager" +DAEMON_HOME="$MINAGER_HOME/bin" +DAEMON="$DAEMON_HOME/$NAME.jar" +PIDFILE="$DAEMON_HOME/$NAME.pid" +SCRIPTNAME="$DAEMON_HOME/$0" + +start() +{ + # If PIDFILE exists and PID is in running processes + if [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>/dev/null + then + echo 'Service already running\n' + else + echo "Starting service $NAME" + cd $DAEMON_HOME + nohup 2>/dev/null java -jar $DAEMON &>/dev/null & + expr $! - 1 > $PIDFILE + echo "Service started [`cat $PIDFILE`]\n" + fi +} + +stop() +{ + # If PIDFILE doesn't exists or PID isn't in running processes + if [ ! -f "$PIDFILE" ] || ! kill -0 `cat "$PIDFILE"` + then + echo 'Service not running\n' + else + echo 'Stopping service...' + # Send signal to end to the process + kill -15 `cat "$PIDFILE"` && rm -f "$PIDFILE" + echo 'Service stopped\n' + fi +} + +status() +{ + if [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>/dev/null + then + echo "Service is running (${GREEN}● active${NC})\n" + else + echo "Service not running (${RED}● inactive${NC})\n" + fi +} + +case "$1" in + 'start') + start + ;; + 'stop') + stop + ;; + 'status') + status + ;; + 'restart') + stop + sleep 5 + start + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart}\n" >&2 + exit 3 + ;; +esac +exit 0 diff --git a/src/main/bash/minecraft-server/minecraft-server-service b/src/main/bash/minecraft-server/minecraft-server-service new file mode 100644 index 0000000..67cee47 --- /dev/null +++ b/src/main/bash/minecraft-server/minecraft-server-service @@ -0,0 +1,28 @@ +#!/bin/sh + +USER=takiguchi +MINECRAFT_SERVER_PATH=/home/minecraft/server +MINECRAFT_SHELL=minecraft-server.sh + +case "$1" in + 'start') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL start" + ;; + 'stop') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL stop" + ;; + 'status') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL status" + ;; + 'restart') + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL restart" + ;; + *) + # If no argument, we launch the app in case of server startup + sudo -H -u $USER bash -c "$MINECRAFT_SERVER_PATH/$MINECRAFT_SHELL start &>/dev/null" + # And show the script usage + echo "Usage: /etc/init.d/minecraft {start|stop|status|restart}\n" >&2 + exit 3 + ;; +esac +exit 0 diff --git a/src/main/bash/minecraft-server/minecraft-server.sh b/src/main/bash/minecraft-server/minecraft-server.sh new file mode 100644 index 0000000..41b5125 --- /dev/null +++ b/src/main/bash/minecraft-server/minecraft-server.sh @@ -0,0 +1,158 @@ +#!/bin/sh +# kFreeBSD do not accept scripts as interpreters, using #!/bin/sh and sourcing. +if [ true != "$INIT_D_SCRIPT_SOURCED" ] ; then + set "$0" "$@"; INIT_D_SCRIPT_SOURCED=true . /lib/init/init-d-script +fi + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +DESC="The Minecraft server" +NAME='minecraft-server' +MINECRAFT_HOME='/home/minecraft' +DAEMON_HOME="$MINECRAFT_HOME/server" +DAEMON="$DAEMON_HOME/$NAME.jar" +PIDFILE="$DAEMON_HOME/$NAME.pid" +SCRIPTNAME="$DAEMON_HOME/$0" + +# *********************************************** +# Normal functions for bash commands. +# *********************************************** +start() +{ + # If PIDFILE exists and PID is in running processes + if [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>/dev/null + then + echo 'Service already running\n' + else + echo "Starting service $NAME" + cd $DAEMON_HOME + nohup 2>/dev/null java -jar $DAEMON &>/dev/null & + expr $! - 1 > $PIDFILE + echo "Service started [`cat $PIDFILE`]\n" + fi +} + +stop() +{ + # If PIDFILE doesn't exists or PID isn't in running processes + if [ ! -f "$PIDFILE" ] || ! kill -0 `cat "$PIDFILE"` + then + echo 'Service not running\n' + else + echo 'Stopping service...' + # Send signal to end to the process + kill -15 `cat "$PIDFILE"` && rm -f "$PIDFILE" + echo 'Service stopped\n' + fi +} + +status() +{ + if [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>/dev/null + then + echo "Service is running (${GREEN}● active${NC})\n" + else + echo "Service not running (${RED}● inactive${NC})\n" + fi +} + +# *********************************************** +# Commands used by Minager to drive the server. +# *********************************************** +api_check_error() +{ + if [ $1 != 0 ] + then + exit 1 + fi +} + +api_status() +{ + if [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>/dev/null + then + echo 1 # Running + else + echo 0 # Stopped + fi +} + +api_start() { + # If PIDFILE exists and PID is in running processes + if [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>/dev/null + then + exit 2 # STATE_UNCHANGED + else + cd $DAEMON_HOME + api_check_error $? + + nohup 2>/dev/null java -jar $DAEMON &>/dev/null & + api_check_error $? + + expr $! - 1 > $PIDFILE + api_check_error $? + fi +} + +api_stop() +{ + # If PIDFILE doesn't exists or PID isn't in running processes + if [ ! -f "$PIDFILE" ] || ! kill -0 `cat "$PIDFILE"` + then + exit 2 # STATE_UNCHANGED + else + # Send signal to end to the process + kill -15 `cat "$PIDFILE"` && rm -f "$PIDFILE" + api_check_error $? + fi +} + +api_restart() +{ + # Stop if running + if [ $(api_status) = 1 ] + then + kill -15 `cat "$PIDFILE"` && rm -f "$PIDFILE" + api_check_error $? + fi + + sleep 5 + api_start +} + +case "$1" in + 'start') + start + ;; + 'stop') + stop + ;; + 'status') + status + ;; + 'restart') + stop + sleep 5 + start + ;; + 'api_status') + api_status + ;; + 'api_start') + api_start + ;; + 'api_stop') + api_stop + ;; + 'api_restart') + api_restart + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart}\n" >&2 + exit 3 + ;; +esac +exit 0 diff --git a/src/main/java/org/minager/MinagerApplication.java b/src/main/java/org/minager/MinagerApplication.java new file mode 100644 index 0000000..07d2aa5 --- /dev/null +++ b/src/main/java/org/minager/MinagerApplication.java @@ -0,0 +1,14 @@ +package org.minager; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableAutoConfiguration +public class MinagerApplication { + + public static void main(String[] args) { + SpringApplication.run(MinagerApplication.class, args); + } +} diff --git a/src/main/java/org/minager/account/AccountController.java b/src/main/java/org/minager/account/AccountController.java new file mode 100644 index 0000000..86b8039 --- /dev/null +++ b/src/main/java/org/minager/account/AccountController.java @@ -0,0 +1,96 @@ +package org.minager.account; + +import java.io.IOException; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.minager.core.entities.dto.PasswordWrapperDTO; +import org.minager.core.entities.dto.UserDTO; +import org.minager.core.entities.persistence.User; +import org.minager.core.security.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/account") +public class AccountController { + + private static final String HEADER_TOKEN = "token"; + + @Autowired + private AccountService accountService; + + @Autowired + private TokenService tokenService; + + /** + * Log in the user in request body. + * + * @param pUser + * The user to connect. + * @param response + * The reponse injected by Spring. + * @return The connected user object. + * @throws IOException + * If credentials are bad. + */ + @PostMapping("/login") + public UserDTO login(@RequestBody UserDTO pUser, HttpServletResponse response) throws IOException { + return accountService.checkCredentials(response, pUser); + } + + /** + * Log out the user. + * + * @param pRequest + * The request injected by Spring. + */ + @GetMapping("/logout") + public void logout(HttpServletRequest pRequest) { + tokenService.removeUser(pRequest.getHeader(HEADER_TOKEN)); + } + + /** + * Updates the user password. + * + * @param pPasswordWrapper + * The object which contains the old password for verification and + * the new password to set to the user. + * @param pRequest + * The request injected by Spring. + * @param pResponse + * The reponse injected by Spring. + * @throws IOException + * If the old password doesn't match to the user password in + * database. + */ + @PutMapping("/changePassword") + public void changePassword(@RequestBody final PasswordWrapperDTO pPasswordWrapper, + final HttpServletRequest pRequest, + final HttpServletResponse pResponse) throws IOException { + final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + if(connectedUser.isPresent()) { + accountService.changePassword(connectedUser.get(), pPasswordWrapper, pResponse); + } else { + pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + @PostMapping("/signin") + public UserDTO signin(@RequestBody final UserDTO pUser, final HttpServletResponse pResponse) throws IOException { + return accountService.signin(pUser, pResponse); + } + + @PutMapping("/") + public void update(@RequestBody final UserDTO pUser, final HttpServletRequest pRequest, + final HttpServletResponse pResponse) throws IOException { + accountService.updateUser(pUser, pRequest, pResponse); + } +} diff --git a/src/main/java/org/minager/account/AccountService.java b/src/main/java/org/minager/account/AccountService.java new file mode 100644 index 0000000..9aff9a2 --- /dev/null +++ b/src/main/java/org/minager/account/AccountService.java @@ -0,0 +1,124 @@ +package org.minager.account; + +import java.io.IOException; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.naming.AuthenticationException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.minager.core.entities.dto.PasswordWrapperDTO; +import org.minager.core.entities.dto.UserDTO; +import org.minager.core.entities.persistence.User; +import org.minager.core.repositories.UserRepository; +import org.minager.core.security.TokenService; +import org.minager.core.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class AccountService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenService tokenService; + + /** + * Check the user credentials and generate him a token if they are correct. + * + * @param pUser + * The user sent from client. + * @return The user populated with the generated token. + * @throws IOException + * If the credentials are bad. + * @throws AuthenticationException + * If the credentials are wrong. + */ + public UserDTO checkCredentials(HttpServletResponse pResponse, UserDTO pUser) throws IOException { + UserDTO result = null; + + Optional user = userRepository.findByEmail(pUser.getEmail()); + + if(user.isPresent() && StringUtils.compareHash(pUser.getPassword(), user.get().getPassword())) { + tokenService.addUser(user.get()); + result = new UserDTO(user.get(), true); + } else { + pResponse.sendError(HttpServletResponse.SC_FORBIDDEN); + } + + return result; + } + + public void changePassword(final User pUser, final PasswordWrapperDTO pPasswordWrapper, + final HttpServletResponse pResponse) throws IOException { + if(pPasswordWrapper.getNewPassword().equals(pPasswordWrapper.getConfirmPassword())) { + // We fetch the connected user from database to get his hashed password + final Optional userFromDb = userRepository.findById(pUser.getId()); + if(userFromDb.isPresent() && StringUtils.compareHash(pPasswordWrapper.getOldPassword(), + userFromDb.get().getPassword())) { + userFromDb.get().setPassword(StringUtils.hashPassword(pPasswordWrapper.getNewPassword())); + userRepository.save(userFromDb.get()); + } else { + pResponse.sendError(HttpServletResponse.SC_FORBIDDEN, + "Le mot de passe saisi ne correspond pas au votre."); + } + } else { + pResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, + "Le mot de passe saisi ne correspond pas au votre."); + } + } + + public UserDTO signin(final UserDTO pUser, final HttpServletResponse pResponse) throws IOException { + User user = new User(); + + if(pUser.getName() == null || pUser.getEmail() == null || pUser.getPassword() == null || "".equals(pUser.getPassword().trim())) { + pResponse.sendError(HttpServletResponse.SC_BAD_REQUEST); + } else if(userRepository.findByEmail(pUser.getEmail()).isPresent()) { + pResponse.sendError(HttpServletResponse.SC_CONFLICT); + } else { + user.setName(pUser.getName()); + user.setEmail(pUser.getEmail()); + user.setPassword(StringUtils.hashPassword(pUser.getPassword())); + user.setInscriptionDate(new Date()); + userRepository.save(user); + } + + return new UserDTO(user); + } + + public void updateUser(final UserDTO pUser, final HttpServletRequest pRequest, + final HttpServletResponse pResponse) throws IOException { + final Optional connectedUserOpt = tokenService.getAuthenticatedUserByToken(pRequest); + if(connectedUserOpt.isPresent()) { + final User connectedUser = connectedUserOpt.get(); + + final Optional userFromDb = userRepository.findByEmail(pUser.getEmail()); + + /* + * If a user is returned by the repository, that's the email adress is used, but + * if it is not the same as the connected user, that's the email adress + * corresponds to another user. So a 409 error is sent. Otherwise, if no user is + * returned by the repository, that's the email adress is free to be used. So, + * user can change him email adress. + */ + if(userFromDb.isPresent() && !connectedUser.getEmail().equals(userFromDb.get().getEmail())) { + pResponse.sendError(HttpServletResponse.SC_CONFLICT); + } else { + connectedUser.setName(pUser.getName()); + connectedUser.setEmail(pUser.getEmail()); + + userRepository.save(connectedUser); + } + } else { + pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + } +} diff --git a/src/main/java/org/minager/core/AbstractFilter.java b/src/main/java/org/minager/core/AbstractFilter.java new file mode 100644 index 0000000..56b4918 --- /dev/null +++ b/src/main/java/org/minager/core/AbstractFilter.java @@ -0,0 +1,129 @@ +package org.minager.core; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.minager.core.security.Route; +import org.minager.core.utils.StringUtils; +import org.springframework.http.HttpMethod; + +/** + * Base class for all filters of the application.
+ *
+ * The children classes have to implements the method + * {@link AbstractFilter#getClass()} to set the URLs filtered (with all or some + * http methods), and the method + * {@link AbstractFilter#filter(HttpServletRequest, ServletResponse, FilterChain)} + * to define the filter processing. + * + * @author Takiguchi + * + */ +public abstract class AbstractFilter implements Filter { + + /** Regex url path prefix for method {@link this#isRequestFiltered(String)}. */ + private static final String PREFIX_URL_PATH = "https?:\\/\\/.*(:\\d{0,5})?"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Do nothing + } + + /** + * Returns the list of routes which will be processed by the filter. + * + * @return The routes. + */ + protected abstract List getRoutes(); + + /** + * Filter actions for its processing. + * + * @param request + * The http request. + * @param response + * The response. + * @param chain + * The chain. + */ + protected abstract void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + + if(isRequestFiltered(httpRequest.getRequestURL().toString(), httpRequest.getMethod())) { + filter(httpRequest, (HttpServletResponse) response, chain); + } else { + chain.doFilter(request, response); + } + } + + /** + * Check if the url is allowed with the given method in parameters. + * + * @param pRequestUrl + * The url request. + * @param pRequestMethod + * The http method of the request. + * @return {@code true} if the url is allowed with the method, {@code false} + * otherwise. + */ + boolean isRequestFiltered(final String pRequestUrl, final String pRequestMethod) { + boolean result = false; + + for(final Route route : getRoutes()) { + /* + * Check urls matching, and if the method of the route isn't set, all methods + * are allowed. Otherwise, we check the methods too. + */ + if(Pattern.matches(StringUtils.concat(PREFIX_URL_PATH, route.getUrl()), pRequestUrl)) { + if(!route.getMethod().isPresent() || isMethodFiltered(route, pRequestMethod)) { + result = true; + break; + } + } + } + + return result; + } + + /** + * Checks if the route do filter the method in parameters. + * + * @param pRoute + * The registered route. + * @param pRequestMethod + * The http method to check with the registered route. + */ + boolean isMethodFiltered(final Route pRoute, final String pRequestMethod) { + boolean result = false; + + if(pRoute.getMethod().isPresent()) { + for(final HttpMethod routeMethod : pRoute.getMethod().get()) { + if(routeMethod.name().equals(pRequestMethod)) { + result = true; + break; + } + } + } + + return result; + } + + @Override + public void destroy() { + // Do nothing + } +} diff --git a/src/main/java/org/minager/core/config/JpaConfiguration.java b/src/main/java/org/minager/core/config/JpaConfiguration.java new file mode 100644 index 0000000..89a13c4 --- /dev/null +++ b/src/main/java/org/minager/core/config/JpaConfiguration.java @@ -0,0 +1,42 @@ +package org.minager.core.config; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EntityScan("org.minager") +@EnableTransactionManagement +@EnableJpaRepositories("org.minager") +@PropertySource("classpath:application.properties") +public class JpaConfiguration { + + @Value("${spring.datasource.driverClassName}") + private String driverClassName; + + @Value("${spring.datasource.url}") + private String url; + + @Value("${spring.datasource.username}") + private String username; + + @Value("${spring.datasource.password}") + private String password; + + @Bean(name="dataSource") + public DataSource getDataSource() { + return DataSourceBuilder.create() + .username(username) + .password(password) + .url(url) + .driverClassName(driverClassName) + .build(); + } +} diff --git a/src/main/java/org/minager/core/constant/ResultCode.java b/src/main/java/org/minager/core/constant/ResultCode.java new file mode 100644 index 0000000..a7098a9 --- /dev/null +++ b/src/main/java/org/minager/core/constant/ResultCode.java @@ -0,0 +1,18 @@ +package org.minager.core.constant; + +public enum ResultCode { + SUCCESS(0), + FAILED(1), + STATE_UNCHANGED(2), + ILLEGAL_ARGUMENT(3); + + private int val; + + private ResultCode(final int pVal) { + val = pVal; + } + + public int val() { + return val; + } +} diff --git a/src/main/java/org/minager/core/entities/business/SystemResult.java b/src/main/java/org/minager/core/entities/business/SystemResult.java new file mode 100644 index 0000000..fbd6d05 --- /dev/null +++ b/src/main/java/org/minager/core/entities/business/SystemResult.java @@ -0,0 +1,99 @@ +package org.minager.core.entities.business; + +/** + * Object that contains the results of a system command. + * + * @author takiguchi + * + */ +public class SystemResult { + /** The result code of the command. */ + private int resultCode; + /** The standard output of the command. */ + private StringBuilder stdOut; + /** The standard errors output of the command. */ + private StringBuilder stdErr; + + public SystemResult() { + super(); + stdOut = new StringBuilder(); + stdErr = new StringBuilder(); + } + + /** + * Gets the result code. + * + * @return the result code + */ + public int getResultCode() { + return resultCode; + } + + /** + * Sets the result code. + * + * @param resultCode + * the new result code + */ + public void setResultCode(int resultCode) { + this.resultCode = resultCode; + } + + /** + * Gets the std out. + * + * @return the std out + */ + public String getStdOut() { + return stdOut.toString(); + } + + /** + * Add an output line to the {@code stdOut}. + * + * @param pLine + * The output line to append. + */ + public void addOutputLine(final String pLine) { + stdOut.append(pLine); + } + + /** + * Sets the std out. + * + * @param stdOut + * the new std out + */ + public void setStdOut(String stdOut) { + this.stdOut = new StringBuilder(stdOut); + } + + /** + * Gets the std err. + * + * @return the std err + */ + public String getStdErr() { + return stdErr.toString(); + } + + /** + * Add an error line to the {@code stdErr}. + * + * @param pLine + * The error line to append. + */ + public void addErrorLine(final String pLine) { + stdErr.append(pLine); + } + + /** + * Sets the std err. + * + * @param stdErr + * the new std err + */ + public void setStdErr(String stdErr) { + this.stdErr = new StringBuilder(stdErr); + } +} \ No newline at end of file diff --git a/src/main/java/org/minager/core/entities/dto/PasswordWrapperDTO.java b/src/main/java/org/minager/core/entities/dto/PasswordWrapperDTO.java new file mode 100644 index 0000000..2de50a1 --- /dev/null +++ b/src/main/java/org/minager/core/entities/dto/PasswordWrapperDTO.java @@ -0,0 +1,34 @@ +package org.minager.core.entities.dto; + +public class PasswordWrapperDTO { + + private String oldPassword; + + private String newPassword; + + private String confirmPassword; + + public String getOldPassword() { + return oldPassword; + } + + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } +} diff --git a/src/main/java/org/minager/core/entities/dto/UserDTO.java b/src/main/java/org/minager/core/entities/dto/UserDTO.java new file mode 100644 index 0000000..944e9e3 --- /dev/null +++ b/src/main/java/org/minager/core/entities/dto/UserDTO.java @@ -0,0 +1,97 @@ +package org.minager.core.entities.dto; + +import java.util.Date; + +import org.minager.core.entities.persistence.User; + +public class UserDTO { + + private String key; + + private String name; + + private String email; + + private String password; + + private String image; + + private Date inscriptionDate; + + private String token; + + public UserDTO() { + super(); + } + + public UserDTO(final User pUser) { + key = pUser.getKey(); + name = pUser.getName(); + email = pUser.getEmail(); + image = pUser.getImage(); + inscriptionDate = pUser.getInscriptionDate(); + } + + public UserDTO(final User pUser, final boolean pWithToken) { + this(pUser); + if(pWithToken) { + token = pUser.getToken().getValue(); + } + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public Date getInscriptionDate() { + return inscriptionDate; + } + + public void setInscriptionDate(Date inscriptionDate) { + this.inscriptionDate = inscriptionDate; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/org/minager/core/entities/persistence/User.java b/src/main/java/org/minager/core/entities/persistence/User.java new file mode 100644 index 0000000..0277c38 --- /dev/null +++ b/src/main/java/org/minager/core/entities/persistence/User.java @@ -0,0 +1,126 @@ +package org.minager.core.entities.persistence; + +import java.io.Serializable; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.GenerationTime; +import org.minager.core.entities.security.Token; + +@Entity +@Table(name="`user`") +public class User implements Serializable { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="user_id_seq") + @SequenceGenerator(name="user_id_seq", sequenceName="user_id_seq", allocationSize=1) + private Long id; + + // This annotation serves to fetch the attribute after an insert into db + @Generated(GenerationTime.ALWAYS) + private String key; + + private String name; + + private String email; + + private String password; + + private String image; + + @Column(name = "inscription_date") + @Temporal(TemporalType.TIMESTAMP) + private Date inscriptionDate; + + /** Authentication token. */ + private transient Token token; + + /* ******************* */ + /* Constructors */ + /* ******************* */ + public User() { + super(); + token = new Token(); + } + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public Date getInscriptionDate() { + return inscriptionDate; + } + + public void setInscriptionDate(Date inscriptionDate) { + this.inscriptionDate = inscriptionDate; + } + + public Token getToken() { + return token; + } + +} diff --git a/src/main/java/org/minager/core/entities/security/Token.java b/src/main/java/org/minager/core/entities/security/Token.java new file mode 100644 index 0000000..c2a314d --- /dev/null +++ b/src/main/java/org/minager/core/entities/security/Token.java @@ -0,0 +1,82 @@ +package org.minager.core.entities.security; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Calendar; + +public class Token { + /** The metric in which the validation delay is defined. */ + private static final int METRIC = Calendar.MINUTE; + /** Number of {@link METRIC} after that the token become invalid. */ + private static final int DELAY = 30; + + /** The Constant BITS_NUMBER. */ + private static final int BITS_NUMBER = 1000; + /** The Constant RADIX. */ + private static final int RADIX = 32; + + /** The value. */ + private String value; + + /** + * Last access date. For each request to the server, this date is consulted + * and if the valid delay is ok, this date must be updated. + */ + private Calendar lastAccess; + + /** + * Instantiates a new token. + */ + public Token() { + super(); + value = new BigInteger(BITS_NUMBER, new SecureRandom()).toString(RADIX); + lastAccess = Calendar.getInstance(); + } + + /** + * Gets the value. + * + * @return the value + */ + public String getValue() { + return value; + } + + /** + * Gets the last access date. + * + * @return the last access date + */ + public Calendar getLastAccess() { + return lastAccess; + } + + /** + * Sets the last access date. + */ + public void setLastAccess() { + lastAccess = Calendar.getInstance(); + } + + /** + * Indicate if the token is still valid.
+ * A token is valid is its {@link Token#lastAccess} is after the current + * date minus the {@link Token#DELAY} {@link Token#METRIC}.
+ *
+ * Example:
+ * {@link Token#DELAY} = 30 and {@link Token#METRIC} = + * {@link Calendar#MINUTE}.
+ * A token is valid only on the 30 minutes after its + * {@link Token#lastAccess}.
+ * If the current date-time minus the 30 minutes is before the + * {@link Token#lastAccess}, the token is still valid. + * + * @return {@code true} if the token is still valid, {@code false} + * otherwise. + */ + public boolean isValid() { + final Calendar lastTimeValidation = Calendar.getInstance(); + lastTimeValidation.add(METRIC, -DELAY); + return lastAccess.getTime().after(lastTimeValidation.getTime()); + } +} diff --git a/src/main/java/org/minager/core/repositories/UserRepository.java b/src/main/java/org/minager/core/repositories/UserRepository.java new file mode 100644 index 0000000..31d1601 --- /dev/null +++ b/src/main/java/org/minager/core/repositories/UserRepository.java @@ -0,0 +1,39 @@ +package org.minager.core.repositories; + +import java.util.Optional; + +import javax.transaction.Transactional; + +import org.minager.core.entities.persistence.User; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends CrudRepository { + + Optional findByEmail(@Param("email") final String pEmail); + + /** + * Checks if the password in parameters is the passwords of the user in + * database. + * + * @param pId + * The user id. + * @param pPassword + * The password to check. + * @return {@code true} if the password is the user password in database, + * {@code false} otherwise. + */ + @Query(value = "SELECT CASE WHEN EXISTS(" + + " SELECT id FROM \"user\" WHERE id = :id AND password = :password" + + ") THEN TRUE ELSE FALSE END", nativeQuery = true) + boolean checkPassword(@Param("id") final Long pId, @Param("password") final String pPassword); + + @Query(value = "UPDATE \"user\" SET password = :password WHERE id = :id", nativeQuery = true) + @Transactional + @Modifying + void updatePassword(@Param("id") final Long pId, @Param("password") final String pPassword); +} diff --git a/src/main/java/org/minager/core/security/AuthenticationFilter.java b/src/main/java/org/minager/core/security/AuthenticationFilter.java new file mode 100644 index 0000000..3805b78 --- /dev/null +++ b/src/main/java/org/minager/core/security/AuthenticationFilter.java @@ -0,0 +1,47 @@ +package org.minager.core.security; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.minager.core.AbstractFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class AuthenticationFilter extends AbstractFilter { + + private static final String HTTP_OPTIONS = "OPTIONS"; + + private static final String HEADER_TOKEN = "token"; + + @Autowired + private TokenService tokenService; + + @Override + protected List getRoutes() { + return Arrays.asList( + new Route("\\/api\\/server\\/start"), + new Route("\\/api\\/server\\/stop"), + new Route("\\/api\\/server\\/restart") + ); + } + + @Override + protected void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + if(HTTP_OPTIONS.equals(request.getMethod()) || tokenService.isUserConnected(request.getHeader(HEADER_TOKEN))) { + chain.doFilter(request, response); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + +} diff --git a/src/main/java/org/minager/core/security/CorsFilter.java b/src/main/java/org/minager/core/security/CorsFilter.java new file mode 100644 index 0000000..daf1bea --- /dev/null +++ b/src/main/java/org/minager/core/security/CorsFilter.java @@ -0,0 +1,44 @@ +package org.minager.core.security; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class CorsFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Do nothing + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.addHeader("Access-Control-Allow-Origin", "*"); + httpResponse.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, HEAD"); + httpResponse.addHeader("Access-Control-Allow-Headers", "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers"); + httpResponse.addHeader("Access-Control-Expose-Headers", "Access-Control-Allow-Origin, Access-Control-Allow-Credentials"); + httpResponse.addHeader("Access-Control-Allow-Credentials", "true"); + httpResponse.addIntHeader("Access-Control-Max-Age", 10); + chain.doFilter(request, httpResponse); + } + + @Override + public void destroy() { + // Do nothing + } + +} diff --git a/src/main/java/org/minager/core/security/Route.java b/src/main/java/org/minager/core/security/Route.java new file mode 100644 index 0000000..927843b --- /dev/null +++ b/src/main/java/org/minager/core/security/Route.java @@ -0,0 +1,71 @@ +package org.minager.core.security; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpMethod; + +/** + * Route for filter matching. + * + * @author Takiguchi + * + */ +public class Route { + /** The regex to match urls. */ + private String url; + /** The http method to match. Use a {@link Optional#empty()} to match all methods. */ + private Optional> method; + + /** + * Instanciate a vierge route. + */ + public Route() { + super(); + url = ""; + method = Optional.empty(); + } + + /** + * Instanciate a route for all http methods. + * + * @param pUrl + * The regex to match urls. + */ + public Route(final String pUrl) { + this(); + this.url = pUrl; + } + + /** + * Instanciate a route for methods given in parameters + * + * @param pUrl + * The regex to match urls. + * @param pMethod + * The http method to match. Use a {@link Optional#empty()} to match + * all methods. + */ + public Route(final String pUrl, final HttpMethod... pMethods) { + this(pUrl); + this.method = Optional.of(Arrays.asList(pMethods)); + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Optional> getMethod() { + return method; + } + + public void setMethod(HttpMethod pMethods) { + this.method = Optional.of(Arrays.asList(pMethods)); + } + +} diff --git a/src/main/java/org/minager/core/security/TokenService.java b/src/main/java/org/minager/core/security/TokenService.java new file mode 100644 index 0000000..fc16093 --- /dev/null +++ b/src/main/java/org/minager/core/security/TokenService.java @@ -0,0 +1,132 @@ +package org.minager.core.security; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import javax.servlet.http.HttpServletRequest; + +import org.minager.core.entities.persistence.User; +import org.springframework.stereotype.Service; + +@Service +public class TokenService { + /** Map of connected users. */ + private static final Map connectedUsers; + + private static final String HEADER_TOKEN = "token"; + + private static final long INTERVAL_USER_CLEANING = 5; + + private static final long INTERVAL_USER_CLEANING_VAL = INTERVAL_USER_CLEANING * 60 * 1000; + + private Date lastUsersCleaning; + + /** + * Class constructor + */ + static { + connectedUsers = new TreeMap<>(); + } + + /** + * Check if the token matches to a user session, and if it is still valid. + * + * @param pToken + * The token to check. + * @return {@code true} if the token is still valid, {@code false} + * otherwise. + */ + public boolean isUserConnected(final String pToken) { + boolean result = false; + + if (pToken != null && connectedUsers.containsKey(pToken)) { + if (connectedUsers.get(pToken).getToken().isValid()) { + result = true; + } else { + connectedUsers.remove(pToken); + } + } + + // clear all the expired sessions + final Date now = new Date(); + if(lastUsersCleaning == null || now.getTime() - lastUsersCleaning.getTime() >= INTERVAL_USER_CLEANING_VAL) { + new Thread(this::clearExpiredUsers).start(); + lastUsersCleaning = now; + } + + + return result; + } + + /** + * Remove from the connected users map all the elements which their token is + * expired. + */ + private void clearExpiredUsers() { + synchronized (this) { + List usersToRemove = new LinkedList<>(); + connectedUsers.entrySet().stream().forEach(user -> { + if(!user.getValue().getToken().isValid()) { + usersToRemove.add(user.getValue()); + } + }); + usersToRemove.stream().map(User::getKey).forEach(connectedUsers::remove); + } + } + + /** + * Add the user to the connected users map. + * + * @param pUser + * The user to add. + */ + public void addUser(final User pUser) { + if(connectedUsers.get(pUser.getToken().getValue()) == null) { + connectedUsers.put(pUser.getToken().getValue(), pUser); + } + } + + /** + * Refresh the user token last access date in the token service. + * + * @param pToken + * The user token. + */ + public void refreshUserToken(final String pToken) { + final User user = connectedUsers.get(pToken); + if(user != null) { + user.getToken().setLastAccess(); + } + } + + /** + * Remove the user to the connected users map. + * + * @param pUser + * The user to remove. + */ + public void removeUser(final User pUser) { + removeUser(pUser.getToken().getValue()); + } + + /** + * Remove the user associated to the token given in parameters, from the + * connected users map. + * + * @param pToken + * The user to delete token. + */ + public void removeUser(final String pToken) { + if(pToken != null && connectedUsers.containsKey(pToken)) { + connectedUsers.remove(pToken); + } + } + + public Optional getAuthenticatedUserByToken(final HttpServletRequest pRequest) { + return Optional.ofNullable(connectedUsers.get(pRequest.getHeader(HEADER_TOKEN))); + } +} diff --git a/src/main/java/org/minager/core/services/business/SystemService.java b/src/main/java/org/minager/core/services/business/SystemService.java new file mode 100644 index 0000000..ff7839e --- /dev/null +++ b/src/main/java/org/minager/core/services/business/SystemService.java @@ -0,0 +1,69 @@ +package org.minager.core.services.business; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.minager.core.entities.business.SystemResult; +import org.springframework.stereotype.Service; + +@Service +public class SystemService { + + public SystemResult executeCommand(final String pCommand, final String... pArgs) { + final String command = buildCommand(pCommand, pArgs); + + final SystemResult commandResults = new SystemResult(); + + try { + // Process creation and execution of the command. + final Process process = Runtime.getRuntime().exec(command); + + // Waiting the end of the command execution. + process.waitFor(); + + // Getting of the stantard command output, and the standard error output. + BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + + String tempLine = null; + while((tempLine = outputReader.readLine()) != null) { + commandResults.addOutputLine(tempLine); + } + + while((tempLine = errorReader.readLine()) != null) { + commandResults.addErrorLine(tempLine); + } + + commandResults.setResultCode(process.exitValue()); + } catch(IOException | InterruptedException ex) { +// LOGGER.error(StringUtils.concat("Une erreur est survenue lors de l'exécution de la commande \"", command, +// "\"."), ex); + System.err.println(ex.getMessage()); + ex.printStackTrace(); + } + + return commandResults; + } + + /** + * Build the command in form of one string from the parameters. + * + * @param pCommand + * The command to execute. + * @param pArgs + * The command arguments, could be {@code null}. + * @return The command built. + */ + private String buildCommand(final String pCommand, final Object... pArgs) { + final StringBuilder command = new StringBuilder(pCommand); + + if (pArgs != null) { + for (final Object arg : pArgs) { + command.append(" ").append(arg); + } + } + + return command.toString(); + } +} diff --git a/src/main/java/org/minager/core/utils/RegexUtils.java b/src/main/java/org/minager/core/utils/RegexUtils.java new file mode 100644 index 0000000..f42912d --- /dev/null +++ b/src/main/java/org/minager/core/utils/RegexUtils.java @@ -0,0 +1,82 @@ +package org.minager.core.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class RegexUtils { + + private static final String EMAIL_REGEX = "^.*@.*\\..{2,}$"; + private static final String LOWER_LETTERS_REGEX = ".*[a-z].*"; + private static final String UPPER_LETTERS_REGEX = ".*[A-Z].*"; + private static final String NUMBER_REGEX = ".*[0-9].*"; + private static final String SPECIAL_CHAR_REGEX = ".*\\W.*"; + private static final String NUMBER_ONLY_REGEX = "^[0-9]+$"; + + // La portée "package" permet à la classe StringUtils d'utiliser les patterns + // suivants : + static final Pattern EMAIL_PATTERN; + static final Pattern LOWER_LETTERS_PATTERN; + static final Pattern UPPER_LETTERS_PATTERN; + static final Pattern NUMBER_PATTERN; + static final Pattern SPECIAL_CHAR_PATTERN; + static final Pattern NUMBER_ONLY_PATTERN; + + static { + EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + LOWER_LETTERS_PATTERN = Pattern.compile(LOWER_LETTERS_REGEX); + UPPER_LETTERS_PATTERN = Pattern.compile(UPPER_LETTERS_REGEX); + NUMBER_PATTERN = Pattern.compile(NUMBER_REGEX); + SPECIAL_CHAR_PATTERN = Pattern.compile(SPECIAL_CHAR_REGEX); + NUMBER_ONLY_PATTERN = Pattern.compile(NUMBER_ONLY_REGEX); + } + + /** + * Chekcs if {@code pString} corresponds to an email address. + * + * @param pString + * The string which should be an email address. + * @return {@code true} if {@link pString} corresponds to an email address, + * {@code false} otherwise. + */ + public static boolean isEmail(final String pString) { + return EMAIL_PATTERN.matcher(pString).find(); + } + + /** + * Replace the sequences of {@code pString} matched by the {@code pRegex} + * with the {@code pReplacingString}. + * + * @param pString + * The string to update. + * @param pRegex + * The regex to match the sentences to replace. + * @param pReplacingString + * The string to replace the sentences which match with the + * regex. + * @return The new string. + */ + public static String replaceSequence(final String pString, + final String pRegex, final String pReplacingString) { + return Pattern.compile(pRegex).matcher(pString) + .replaceAll(pReplacingString); + } + + /** + * Checks if {@code pString} corresponds to a number. + * + * @param pString + * The string which should be a number. + * @return {@code true} if {@code pString} corresponds to a number, + * {@code false} otherwise. + */ + public static boolean isNumber(final String pString) { + return NUMBER_ONLY_PATTERN.matcher(pString).find(); + } + + public static String getGroup(final String regex, final int numeroGroupe, final String chaine) { + final Pattern pattern = Pattern.compile(regex); + final Matcher matcher = pattern.matcher(chaine); + matcher.find(); + return matcher.group(numeroGroupe); + } +} \ No newline at end of file diff --git a/src/main/java/org/minager/core/utils/StringUtils.java b/src/main/java/org/minager/core/utils/StringUtils.java new file mode 100644 index 0000000..117d417 --- /dev/null +++ b/src/main/java/org/minager/core/utils/StringUtils.java @@ -0,0 +1,93 @@ +package org.minager.core.utils; + +import org.mindrot.jbcrypt.BCrypt; + +/** + * Generic methods about {@link String} class. + * + * @author takiguchi + * + */ +public final class StringUtils { + + /** + * Indicate if {@code pString} is null or just composed of spaces. + * + * @param pString + * The string to test. + * @return {@code true} if {@code pString} is null or just composed of + * spaces, {@code false} otherwise. + */ + public static boolean isNull(final String chaine) { + return chaine == null || chaine.trim().length() == 0; + } + + /** + * Hash the password given into parameters. + * + * @param pPassword The password to hash. + * @return The password hashed. + */ + public static String hashPassword(final String pPassword) { + return hashString(pPassword, 10); + } + + public static String hashString(final String pString, final int pSalt) { + return BCrypt.hashpw(pString, BCrypt.gensalt(pSalt)); + } + + /** + * Compare the password and the hashed string given into parameters. + * + * @param pPassword + * The password to compare to the hashed string. + * @param pHashToCompare + * The hashed string to compare to the password. + * @return {@code true} if the password matches to the hashed string. + */ + public static boolean compareHash(final String pPassword, final String pHashToCompare) { + return BCrypt.checkpw(pPassword, pHashToCompare); + } + + /** + * Concatenate the parameters to form just one single string. + * + * @param pArgs + * The strings to concatenate. + * @return The parameters concatenated. + */ + public static String concat(final Object... pArgs) { + final StringBuilder result = new StringBuilder(); + for (final Object arg : pArgs) { + result.append(arg); + } + return result.toString(); + } + + public static String printStrings(final String... pStrings) { + final StringBuilder result = new StringBuilder(); + for (int i = 0 ; i < pStrings.length ; i++) { + result.append(pStrings[i]); + if(i < pStrings.length - 1) { + result.append(","); + } + } + return result.toString(); + } + + public static boolean containLowercase(final String pString) { + return RegexUtils.LOWER_LETTERS_PATTERN.matcher(pString).find(); + } + + public static boolean containUppercase(final String pString) { + return RegexUtils.UPPER_LETTERS_PATTERN.matcher(pString).find(); + } + + public static boolean containNumber(final String pString) { + return RegexUtils.NUMBER_PATTERN.matcher(pString).find(); + } + + public static boolean containSpecialChar(final String pString) { + return RegexUtils.SPECIAL_CHAR_PATTERN.matcher(pString).find(); + } +} diff --git a/src/main/java/org/minager/serverhandling/ServerHandlingController.java b/src/main/java/org/minager/serverhandling/ServerHandlingController.java new file mode 100644 index 0000000..ad25eed --- /dev/null +++ b/src/main/java/org/minager/serverhandling/ServerHandlingController.java @@ -0,0 +1,38 @@ +package org.minager.serverhandling; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/server") +public class ServerHandlingController { + @Autowired + private ServerHandlingService serverHandlingService; + + @GetMapping("/status") + public int getStatus() { + return serverHandlingService.getStatus(); + } + + @PostMapping("/start") + public void startServer(final HttpServletResponse pResponse) throws IOException { + serverHandlingService.startServer(pResponse); + } + + @PostMapping("/stop") + public void stopServer(final HttpServletResponse pResponse) throws IOException { + serverHandlingService.stopServer(pResponse); + } + + @PostMapping("/restart") + public void restartServer(final HttpServletResponse pResponse) throws IOException { + serverHandlingService.restartServer(pResponse); + } +} diff --git a/src/main/java/org/minager/serverhandling/ServerHandlingService.java b/src/main/java/org/minager/serverhandling/ServerHandlingService.java new file mode 100644 index 0000000..313aacf --- /dev/null +++ b/src/main/java/org/minager/serverhandling/ServerHandlingService.java @@ -0,0 +1,59 @@ +package org.minager.serverhandling; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.minager.core.constant.ResultCode; +import org.minager.core.entities.business.SystemResult; +import org.minager.core.services.business.SystemService; +import org.minager.core.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class ServerHandlingService { + + @Autowired + private SystemService systemService; + + @Value("${minecraft.server.path}") + private String minecraftServerPath; + + @Value("${minecraft.server.shell.name}") + private String minecraftServerShellName; + + private String buildMinecraftServerShellPath() { + return StringUtils.concat(minecraftServerPath, + minecraftServerPath.charAt(minecraftServerPath.length() - 1) == '/' ? "" : '/', + minecraftServerShellName); + } + + public int getStatus() { + final SystemResult shellResult = systemService.executeCommand(buildMinecraftServerShellPath(), "api_status"); + return Integer.parseInt(shellResult.getStdOut()); + } + + private void startOrStopServer(final HttpServletResponse pResponse, final String pAction) throws IOException { + final SystemResult shellResult = systemService.executeCommand(buildMinecraftServerShellPath(), pAction); + + if(shellResult.getResultCode() == ResultCode.FAILED.val()) { + pResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + } else if(shellResult.getResultCode() == ResultCode.STATE_UNCHANGED.val()) { + pResponse.sendError(HttpServletResponse.SC_CONFLICT); + } // else -> SUCCESS, so code 200 + } + + public void startServer(final HttpServletResponse pResponse) throws IOException { + startOrStopServer(pResponse, "api_start"); + } + + public void stopServer(final HttpServletResponse pResponse) throws IOException { + startOrStopServer(pResponse, "api_stop"); + } + + public void restartServer(final HttpServletResponse pResponse) throws IOException { + startOrStopServer(pResponse, "api_restart"); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..06bb90a --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,15 @@ +# *********************************************** +# Hibernate database configuration +# *********************************************** +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/db_minager +spring.datasource.username=minager +spring.datasource.password=P@ssword +# Disable feature detection by this undocumented parameter. Check the org.hibernate.engine.jdbc.internal.JdbcServiceImpl.configure method for more details. +spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false + +# *********************************************** +# Application custom parameters +# *********************************************** +minecraft.server.path=/home/minecraft/server +minecraft.server.shell.name=minecraft-server.sh \ No newline at end of file diff --git a/src/main/sql/0_init_db.sql b/src/main/sql/0_init_db.sql new file mode 100644 index 0000000..29aa756 --- /dev/null +++ b/src/main/sql/0_init_db.sql @@ -0,0 +1,25 @@ +-- Execute with psql as postgres user +create user minager_admin with password 'P@ssword'; +create user minager with password 'P@ssword'; +create database db_minager owner minager_admin; +\c db_minager; + +-- ****************************************************************** +-- T A B L E S +-- ****************************************************************** +drop table if exists "user"; + +create table "user" ( + "id" serial, + "key" varchar, + "name" varchar, + "email" varchar, + "password" varchar, + "image" varchar, + "inscription_date" timestamp, + constraint user_pk primary key ("id"), + constraint user_key_unique unique ("key"), + constraint user_email_unique unique ("email") +); +grant select, insert, update, delete on "user" to minager; +grant usage, select, update on user_id_seq to minager; \ No newline at end of file diff --git a/src/main/ts/.editorconfig b/src/main/ts/.editorconfig new file mode 100644 index 0000000..6e87a00 --- /dev/null +++ b/src/main/ts/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/src/main/ts/README.md b/src/main/ts/README.md new file mode 100644 index 0000000..29dcc0d --- /dev/null +++ b/src/main/ts/README.md @@ -0,0 +1,27 @@ +# Ts + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.2.3. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/src/main/ts/angular.json b/src/main/ts/angular.json new file mode 100644 index 0000000..ae457b5 --- /dev/null +++ b/src/main/ts/angular.json @@ -0,0 +1,137 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "ts": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": { + "@schematics/angular:component": { + "styleext": "scss" + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/ts", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/favicon.png", + "src/assets" + ], + "styles": [ + "node_modules/font-awesome/scss/font-awesome.scss", + "node_modules/angular-bootstrap-md/scss/bootstrap/bootstrap.scss", + "node_modules/angular-bootstrap-md/scss/mdb-free.scss", + "src/styles.scss" + ], + "scripts": [ + "node_modules/chart.js/dist/Chart.js", + "node_modules/hammerjs/hammer.min.js" + ] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "ts:build" + }, + "configurations": { + "production": { + "browserTarget": "ts:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "ts:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/styles.scss" + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "ts-e2e": { + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "ts:serve" + }, + "configurations": { + "production": { + "devServerTarget": "ts:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "ts" +} \ No newline at end of file diff --git a/src/main/ts/e2e/protractor.conf.js b/src/main/ts/e2e/protractor.conf.js new file mode 100644 index 0000000..86776a3 --- /dev/null +++ b/src/main/ts/e2e/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.e2e.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; \ No newline at end of file diff --git a/src/main/ts/e2e/src/app.e2e-spec.ts b/src/main/ts/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000..9fe8cab --- /dev/null +++ b/src/main/ts/e2e/src/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to ts!'); + }); +}); diff --git a/src/main/ts/e2e/src/app.po.ts b/src/main/ts/e2e/src/app.po.ts new file mode 100644 index 0000000..82ea75b --- /dev/null +++ b/src/main/ts/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/src/main/ts/e2e/tsconfig.e2e.json b/src/main/ts/e2e/tsconfig.e2e.json new file mode 100644 index 0000000..a6dd622 --- /dev/null +++ b/src/main/ts/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/src/main/ts/package.json b/src/main/ts/package.json new file mode 100644 index 0000000..91d2773 --- /dev/null +++ b/src/main/ts/package.json @@ -0,0 +1,55 @@ +{ + "name": "ts", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "^6.1.0", + "@angular/common": "^6.1.0", + "@angular/compiler": "^6.1.0", + "@angular/core": "^6.1.0", + "@angular/forms": "^6.1.0", + "@angular/http": "^6.1.0", + "@angular/platform-browser": "^6.1.0", + "@angular/platform-browser-dynamic": "^6.1.0", + "@angular/router": "^6.1.0", + "@ngtools/webpack": "^1.2.4", + "@types/chart.js": "^2.7.36", + "angular-bootstrap-md": "^6.2.4", + "angular5-csv": "^0.2.10", + "chart.js": "^2.5.0", + "core-js": "^2.5.4", + "font-awesome": "^4.7.0", + "hammerjs": "^2.0.8", + "rxjs": "~6.2.0", + "zone.js": "~0.8.26" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~0.8.0", + "@angular/cli": "~6.2.3", + "@angular/compiler-cli": "^6.1.0", + "@angular/language-service": "^6.1.0", + "@types/jasmine": "~2.8.8", + "@types/jasminewd2": "~2.0.3", + "@types/node": "~8.9.4", + "codelyzer": "~4.3.0", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~3.0.0", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.1", + "karma-jasmine": "~1.1.2", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.4.0", + "ts-node": "~7.0.0", + "tslint": "~5.11.0", + "typescript": "~2.9.2" + } +} diff --git a/src/main/ts/src/app/app.component.html b/src/main/ts/src/app/app.component.html new file mode 100644 index 0000000..f35249f --- /dev/null +++ b/src/main/ts/src/app/app.component.html @@ -0,0 +1,6 @@ + +
+ +
+ + diff --git a/src/main/ts/src/app/app.component.scss b/src/main/ts/src/app/app.component.scss new file mode 100644 index 0000000..13dbfd3 --- /dev/null +++ b/src/main/ts/src/app/app.component.scss @@ -0,0 +1,4 @@ +main { + margin-top: 84px; + padding-top: 25px; +} \ No newline at end of file diff --git a/src/main/ts/src/app/app.component.spec.ts b/src/main/ts/src/app/app.component.spec.ts new file mode 100644 index 0000000..8d664b2 --- /dev/null +++ b/src/main/ts/src/app/app.component.spec.ts @@ -0,0 +1,27 @@ +import { TestBed, async } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + it(`should have as title 'ts'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('ts'); + })); + it('should render title in a h1 tag', async(() => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to ts!'); + })); +}); diff --git a/src/main/ts/src/app/app.component.ts b/src/main/ts/src/app/app.component.ts new file mode 100644 index 0000000..7d943bc --- /dev/null +++ b/src/main/ts/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'app'; +} diff --git a/src/main/ts/src/app/app.module.ts b/src/main/ts/src/app/app.module.ts new file mode 100644 index 0000000..7ce80c3 --- /dev/null +++ b/src/main/ts/src/app/app.module.ts @@ -0,0 +1,68 @@ +// ********************************************** +// Angular Core +// ********************************************** +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; + +// ********************************************** +// External dependencies +// ********************************************** +import { MDBBootstrapModule } from 'angular-bootstrap-md'; + +// ********************************************** +// App Components +// ********************************************** +import { AppComponent } from './app.component'; +import { HeaderComponent } from './header/header.component'; +import { ServerComponent } from './server/server.component'; +import { LoginComponent } from './login/login.component'; +import { DisconnectionComponent } from './disconnection/disconnection.component'; + +// ********************************************** +// App Services +// ********************************************** +import { ServerService } from './server/server.service'; +import { LoginService } from './login/login.service'; +import { AuthService } from './core/services/auth.service'; + +// Router +import { appRoutes } from './app.routes'; + +// Interceptor +import { TokenInterceptor } from './core/interceptors/token-interceptor'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + ServerComponent, + LoginComponent, + DisconnectionComponent + ], + imports: [ + BrowserModule, + HttpClientModule, + FormsModule, + RouterModule.forRoot( + appRoutes, + // { enableTracing: true } // Enabling tracing + { onSameUrlNavigation: 'reload' } + ), + MDBBootstrapModule.forRoot() + ], + providers: [ + ServerService, + LoginService, + AuthService, + { + provide: HTTP_INTERCEPTORS, + useClass: TokenInterceptor, + multi: true + } + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/src/main/ts/src/app/app.routes.ts b/src/main/ts/src/app/app.routes.ts new file mode 100644 index 0000000..838e824 --- /dev/null +++ b/src/main/ts/src/app/app.routes.ts @@ -0,0 +1,15 @@ +import { Routes } from '@angular/router'; + +import { LoginComponent } from './login/login.component'; +import { DisconnectionComponent } from './disconnection/disconnection.component'; +import { ServerComponent } from './server/server.component'; + +// import { AuthGuard } from './core/guards/auth.guard'; + +export const appRoutes: Routes = [ + { path: 'login', component: LoginComponent }, + { path: 'disconnection', component: DisconnectionComponent }, + { path: 'server', component: ServerComponent }, + { path: '', redirectTo: '/server', pathMatch: 'full' }, + { path: '**', redirectTo: '/server', pathMatch: 'full' } +]; diff --git a/src/main/ts/src/app/core/entities.ts b/src/main/ts/src/app/core/entities.ts new file mode 100644 index 0000000..8c0f12e --- /dev/null +++ b/src/main/ts/src/app/core/entities.ts @@ -0,0 +1,10 @@ +export class User { + constructor( + public key: string, + public name: string, + public email: string, + public password: string, + public inscriptionDate: Date, + public token: string + ) { } +} diff --git a/src/main/ts/src/app/core/interceptors/token-interceptor.ts b/src/main/ts/src/app/core/interceptors/token-interceptor.ts new file mode 100644 index 0000000..0a002d2 --- /dev/null +++ b/src/main/ts/src/app/core/interceptors/token-interceptor.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { Router } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { map, filter, tap } from 'rxjs/operators'; + +import { User } from '../entities'; +import { AuthService } from '../services/auth.service'; + +@Injectable() +export class TokenInterceptor implements HttpInterceptor { + + constructor( + private authService: AuthService, + private router: Router + ) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const token = this.authService.getToken(); + + let request: HttpRequest = req; + + if (token) { + request = req.clone({ + setHeaders: { + token: token + } + }); + } + + return next.handle(request).pipe( + tap(event => { + // Do nothing for the interceptor + }, (err: any) => { + if (err instanceof HttpErrorResponse && err.status === 401) { + this.authService.disconnect(); + this.router.navigate(['/login']); + } + }) + ); + } + + +} diff --git a/src/main/ts/src/app/core/services/auth.service.ts b/src/main/ts/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..016b592 --- /dev/null +++ b/src/main/ts/src/app/core/services/auth.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { User } from '../entities'; + +const PARAM_TOKEN = 'token'; +const PARAM_USER = 'user'; + +@Injectable() +export class AuthService { + + constructor() {} + + public getToken(): string { + return localStorage.getItem(PARAM_TOKEN); + } + + public setToken(token: string): void { + localStorage.setItem(PARAM_TOKEN, token); + } + + public isAuthenticated(): boolean { + return this.getToken() != null; + } + + public disconnect(): void { + localStorage.clear(); + } + + public isAdmin(): boolean { + return false; + } + + public setUser(user: User): void { + localStorage.setItem(PARAM_USER, JSON.stringify(user)); + } + + public getUser(): User { + return JSON.parse(localStorage.getItem(PARAM_USER)); + } +} diff --git a/src/main/ts/src/app/disconnection/disconnection.component.ts b/src/main/ts/src/app/disconnection/disconnection.component.ts new file mode 100644 index 0000000..9082ff0 --- /dev/null +++ b/src/main/ts/src/app/disconnection/disconnection.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthService } from '../core/services/auth.service'; + +@Component({ + selector: 'app-disconnection', + template: 'Déconnexion...' +}) +export class DisconnectionComponent implements OnInit { + + constructor( + private authService: AuthService, + private router: Router + ) {} + + ngOnInit(): void { + this.authService.disconnect(); + this.router.navigate(['/server']); + } +} diff --git a/src/main/ts/src/app/header/header.component.html b/src/main/ts/src/app/header/header.component.html new file mode 100644 index 0000000..dad40af --- /dev/null +++ b/src/main/ts/src/app/header/header.component.html @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/ts/src/app/header/header.component.ts b/src/main/ts/src/app/header/header.component.ts new file mode 100644 index 0000000..ab5dfa0 --- /dev/null +++ b/src/main/ts/src/app/header/header.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { AuthService } from '../core/services/auth.service'; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styles: [` + img { + width: 50px; + height: 50px; + } + `] +}) +export class HeaderComponent { + constructor( + private authService: AuthService + ) {} + + isAuthenticated(): boolean { + return this.authService.isAuthenticated(); + } +} diff --git a/src/main/ts/src/app/login/login.component.html b/src/main/ts/src/app/login/login.component.html new file mode 100644 index 0000000..b8bc0ac --- /dev/null +++ b/src/main/ts/src/app/login/login.component.html @@ -0,0 +1,22 @@ +
+ + +
+
+ + +
+ +
+
+

{{loginError}}

+
+
\ No newline at end of file diff --git a/src/main/ts/src/app/login/login.component.ts b/src/main/ts/src/app/login/login.component.ts new file mode 100644 index 0000000..ef979d6 --- /dev/null +++ b/src/main/ts/src/app/login/login.component.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { User } from '../core/entities'; +import { LoginService } from './login.service'; +import { AuthService } from '../core/services/auth.service'; + +@Component({ + selector: 'app-login', + templateUrl: 'login.component.html', + styles: [` + #form { + padding-bottom: 10px; + } + + .submitFormArea { + line-height: 50px; + } + + #errorMsg { + max-height: 0; + overflow: hidden; + transition: max-height 0.5s ease-out; + margin: 0; + } + `] +}) +export class LoginComponent { + model: User = new User('', '', '', '', undefined, ''); + loginError; + + constructor( + private loginService: LoginService, + private authService: AuthService, + private router: Router + ) {} + + submitLogin(): void { + this.loginService.login(this.model).subscribe(user => { + this.authService.setToken(user.token); + this.authService.setUser(user); + this.router.navigate(['/server']); + }, error => { + this.setMessage('Email ou password incorrect.'); + }); + } + + setMessage(message: string): void { + this.loginError = message; + + const resultMsgDiv = document.getElementById('errorMsg'); + resultMsgDiv.style.maxHeight = '64px'; + + setTimeout(() => { + resultMsgDiv.style.maxHeight = '0px'; + setTimeout(() => { + this.loginError = undefined; + }, 550); + }, 3000); + } +} diff --git a/src/main/ts/src/app/login/login.service.ts b/src/main/ts/src/app/login/login.service.ts new file mode 100644 index 0000000..15384f0 --- /dev/null +++ b/src/main/ts/src/app/login/login.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; + +import { User } from '../core/entities'; + +const LOGIN_URL = environment.apiUrl + '/api/account/login'; + +@Injectable() +export class LoginService { + + constructor( + private http: HttpClient + ) {} + + login(user: User): Observable { + return this.http.post(LOGIN_URL, user); + } +} diff --git a/src/main/ts/src/app/server/server.component.html b/src/main/ts/src/app/server/server.component.html new file mode 100644 index 0000000..e1c3814 --- /dev/null +++ b/src/main/ts/src/app/server/server.component.html @@ -0,0 +1,46 @@ +
+ Status du serveur : + + Vérification + + + {{serverStarted ? 'Démarré' : 'Éteint'}} + +
+
+ + +
+
+
+

{{errorMsg}}

+
+
+
+
+

{{warnMsg}}

+
+
+
+
+

{{successMsg}}

+
+
\ No newline at end of file diff --git a/src/main/ts/src/app/server/server.component.ts b/src/main/ts/src/app/server/server.component.ts new file mode 100644 index 0000000..bca0727 --- /dev/null +++ b/src/main/ts/src/app/server/server.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { ServerService } from './server.service'; +import { AuthService } from '../core/services/auth.service'; + +@Component({ + selector: 'app-server', + templateUrl: 'server.component.html', + styles: [` + .msg { + max-height: 0; + overflow: hidden; + transition: max-height 0.5s ease-out; + margin: 0; + } + `] +}) +export class ServerComponent implements OnInit { + serverStartedChecked = false; + serverStarted = false; + errorMsg; + warnMsg; + successMsg; + restarting = false; + + constructor( + private serverService: ServerService, + private authService: AuthService + ) {} + + ngOnInit(): void { + this.serverService.getStatus().subscribe(pServerStatus => { + this.serverStartedChecked = true; + this.serverStarted = pServerStatus === 1; + }); + } + + isAuthenticated(): boolean { + return this.authService.isAuthenticated(); + } + + restartServer(): void { + this.restarting = true; + + this.serverService.restartServer().subscribe(() => { + this.setMessage('Serveur redémarré.', 'successMsg'); + }, error => { + this.setMessage('Une erreur est survenue lors du redémarrage du serveur.', + 'errorMsg'); + }, () => { + this.restarting = false; + }); + } + + startOrStopServer(): void { + this.serverService[(this.serverStarted ? 'stop' : 'start') + 'Server']().subscribe(() => { + this.setMessage('Serveur ' + (this.serverStarted ? 'éteint' : 'démarré') + '.', 'successMsg'); + this.serverStarted = !this.serverStarted; + }, error => { + if (error.status === 409) { + this.setMessage('Le serveur est déjà ' + + (this.serverStarted ? 'éteint' : 'démarré') + '.', + 'warnMsg'); + } else { + this.setMessage('Une erreur est survenue lors ' + + (this.serverStarted ? 'de l\'extinction' : 'du démarrage') + + 'du serveur.', + 'errorMsg'); + } + }); + } + + setMessage(message: string, type: string): void { + this[type] = message; + + const resultMsgDiv = document.getElementById(type); + resultMsgDiv.style.maxHeight = '64px'; + + setTimeout(() => { + resultMsgDiv.style.maxHeight = '0px'; + setTimeout(() => { + this[type] = undefined; + }, 550); + }, 3000); + } +} diff --git a/src/main/ts/src/app/server/server.service.ts b/src/main/ts/src/app/server/server.service.ts new file mode 100644 index 0000000..f2c448a --- /dev/null +++ b/src/main/ts/src/app/server/server.service.ts @@ -0,0 +1,29 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +const SERVER_URL = environment.apiUrl + '/api/server'; + +@Injectable() +export class ServerService { + constructor( + private http: HttpClient + ) {} + + getStatus(): Observable { + return this.http.get(`${SERVER_URL}/status`); + } + + startServer(): Observable { + return this.http.post(`${SERVER_URL}/start`, undefined); + } + + stopServer(): Observable { + return this.http.post(`${SERVER_URL}/stop`, undefined); + } + + restartServer(): Observable { + return this.http.post(`${SERVER_URL}/restart`, undefined); + } +} diff --git a/src/main/ts/src/assets/favicon.png b/src/main/ts/src/assets/favicon.png new file mode 100644 index 0000000..bebe7cb Binary files /dev/null and b/src/main/ts/src/assets/favicon.png differ diff --git a/src/main/ts/src/browserslist b/src/main/ts/src/browserslist new file mode 100644 index 0000000..37371cb --- /dev/null +++ b/src/main/ts/src/browserslist @@ -0,0 +1,11 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 \ No newline at end of file diff --git a/src/main/ts/src/environments/environment.prod.ts b/src/main/ts/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/src/main/ts/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/src/main/ts/src/environments/environment.ts b/src/main/ts/src/environments/environment.ts new file mode 100644 index 0000000..c8c637e --- /dev/null +++ b/src/main/ts/src/environments/environment.ts @@ -0,0 +1,19 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, + apiUrl: 'http://localhost:8080', + appVersion: '0.0.1', + title: 'LOCAL' +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/src/main/ts/src/favicon.ico b/src/main/ts/src/favicon.ico new file mode 100644 index 0000000..8081c7c Binary files /dev/null and b/src/main/ts/src/favicon.ico differ diff --git a/src/main/ts/src/favicon.png b/src/main/ts/src/favicon.png new file mode 100644 index 0000000..bebe7cb Binary files /dev/null and b/src/main/ts/src/favicon.png differ diff --git a/src/main/ts/src/index.html b/src/main/ts/src/index.html new file mode 100644 index 0000000..c843652 --- /dev/null +++ b/src/main/ts/src/index.html @@ -0,0 +1,14 @@ + + + + Minager + + + + + + + + + + diff --git a/src/main/ts/src/karma.conf.js b/src/main/ts/src/karma.conf.js new file mode 100644 index 0000000..b6e0042 --- /dev/null +++ b/src/main/ts/src/karma.conf.js @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; \ No newline at end of file diff --git a/src/main/ts/src/main.ts b/src/main/ts/src/main.ts new file mode 100644 index 0000000..28bfa9e --- /dev/null +++ b/src/main/ts/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); + diff --git a/src/main/ts/src/polyfills.ts b/src/main/ts/src/polyfills.ts new file mode 100644 index 0000000..d310405 --- /dev/null +++ b/src/main/ts/src/polyfills.ts @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/src/main/ts/src/styles.scss b/src/main/ts/src/styles.scss new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/src/main/ts/src/styles.scss @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/src/main/ts/src/test.ts b/src/main/ts/src/test.ts new file mode 100644 index 0000000..1631789 --- /dev/null +++ b/src/main/ts/src/test.ts @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/src/main/ts/src/tsconfig.app.json b/src/main/ts/src/tsconfig.app.json new file mode 100644 index 0000000..190fd30 --- /dev/null +++ b/src/main/ts/src/tsconfig.app.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/src/main/ts/src/tsconfig.spec.json b/src/main/ts/src/tsconfig.spec.json new file mode 100644 index 0000000..de77336 --- /dev/null +++ b/src/main/ts/src/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/src/main/ts/src/tslint.json b/src/main/ts/src/tslint.json new file mode 100644 index 0000000..52e2c1a --- /dev/null +++ b/src/main/ts/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git a/src/main/ts/tsconfig.json b/src/main/ts/tsconfig.json new file mode 100644 index 0000000..916247e --- /dev/null +++ b/src/main/ts/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "module": "es2015", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git a/src/main/ts/tslint.json b/src/main/ts/tslint.json new file mode 100644 index 0000000..6ddb6b2 --- /dev/null +++ b/src/main/ts/tslint.json @@ -0,0 +1,131 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git a/src/test/java/org/minager/MinagerApplicationTests.java b/src/test/java/org/minager/MinagerApplicationTests.java new file mode 100644 index 0000000..cbc4d87 --- /dev/null +++ b/src/test/java/org/minager/MinagerApplicationTests.java @@ -0,0 +1,16 @@ +package org.minager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class MinagerApplicationTests { + + @Test + public void contextLoads() { + } + +}