First commit

This commit is contained in:
2018-09-29 18:33:56 +02:00
commit ba2c76ff06
73 changed files with 3243 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@@ -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

84
pom.xml Normal file
View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.minager</groupId>
<artifactId>minager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Minager</name>
<description>Minecraft server managing application</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- <dependency> -->
<!-- <groupId>commons-lang</groupId> -->
<!-- <artifactId>commons-lang</artifactId> -->
<!-- <version>2.3</version> -->
<!-- </dependency> -->
<!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
<!-- <dependency> -->
<!-- <groupId>org.apache.commons</groupId> -->
<!-- <artifactId>commons-lang3</artifactId> -->
<!-- <version>3.7</version> -->
<!-- </dependency> -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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

80
src/main/bash/minager.sh Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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<User> 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);
}
}

View File

@@ -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> 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<User> 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<User> connectedUserOpt = tokenService.getAuthenticatedUserByToken(pRequest);
if(connectedUserOpt.isPresent()) {
final User connectedUser = connectedUserOpt.get();
final Optional<User> 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);
}
}
}

View File

@@ -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.<br/>
* <br/>
* 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<Route> 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
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.<br/>
* A token is valid is its {@link Token#lastAccess} is after the current
* date minus the {@link Token#DELAY} {@link Token#METRIC}.<br/>
* <br/>
* Example:<br/>
* {@link Token#DELAY} = 30 and {@link Token#METRIC} =
* {@link Calendar#MINUTE}.<br/>
* A token is valid only on the 30 minutes after its
* {@link Token#lastAccess}.<br/>
* 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());
}
}

View File

@@ -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<User, Long> {
Optional<User> 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);
}

View File

@@ -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<Route> 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);
}
}
}

View File

@@ -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
}
}

View File

@@ -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<List<HttpMethod>> 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<List<HttpMethod>> getMethod() {
return method;
}
public void setMethod(HttpMethod pMethods) {
this.method = Optional.of(Arrays.asList(pMethods));
}
}

View File

@@ -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<String, User> 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<User> 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<User> getAuthenticatedUserByToken(final HttpServletRequest pRequest) {
return Optional.ofNullable(connectedUsers.get(pRequest.getHeader(HEADER_TOKEN)));
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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;

13
src/main/ts/.editorconfig Normal file
View File

@@ -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

27
src/main/ts/README.md Normal file
View File

@@ -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).

137
src/main/ts/angular.json Normal file
View File

@@ -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"
}

View File

@@ -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 } }));
}
};

View File

@@ -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!');
});
});

View File

@@ -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();
}
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

55
src/main/ts/package.json Normal file
View File

@@ -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"
}
}

View File

@@ -0,0 +1,6 @@
<app-header></app-header>
<main class="container">
<router-outlet></router-outlet>
</main>
<!-- <app-footer></app-footer> -->

View File

@@ -0,0 +1,4 @@
main {
margin-top: 84px;
padding-top: 25px;
}

View File

@@ -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!');
}));
});

View File

@@ -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';
}

View File

@@ -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 { }

View File

@@ -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' }
];

View File

@@ -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
) { }
}

View File

@@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
let request: HttpRequest<any> = 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']);
}
})
);
}
}

View File

@@ -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));
}
}

View File

@@ -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']);
}
}

View File

@@ -0,0 +1,27 @@
<mdb-navbar SideClass="navbar fixed-top navbar-expand-lg navbar-dark green scrolling-navbar ie-nav" [containerInside]="false">
<logo>
<a class="logo navbar-brand" routerLink="/">
<img id="logo" src="./assets/favicon.png" alt="logo" />
<span id="title" *ngIf="!title">Minager</span>
<span id="title" *ngIf="title">Minager - {{title}}</span>
</a>
</logo>
<links>
<ul class="navbar-nav ml-auto nav-flex-icons" style="margin-left: 0 !important;">
<li class="nav-item">
<a routerLink="/login"
*ngIf="!isAuthenticated()"
class="nav-link waves-light"
mdbRippleRadius>
<i class="fa fa-sign-in"></i> Connexion
</a>
<a routerLink="/disconnection"
*ngIf="isAuthenticated()"
class="nav-link waves-light danger-color-dark"
mdbRippleRadius >
<i class="fa fa-sign-out"></i> Déconnexion
</a>
</li>
</ul>
</links>
</mdb-navbar>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,22 @@
<div>
<label for="email">Adresse mail</label>
<input
id="email"
name="email"
type="email"
[(ngModel)]="model.email" />
</div>
<div>
<label for="password">Mot de passe</label>
<input
id="password"
name="password"
type="password"
[(ngModel)]="model.password" />
</div>
<button (click)="submitLogin()">Suivant</button>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{loginError}}</p>
</div>
</div>

View File

@@ -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);
}
}

View File

@@ -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<User> {
return this.http.post<User>(LOGIN_URL, user);
}
}

View File

@@ -0,0 +1,46 @@
<div>
Status du serveur :
<span *ngIf="!serverStartedChecked" class="badge badge-warning">
Vérification
</span>
<span *ngIf="serverStartedChecked" class="badge"
[ngClass]="serverStarted ? 'badge-success' : 'badge-danger'">
{{serverStarted ? 'Démarré' : 'Éteint'}}
</span>
</div>
<div *ngIf="isAuthenticated()">
<button mdbBtn
*ngIf="serverStarted"
type="button"
color="primary"
class="waves-light"
[disabled]="restarting"
(click)="restartServer()"
mdbWavesEffect>
Redémarrer
</button>
<button mdbBtn
type="button"
class="waves-light"
[ngClass]="serverStarted ? 'btn-danger' : 'btn-success'"
[disabled]="restarting"
(click)="startOrStopServer()"
mdbWavesEffect>
{{serverStarted ? 'Éteindre' : 'Démarrer'}}
</button>
</div>
<div id="errorMsg" class="msg card red lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{errorMsg}}</p>
</div>
</div>
<div id="warnMsg" class="msg card orange lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{warnMsg}}</p>
</div>
</div>
<div id="successMsg" class="msg card green lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{successMsg}}</p>
</div>
</div>

View File

@@ -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);
}
}

View File

@@ -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<number> {
return this.http.get<number>(`${SERVER_URL}/status`);
}
startServer(): Observable<any> {
return this.http.post(`${SERVER_URL}/start`, undefined);
}
stopServer(): Observable<any> {
return this.http.post(`${SERVER_URL}/stop`, undefined);
}
restartServer(): Observable<any> {
return this.http.post(`${SERVER_URL}/restart`, undefined);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -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

View File

@@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@@ -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.

BIN
src/main/ts/src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
src/main/ts/src/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="fr">
<head>
<title>Minager</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<base href="/" />
<link rel="icon" type="image/x-icon" href="favicon.png">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -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
});
};

13
src/main/ts/src/main.ts Normal file
View File

@@ -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));

View File

@@ -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
*/

View File

@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

20
src/main/ts/src/test.ts Normal file
View File

@@ -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);

View File

@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

View File

@@ -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"
]
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}

21
src/main/ts/tsconfig.json Normal file
View File

@@ -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"
]
}
}

131
src/main/ts/tslint.json Normal file
View File

@@ -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
}
}

View File

@@ -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() {
}
}