Compare commits

...

10 Commits

109 changed files with 15494 additions and 71 deletions

View File

@@ -1,37 +1,95 @@
package org.cerberus.controllers; package org.cerberus.controllers;
import com.fasterxml.jackson.annotation.JsonView;
import org.cerberus.core.exceptions.UnauthorizedException;
import org.cerberus.entities.dto.View;
import org.cerberus.entities.persistence.Application; import org.cerberus.entities.persistence.Application;
import org.cerberus.entities.persistence.User; import org.cerberus.entities.persistence.User;
import org.cerberus.services.ApplicationService; import org.cerberus.services.ApplicationService;
import org.cerberus.services.SecurityService; import org.cerberus.services.SecurityService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.security.Principal; import java.security.Principal;
import java.util.List;
import java.util.UUID;
import static org.cerberus.core.constant.RoleSecurity.ADMIN; import static org.cerberus.core.constant.RoleSecurity.ADMIN;
import static org.cerberus.core.constant.RoleSecurity.MAINTAINER; import static org.cerberus.core.constant.RoleSecurity.MAINTAINER;
import static org.cerberus.services.DaemonHandlingService.Action.*;
@RestController @RestController
@RequestMapping("/api/applications") @RequestMapping("/api/applications")
public class ApplicationController { public class ApplicationController {
private ApplicationService applicationService; private ApplicationService service;
private SecurityService securityService; private SecurityService securityService;
public ApplicationController(ApplicationService applicationService, ApplicationController(ApplicationService service,
SecurityService securityService) { SecurityService securityService) {
this.applicationService = applicationService; this.service = service;
this.securityService = securityService; this.securityService = securityService;
} }
@GetMapping("/{id}")
@JsonView({View.ApplicationDTO.class})
public Application findById(@PathVariable("id") UUID id) {
return service.findByIdOrElseThrow(id);
}
@GetMapping
@JsonView({View.ApplicationDTO.class})
public List<Application> findAll(Principal connectedUser) {
User user = securityService.getUserByPrincipal(connectedUser)
.orElseThrow(() -> new UnauthorizedException(""));
return service.findAll(user);
}
@PostMapping @PostMapping
@JsonView({View.ApplicationDTO.class})
public Application create(@RequestBody Application application, Principal connectedUser) { public Application create(@RequestBody Application application, Principal connectedUser) {
User user = securityService.getAdminUser(connectedUser); User user = securityService.getAdminUser(connectedUser);
return applicationService.create(application, user); return service.create(application, user);
} }
@PutMapping @PutMapping
@JsonView({View.ApplicationDTO.class})
public Application update(@RequestBody Application application, Principal connectedUser) { public Application update(@RequestBody Application application, Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, application, ADMIN, MAINTAINER); securityService.checkHasAnyRole(connectedUser, application, ADMIN, MAINTAINER);
return applicationService.update(application); return service.update(application);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable("id") UUID id, Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, id, ADMIN, MAINTAINER);
service.delete(id);
}
@PostMapping("/{id}/start")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void start(@PathVariable("id") UUID id, Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, id, ADMIN, MAINTAINER);
service.doServiceAction(id, START);
}
@PostMapping("/{id}/stop")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void stop(@PathVariable("id") UUID id, Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, id, ADMIN, MAINTAINER);
service.doServiceAction(id, STOP);
}
@PostMapping("/{id}/restart")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void restart(@PathVariable("id") UUID id, Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, id, ADMIN, MAINTAINER);
service.doServiceAction(id, RESTART);
}
@GetMapping("/{id}/status")
public int status(@PathVariable("id") UUID id, Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, id, ADMIN, MAINTAINER);
return service.getStatus(id);
} }
} }

View File

@@ -1,11 +1,15 @@
package org.cerberus.controllers; package org.cerberus.controllers;
import com.fasterxml.jackson.annotation.JsonView;
import org.cerberus.entities.dto.View;
import org.cerberus.entities.persistence.ConfigurationFile; import org.cerberus.entities.persistence.ConfigurationFile;
import org.cerberus.services.ConfigurationFileService; import org.cerberus.services.ConfigurationFileService;
import org.cerberus.services.SecurityService; import org.cerberus.services.SecurityService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.security.Principal; import java.security.Principal;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.cerberus.core.constant.RoleSecurity.ADMIN; import static org.cerberus.core.constant.RoleSecurity.ADMIN;
@@ -14,20 +18,60 @@ import static org.cerberus.core.constant.RoleSecurity.MAINTAINER;
@RestController @RestController
@RequestMapping("/api/applications/{applicationId}/configurationFile") @RequestMapping("/api/applications/{applicationId}/configurationFile")
public class ConfigurationFileController { public class ConfigurationFileController {
private ConfigurationFileService configurationFileService; private ConfigurationFileService service;
private SecurityService securityService; private SecurityService securityService;
ConfigurationFileController(ConfigurationFileService configurationFileService, ConfigurationFileController(ConfigurationFileService service,
SecurityService securityService) { SecurityService securityService) {
this.configurationFileService = configurationFileService; this.service = service;
this.securityService = securityService; this.securityService = securityService;
} }
@GetMapping("/{id}")
@JsonView({View.ConfigurationFileDTO.class})
public ConfigurationFile findById(@PathVariable("applicationId") UUID applicationId,
@PathVariable("id") UUID configurationFileId,
Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, applicationId, ADMIN, MAINTAINER);
return service.findByApplicationIdAndId(applicationId, configurationFileId);
}
/**
* Returns all the configuration files associates to the application that id is given in parameters.
* @return The configuration files but without their content.
*/
@GetMapping
@JsonView({View.ConfigurationFileDTO.class})
public List<ConfigurationFile> getAllByApplication(@PathVariable("applicationId") UUID applicationId,
Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, applicationId, ADMIN, MAINTAINER);
return service.findAllByApplicationId(applicationId);
}
@PostMapping @PostMapping
public void create(@PathVariable("applicationId") UUID applicationId, @JsonView({View.ConfigurationFileDTO.class})
public ConfigurationFile create(@PathVariable("applicationId") UUID applicationId,
@RequestBody ConfigurationFile configurationFile, @RequestBody ConfigurationFile configurationFile,
Principal connectedUser) { Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, applicationId, ADMIN, MAINTAINER); securityService.checkHasAnyRole(connectedUser, applicationId, ADMIN, MAINTAINER);
configurationFileService.create(applicationId, configurationFile); return service.create(applicationId, configurationFile);
}
@PutMapping
@JsonView({View.ConfigurationFileDTO.class})
public ConfigurationFile update(@PathVariable("applicationId") UUID applicationId,
@RequestBody ConfigurationFile configurationFile,
Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, applicationId, ADMIN, MAINTAINER);
return service.update(applicationId, configurationFile);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable("applicationId") UUID applicationId,
@PathVariable("id") UUID configurationFileId,
Principal connectedUser) {
securityService.checkHasAnyRole(connectedUser, applicationId, ADMIN, MAINTAINER);
service.delete(applicationId, configurationFileId);
} }
} }

View File

@@ -0,0 +1,18 @@
package org.cerberus.controllers;
import org.cerberus.core.constant.Role;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/api/refData")
public class RefDataController {
@GetMapping("/roles")
public List<Role> getRoles() {
return Arrays.asList(Role.values());
}
}

View File

@@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
@RestController @RestController
@SuppressWarnings("unused")
public class RobotsTxtController { public class RobotsTxtController {
private static final Logger LOG = LoggerFactory.getLogger(RobotsTxtController.class); private static final Logger LOG = LoggerFactory.getLogger(RobotsTxtController.class);

View File

@@ -1,6 +1,8 @@
package org.cerberus.controllers; package org.cerberus.controllers;
import com.fasterxml.jackson.annotation.JsonView;
import org.cerberus.entities.dto.SignUpDTO; import org.cerberus.entities.dto.SignUpDTO;
import org.cerberus.entities.dto.View;
import org.cerberus.entities.persistence.User; import org.cerberus.entities.persistence.User;
import org.cerberus.services.UserService; import org.cerberus.services.UserService;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -15,17 +17,18 @@ import static org.springframework.http.HttpStatus.NO_CONTENT;
@RestController @RestController
@RequestMapping("/api/users") @RequestMapping("/api/users")
@SuppressWarnings("unused")
public class UserController { public class UserController {
private UserService userService; private UserService service;
public UserController(UserService userService) { public UserController(UserService service) {
this.userService = userService; this.service = service;
} }
@PostMapping("/login") @PostMapping("/login")
@ResponseStatus(NO_CONTENT) @JsonView({View.UserDTO.class})
public void login(@RequestBody User user) { public User login(@RequestBody User user) {
userService.authenticate(user); return service.authenticate(user);
} }
@GetMapping("/disconnection") @GetMapping("/disconnection")
@@ -40,6 +43,6 @@ public class UserController {
@PostMapping("/signup") @PostMapping("/signup")
@ResponseStatus(NO_CONTENT) @ResponseStatus(NO_CONTENT)
public void signUp(@RequestBody SignUpDTO inputData) { public void signUp(@RequestBody SignUpDTO inputData) {
userService.signUp(inputData); service.signUp(inputData);
} }
} }

View File

@@ -0,0 +1,18 @@
package org.cerberus.core.constant;
public enum ResultCode {
SUCCESS(0),
FAILED(1),
STATE_UNCHANGED(2),
ILLEGAL_ARGUMENT(3);
private int val;
ResultCode(final int pVal) {
val = pVal;
}
public final int val() {
return val;
}
}

View File

@@ -2,5 +2,5 @@ package org.cerberus.core.constant;
public enum Role { public enum Role {
VIEWER, VIEWER,
MAINTAINER; MAINTAINER
} }

View File

@@ -11,4 +11,9 @@ abstract class BusinessException extends RuntimeException {
BusinessException(String message, Throwable cause) { BusinessException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
@Override
public StackTraceElement[] getStackTrace() {
return null;
}
} }

View File

@@ -0,0 +1,15 @@
package org.cerberus.core.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(message);
}
public NotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,15 @@
package org.cerberus.core.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.SERVICE_UNAVAILABLE)
public class ServiceUnavailableException extends BusinessException {
public ServiceUnavailableException(String message) {
super(message);
}
public ServiceUnavailableException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,15 @@
package org.cerberus.core.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class UnauthorizedException extends BusinessException {
public UnauthorizedException(String message) {
super(message);
}
public UnauthorizedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,99 @@
package org.cerberus.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,8 @@
package org.cerberus.entities.dto;
public final class View {
private View() {}
public interface ApplicationDTO {}
public interface ConfigurationFileDTO {}
public interface UserDTO {}
}

View File

@@ -1,5 +1,7 @@
package org.cerberus.entities.persistence; package org.cerberus.entities.persistence;
import com.fasterxml.jackson.annotation.JsonView;
import org.cerberus.entities.dto.View;
import org.hibernate.annotations.Proxy; import org.hibernate.annotations.Proxy;
import javax.persistence.*; import javax.persistence.*;
@@ -14,12 +16,15 @@ import static javax.persistence.CascadeType.REMOVE;
@Proxy(lazy = false) @Proxy(lazy = false)
public class Application { public class Application {
@Id @Id
@JsonView({View.ApplicationDTO.class, View.ConfigurationFileDTO.class})
private UUID id; private UUID id;
@Column(nullable = false) @Column(nullable = false, unique = true)
@JsonView({View.ApplicationDTO.class})
private String name; private String name;
@Column(nullable = false) @Column(nullable = false, unique = true)
@JsonView({View.ApplicationDTO.class})
private String serviceName; private String serviceName;
@OneToMany(mappedBy = "application", cascade = { REMOVE }) @OneToMany(mappedBy = "application", cascade = { REMOVE })

View File

@@ -1,5 +1,7 @@
package org.cerberus.entities.persistence; package org.cerberus.entities.persistence;
import com.fasterxml.jackson.annotation.JsonView;
import org.cerberus.entities.dto.View;
import org.hibernate.annotations.Proxy; import org.hibernate.annotations.Proxy;
import javax.persistence.*; import javax.persistence.*;
@@ -10,13 +12,19 @@ import java.util.UUID;
@Proxy(lazy = false) @Proxy(lazy = false)
public class ConfigurationFile { public class ConfigurationFile {
@Id @Id
@JsonView({View.ConfigurationFileDTO.class})
private UUID id; private UUID id;
@Column(nullable = false) @Column(nullable = false)
@JsonView({View.ConfigurationFileDTO.class})
private String path; private String path;
@JsonView({View.ConfigurationFileDTO.class})
private transient String content;
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "application_id") @JoinColumn(name = "application_id")
@JsonView({ConfigurationFile.class})
private Application application; private Application application;
@PrePersist @PrePersist
@@ -40,6 +48,14 @@ public class ConfigurationFile {
this.path = path; this.path = path;
} }
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Application getApplication() { public Application getApplication() {
return application; return application;
} }

View File

@@ -1,5 +1,7 @@
package org.cerberus.entities.persistence; package org.cerberus.entities.persistence;
import com.fasterxml.jackson.annotation.JsonView;
import org.cerberus.entities.dto.View;
import org.hibernate.annotations.Generated; import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime; import org.hibernate.annotations.GenerationTime;
import org.hibernate.annotations.Proxy; import org.hibernate.annotations.Proxy;
@@ -14,22 +16,25 @@ import java.util.UUID;
@Proxy(lazy = false) @Proxy(lazy = false)
public class User { public class User {
@Id @Id
@JsonView({View.UserDTO.class})
private UUID id; private UUID id;
@Column(nullable = false) @Column(nullable = false)
@JsonView({View.UserDTO.class})
private String name; private String name;
@Column(nullable = false) @Column(nullable = false)
@JsonView({View.UserDTO.class})
private String email; private String email;
@Column(nullable = false) @Column(nullable = false)
private String password; private String password;
@Column(nullable = false) @Column(nullable = false)
@JsonView({View.UserDTO.class})
private Boolean isAdmin = false; private Boolean isAdmin = false;
@Column(nullable = false) @Column(nullable = false)
@Generated(GenerationTime.ALWAYS)
private LocalDate inscriptionDate; private LocalDate inscriptionDate;
@OneToMany(mappedBy = "user") @OneToMany(mappedBy = "user")

View File

@@ -4,10 +4,19 @@ import org.cerberus.entities.persistence.Application;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Repository
public interface ApplicationRepository extends JpaRepository<Application, UUID> { public interface ApplicationRepository extends JpaRepository<Application, UUID> {
@Query(value = "SELECT EXISTS(SELECT id FROM application WHERE name = :name)", nativeQuery = true) @Query(value = "SELECT EXISTS(SELECT id FROM application WHERE name = :name)", nativeQuery = true)
Boolean alreadyExists(@Param("name") String name); boolean existsByName(@Param("name") String name);
@Query(value = "SELECT EXISTS(SELECT id FROM application WHERE service_name = :serviceName)", nativeQuery = true)
boolean existsByServiceName(@Param("serviceName") String serviceName);
@Query("SELECT a FROM Application a JOIN a.administratorList adm WHERE adm.user.id = :userId")
List<Application> getByUserId(@Param("userId") UUID userId);
} }

View File

@@ -2,11 +2,24 @@ package org.cerberus.repositories;
import org.cerberus.entities.persistence.ConfigurationFile; import org.cerberus.entities.persistence.ConfigurationFile;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
public interface ConfigurationFileRepository extends JpaRepository<ConfigurationFile, UUID> { public interface ConfigurationFileRepository extends JpaRepository<ConfigurationFile, UUID> {
@Query(value = "SELECT EXISTS(SELECT id FROM configuration_file WHERE path = :path AND application_id = :applicationId)", nativeQuery = true)
boolean existsByPathAndApplicationId(@Param("path") String path,
@Param("applicationId") UUID applicationId);
@Query(value = "SELECT EXISTS(SELECT id FROM configuration_file WHERE id = :configurationFileId " +
"AND application_id = :applicationId)", nativeQuery = true)
boolean doesBelongToApplication(@Param("configurationFileId") UUID configurationFileId,
@Param("applicationId") UUID applicationId);
@Query("SELECT cf FROM ConfigurationFile cf WHERE cf.application.id = :applicationId")
List<ConfigurationFile> findAllByApplicationId(@Param("applicationId") UUID applicationId);
} }

View File

@@ -5,11 +5,13 @@ import org.cerberus.entities.persistence.User;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Repository
public interface UserRepository extends JpaRepository<User, UUID> { public interface UserRepository extends JpaRepository<User, UUID> {
@Query("SELECT u FROM User u WHERE u.email = :email") @Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email); Optional<User> findByEmail(@Param("email") String email);

View File

@@ -0,0 +1,48 @@
package org.cerberus.services;
import org.cerberus.core.exceptions.NotFoundException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.NonNull;
import java.util.Optional;
import java.util.UUID;
abstract class AbstractService<E> {
private JpaRepository<E, UUID> repository;
AbstractService(JpaRepository<E, UUID> repository) {
this.repository = repository;
}
public Optional<E> findById(UUID id) {
return repository.findById(id);
}
/**
* Find the entity by its ID or throw a {@link NotFoundException} if no entity matches to the ID.
* @param id Entity ID.
* @return The entity.
*/
public @NonNull E findByIdOrElseThrow(UUID id) {
return findById(id).orElseThrow(this::newNotFoundException);
}
public void delete(UUID id) {
if(!repository.existsById(id)) {
throwNotFoundException();
}
repository.deleteById(id);
}
public boolean existsById(UUID id) {
return repository.existsById(id);
}
protected void throwNotFoundException() {
throw newNotFoundException();
}
protected NotFoundException newNotFoundException() {
return new NotFoundException("Entity doesn't exists.");
}
}

View File

@@ -8,36 +8,44 @@ import org.cerberus.validators.ApplicationValidator;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Optional; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.cerberus.core.constant.Role.MAINTAINER; import static org.cerberus.core.constant.Role.MAINTAINER;
import static org.cerberus.core.utils.StringUtils.concat; import static org.cerberus.core.utils.StringUtils.concat;
@Service @Service
public class ApplicationService { public class ApplicationService extends AbstractService<Application> {
private final ApplicationRepository repository;
private final ApplicationRoleService applicationRoleService;
private final ApplicationValidator validator;
private final DaemonHandlingService daemonHandlingService;
private ApplicationRepository applicationRepository; ApplicationService(ApplicationRepository repository,
private ApplicationRoleService applicationRoleService;
private ApplicationValidator applicationValidator;
public ApplicationService(ApplicationRepository applicationRepository,
ApplicationRoleService applicationRoleService, ApplicationRoleService applicationRoleService,
ApplicationValidator applicationValidator) { ApplicationValidator validator,
this.applicationRepository = applicationRepository; DaemonHandlingService daemonHandlingService) {
super(repository);
this.repository = repository;
this.applicationRoleService = applicationRoleService; this.applicationRoleService = applicationRoleService;
this.applicationValidator = applicationValidator; this.validator = validator;
this.daemonHandlingService = daemonHandlingService;
} }
@Transactional @Transactional
public Application create(Application application, User user) { public Application create(Application application, User user) {
applicationValidator.validate(application); validator.validate(application);
validator.sanitize(application);
if(applicationRepository.alreadyExists(application.getName())) { if(repository.existsByName(application.getName())) {
throw new BadRequestException(concat("The application ", application.getName(), " already exists.")); throw new BadRequestException(concat("The application ", application.getName(), " already exists."));
} }
if(repository.existsByServiceName(application.getServiceName())) {
throw new BadRequestException(concat("The service name ", application.getServiceName(),
" already exists for another application."));
}
applicationRepository.save(application); repository.save(application);
// Application creator is by default a maintainer // Application creator is by default a maintainer
applicationRoleService.create(application, user, MAINTAINER); applicationRoleService.create(application, user, MAINTAINER);
@@ -45,13 +53,41 @@ public class ApplicationService {
} }
public Application update(Application application) { public Application update(Application application) {
applicationValidator.validate(application); validator.validate(application);
applicationValidator.sanitize(application); validator.sanitize(application);
applicationRepository.save(application);
return application; Application appFromDb = findByIdOrElseThrow(application.getId());
// If the app name changed
if(!appFromDb.getName().equals(application.getName()) && repository.existsByName(application.getName())) {
throw new BadRequestException(concat("The application ", application.getName(), " already exists."));
}
// If the app service name changed
if(!appFromDb.getServiceName().equals(application.getServiceName())
&& repository.existsByServiceName(application.getServiceName())) {
throw new BadRequestException(concat("The service name ", application.getServiceName(),
" already exists for another application."));
} }
public Optional<Application> findById(UUID id) { return repository.save(application);
return applicationRepository.findById(id); }
public void doServiceAction(UUID applicationId, DaemonHandlingService.Action action) {
daemonHandlingService.doServiceAction(findByIdOrElseThrow(applicationId), action);
}
public int getStatus(UUID applicationId) {
return daemonHandlingService.getStatus(findByIdOrElseThrow(applicationId));
}
public List<Application> findAll(User user) {
List<Application> result;
if(user.isAdmin()) {
result = repository.findAll();
} else {
result = repository.getByUserId(user.getId());
}
return result;
} }
} }

View File

@@ -1,37 +1,94 @@
package org.cerberus.services; package org.cerberus.services;
import org.cerberus.core.exceptions.BadRequestException; import org.cerberus.core.exceptions.BadRequestException;
import org.cerberus.core.exceptions.InternalServerErrorException;
import org.cerberus.core.utils.StringUtils; import org.cerberus.core.utils.StringUtils;
import org.cerberus.entities.persistence.ConfigurationFile; import org.cerberus.entities.persistence.ConfigurationFile;
import org.cerberus.repositories.ConfigurationFileRepository; import org.cerberus.repositories.ConfigurationFileRepository;
import org.cerberus.validators.ConfigurationFileValidator; import org.cerberus.validators.ConfigurationFileValidator;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.cerberus.core.utils.StringUtils.concat;
@Service @Service
public class ConfigurationFileService { public class ConfigurationFileService extends AbstractService<ConfigurationFile> {
private ApplicationService applicationService; private ApplicationService applicationService;
private ConfigurationFileRepository configurationFileRepository; private ConfigurationFileRepository repository;
private ConfigurationFileValidator configurationFileValidator; private ConfigurationFileValidator validator;
ConfigurationFileService(ApplicationService applicationService, ConfigurationFileService(ApplicationService applicationService,
ConfigurationFileRepository configurationFileRepository, ConfigurationFileRepository repository,
ConfigurationFileValidator configurationFileValidator) { ConfigurationFileValidator validator) {
super(repository);
this.applicationService = applicationService; this.applicationService = applicationService;
this.configurationFileRepository = configurationFileRepository; this.repository = repository;
this.configurationFileValidator = configurationFileValidator; this.validator = validator;
} }
public void create(UUID applicationId, ConfigurationFile configurationFile) { public ConfigurationFile findByApplicationIdAndId(UUID applicationId, UUID configurationFileId) {
if(applicationId == null || StringUtils.isNull(applicationId.toString())) { if(!applicationService.existsById(applicationId)) {
throw new BadRequestException("Application id is required."); throwNotFoundException();
} }
configurationFileValidator.validate(configurationFile);
configurationFile.setApplication(applicationService.findById(applicationId) ConfigurationFile configurationFile = findByIdOrElseThrow(configurationFileId);
.orElseThrow(() -> new BadRequestException("The application doesn't exist."))
); try {
configurationFileRepository.save(configurationFile); configurationFile.setContent(Files.readString(Paths.get(configurationFile.getPath())));
} catch(IOException ex) {
throw new InternalServerErrorException(concat("Error during file reading for ", configurationFile.getId()));
}
return configurationFile;
}
/**
* Returns all the configuration files associates to the application that id is given in parameters.
* @return The configuration files but without their content.
*/
public List<ConfigurationFile> findAllByApplicationId(UUID applicationId) {
if(!applicationService.existsById(applicationId)) {
throwNotFoundException();
}
return repository.findAllByApplicationId(applicationId);
}
public ConfigurationFile create(UUID applicationId, ConfigurationFile configurationFile) {
return save(applicationId, configurationFile, false);
}
public ConfigurationFile update(UUID applicationId, ConfigurationFile configurationFile) {
return save(applicationId, configurationFile, true);
}
private ConfigurationFile save(UUID applicationId, ConfigurationFile configurationFile, boolean isUpdate) {
if(applicationId == null || StringUtils.isNull(applicationId.toString()) ||
(isUpdate && !repository.doesBelongToApplication(configurationFile.getId(), applicationId))) {
throwNotFoundException();
}
validator.validate(configurationFile);
if(repository.existsByPathAndApplicationId(configurationFile.getPath(), applicationId)) {
throw new BadRequestException("Configuration file already exists.");
}
configurationFile.setApplication(applicationService.findByIdOrElseThrow(applicationId));
return repository.save(configurationFile);
}
public void delete(UUID applicationId, UUID configurationFileId) {
if(applicationId == null || StringUtils.isNull(applicationId.toString())
|| configurationFileId == null || StringUtils.isNull(configurationFileId.toString())
|| !repository.doesBelongToApplication(configurationFileId, applicationId)) {
throwNotFoundException();
}
repository.deleteById(configurationFileId);
} }
} }

View File

@@ -0,0 +1,47 @@
package org.cerberus.services;
import org.cerberus.core.exceptions.BadRequestException;
import org.cerberus.core.exceptions.ServiceUnavailableException;
import org.cerberus.entities.business.SystemResult;
import org.cerberus.entities.persistence.Application;
import org.springframework.stereotype.Service;
import static org.cerberus.core.constant.ResultCode.SUCCESS;
import static org.cerberus.core.utils.StringUtils.concat;
@Service
public class DaemonHandlingService {
public enum Action {
START,
STOP,
RESTART
}
private SystemService systemService;
DaemonHandlingService(SystemService systemService) {
this.systemService = systemService;
}
public int getStatus(Application application) {
return systemService.executeCommand("sudo service", application.getServiceName(), "status")
.getResultCode();
}
public void doServiceAction(Application application, Action action) {
int applicationStatus = getStatus(application);
if((Action.START == action && applicationStatus == 0)
|| (Action.STOP == action && applicationStatus != 0)) {
throw new BadRequestException(concat("Service is already ", action.name().toLowerCase(), "ed."));
}
SystemResult commandResult = systemService.executeCommand("sudo service",
application.getServiceName(),
action.name().toLowerCase());
if(commandResult.getResultCode() != SUCCESS.val()) {
throw new ServiceUnavailableException(commandResult.getStdErr());
}
}
}

View File

@@ -0,0 +1,74 @@
package org.cerberus.services;
import org.cerberus.entities.business.SystemResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import static org.cerberus.core.utils.StringUtils.concat;
/**
* Business service to execute unix commands.
*/
@Service
public class SystemService {
private static final Logger LOG = LoggerFactory.getLogger(SystemService.class);
public SystemResult executeCommand(final String pCommand, final String... arguments) {
final String commandWithArgs = buildCommand(pCommand, arguments);
final SystemResult commandResults = new SystemResult();
try {
// Process creation and execution of the command.
final Process process = Runtime.getRuntime().exec(commandWithArgs);
// 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;
while((tempLine = outputReader.readLine()) != null) {
commandResults.addOutputLine(tempLine);
}
while((tempLine = errorReader.readLine()) != null) {
commandResults.addErrorLine(tempLine);
}
commandResults.setResultCode(process.exitValue());
} catch(IOException | InterruptedException ex) {
LOG.error(concat("Error during command execution of: \"", commandWithArgs, "\"."), ex);
}
return commandResults;
}
/**
* Build the command in form of one string from the parameters.
*
* @param command
* The command to execute.
* @param arguments
* The command arguments, could be {@code null}.
* @return The command built.
*/
private String buildCommand(final String command, final Object... arguments) {
final StringBuilder commandBuilder = new StringBuilder(command);
if (arguments != null) {
for (final Object arg : arguments) {
commandBuilder.append(" ").append(arg);
}
}
return commandBuilder.toString();
}
}

View File

@@ -25,19 +25,19 @@ public class UserService {
private CustomAuthenticationProvider authenticationProvider; private CustomAuthenticationProvider authenticationProvider;
private SignUpMapper signUpMapper; private SignUpMapper signUpMapper;
private SignUpValidator signUpValidator; private SignUpValidator signUpValidator;
private UserRepository userRepository; private UserRepository repository;
public UserService(CustomAuthenticationProvider authenticationProvider, public UserService(CustomAuthenticationProvider authenticationProvider,
SignUpMapper signUpMapper, SignUpMapper signUpMapper,
SignUpValidator signUpValidator, SignUpValidator signUpValidator,
UserRepository userRepository) { UserRepository repository) {
this.authenticationProvider = authenticationProvider; this.authenticationProvider = authenticationProvider;
this.signUpMapper = signUpMapper; this.signUpMapper = signUpMapper;
this.signUpValidator = signUpValidator; this.signUpValidator = signUpValidator;
this.userRepository = userRepository; this.repository = repository;
} }
public void authenticate(User user) { public User authenticate(User user) {
User authenticatedUser = checkCredentials(user.getEmail(), user.getPassword()); User authenticatedUser = checkCredentials(user.getEmail(), user.getPassword());
authenticationProvider.authenticate(new UsernamePasswordAuthenticationToken( authenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(
@@ -45,10 +45,12 @@ public class UserService {
user.getPassword(), user.getPassword(),
fetchGrantedAuthorities(authenticatedUser) fetchGrantedAuthorities(authenticatedUser)
)); ));
return authenticatedUser;
} }
User checkCredentials(String email, String password) { User checkCredentials(String email, String password) {
Optional<User> optUser = userRepository.findByEmail(email); Optional<User> optUser = repository.findByEmail(email);
if(optUser.isEmpty() || !optUser.get().getPassword().equals(password)) { if(optUser.isEmpty() || !optUser.get().getPassword().equals(password)) {
throw new BadRequestException("Credentials are incorrect."); throw new BadRequestException("Credentials are incorrect.");
@@ -73,24 +75,24 @@ public class UserService {
} }
public List<ApplicationRole> getApplicationRolesByEmail(String email) { public List<ApplicationRole> getApplicationRolesByEmail(String email) {
return userRepository.getApplicationRolesByEmail(email); return repository.getApplicationRolesByEmail(email);
} }
public void signUp(SignUpDTO inputData) { public void signUp(SignUpDTO inputData) {
signUpValidator.validate(inputData); signUpValidator.validate(inputData);
if(userRepository.isEmailAlreadyExists(inputData.getEmail())) { if(repository.isEmailAlreadyExists(inputData.getEmail())) {
throw new BadRequestException("Email is already assigned to another user."); throw new BadRequestException("Email is already assigned to another user.");
} }
userRepository.save(signUpMapper.toUser(inputData)); repository.save(signUpMapper.toUser(inputData));
} }
public boolean isAdmin(User user) { public boolean isAdmin(User user) {
return userRepository.isAdmin(user.getId()); return repository.isAdmin(user.getId());
} }
public Optional<User> findByEmail(String email) { public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email); return repository.findByEmail(email);
} }
} }

View File

@@ -11,7 +11,7 @@ CREATE TABLE "user" (
CREATE TABLE application ( CREATE TABLE application (
id uuid DEFAULT uuid_generate_v4(), id uuid DEFAULT uuid_generate_v4(),
name VARCHAR NOT NULL UNIQUE, name VARCHAR NOT NULL UNIQUE,
service_name VARCHAR NOT NULL, service_name VARCHAR NOT NULL UNIQUE,
CONSTRAINT application_pk PRIMARY KEY (id) CONSTRAINT application_pk PRIMARY KEY (id)
); );
@@ -20,7 +20,8 @@ CREATE TABLE configuration_file (
path VARCHAR NOT NULL, path VARCHAR NOT NULL,
application_id uuid NOT NULL, application_id uuid NOT NULL,
CONSTRAINT configuration_file_pk PRIMARY KEY (id), CONSTRAINT configuration_file_pk PRIMARY KEY (id),
CONSTRAINT configuration_file_application_id_fk FOREIGN KEY (application_id) REFERENCES application (id) CONSTRAINT configuration_file_application_id_fk FOREIGN KEY (application_id) REFERENCES application (id),
CONSTRAINT configuration_file_path_application_id_unique UNIQUE (path, application_id)
); );
CREATE INDEX configuration_file_application_id_idx ON configuration_file(application_id); CREATE INDEX configuration_file_application_id_idx ON configuration_file(application_id);

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

@@ -0,0 +1,13 @@
# Editor configuration, see https://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

46
src/main/ts/.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

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

@@ -0,0 +1,150 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"cerberus": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/cerberus",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss",
"node_modules/@fortawesome/fontawesome-free/scss/solid.scss",
"node_modules/@fortawesome/fontawesome-free/scss/regular.scss",
"node_modules/@fortawesome/fontawesome-free/scss/brands.scss",
"node_modules/angular-bootstrap-md/assets/scss/bootstrap/bootstrap.scss",
"node_modules/angular-bootstrap-md/assets/scss/mdb.scss",
"node_modules/animate.css/animate.css",
"src/styles.scss"
],
"scripts": [
"node_modules/chart.js/dist/Chart.js",
"node_modules/hammerjs/hammer.min.js"
],
"es5BrowserSupport": true
},
"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,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "cerberus:build"
},
"configurations": {
"production": {
"browserTarget": "cerberus:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "cerberus: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/**"
]
}
}
}
},
"cerberus-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "cerberus:serve"
},
"configurations": {
"production": {
"devServerTarget": "cerberus:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "cerberus"
}

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,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to cerberus!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root h1')).getText() as Promise<string>;
}
}

View File

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

12583
src/main/ts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,54 @@
{
"name": "cerberus",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~8.2.4",
"@angular/common": "~8.2.4",
"@angular/compiler": "~8.2.4",
"@angular/core": "~8.2.4",
"@angular/forms": "~8.2.4",
"@angular/platform-browser": "~8.2.4",
"@angular/platform-browser-dynamic": "~8.2.4",
"@angular/router": "~8.2.4",
"@fortawesome/fontawesome-free": "^5.10.2",
"@types/chart.js": "^2.8.4",
"angular-bootstrap-md": "^8.2.0",
"animate.css": "^3.7.2",
"chart.js": "^2.8.0",
"core-js": "^3.2.1",
"hammerjs": "^2.0.8",
"rxjs": "~6.5.3",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.803.2",
"@angular/cli": "~8.3.2",
"@angular/compiler-cli": "~8.2.4",
"@angular/language-service": "~8.2.4",
"@types/jasmine": "~3.4.0",
"@types/jasminewd2": "~2.0.6",
"@types/node": "~12.7.4",
"codelyzer": "~5.1.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.2",
"protractor": "~5.4.2",
"ts-node": "~8.3.0",
"tslint": "~5.19.0",
"typescript": "^3.5.3"
}
}

View File

@@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:8080",
"secure": false
}
}

View File

@@ -0,0 +1,23 @@
import { UserManagementComponent } from './management/user-management/user-management.component';
import { AppEditionComponent } from './management/app-management/app-edition/app-edition.component';
import { AppManagementComponent } from './management/app-management/app-management.component';
import { DisconnectionComponent } from './disconnection/disconnection.component';
import { AppComponent } from './app.component';
import { ServiceUnavailableComponent } from './service-unavailable/service-unavailable.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: 'serviceUnavailable', component: ServiceUnavailableComponent },
{ path: 'disconnection', component: DisconnectionComponent },
{ path: 'management/applications', component: AppManagementComponent },
{ path: 'management/applications/:appId', component: AppEditionComponent },
{ path: 'management/users', component: UserManagementComponent },
{ path: '', component: AppComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

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

View File

@@ -0,0 +1,7 @@
main {
&.container {
margin-top: 75px;
padding: 15px 0;
padding-bottom: 50px;
}
}

View File

@@ -0,0 +1,35 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'cerberus'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('cerberus');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to cerberus!');
});
});

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 = 'cerberus';
}

View File

@@ -0,0 +1,58 @@
import { AccordionComponent } from './core/components/accordion/accordion.component';
import { UserManagementComponent } from './management/user-management/user-management.component';
import { AppEditionComponent } from './management/app-management/app-edition/app-edition.component';
import { AppManagementComponent } from './management/app-management/app-management.component';
import { SideNavElementComponent } from './header/side-nav/side-nav-element/side-nav-element.component';
import { SideNavComponent } from './header/side-nav/side-nav.component';
import { ServiceUnavailableInterceptor } from './core/interceptors/service-unavailable.interceptor';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { UserService } from './core/services/user.service';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MDBBootstrapModule } from 'angular-bootstrap-md';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { NotificationElement } from './core/notifications/notification-element/notification-element.component';
import { NotificationsComponent } from './core/notifications/notifications.component';
import { LoginComponent } from './login/login.component';
import { ServiceUnavailableComponent } from './service-unavailable/service-unavailable.component';
import { DisconnectionComponent } from './disconnection/disconnection.component';
@NgModule({
declarations: [
AppComponent,
FooterComponent,
HeaderComponent,
LoginComponent,
NotificationElement,
NotificationsComponent,
ServiceUnavailableComponent,
DisconnectionComponent,
SideNavComponent,
SideNavElementComponent,
AppManagementComponent,
AppEditionComponent,
UserManagementComponent,
AccordionComponent
],
imports: [
AppRoutingModule,
BrowserModule,
FormsModule,
HttpClientModule,
MDBBootstrapModule.forRoot()
],
providers: [
UserService,
{ provide: HTTP_INTERCEPTORS, useClass: ServiceUnavailableInterceptor, multi: true }
],
bootstrap: [
AppComponent
]
})
export class AppModule { }

View File

@@ -0,0 +1,6 @@
<div id="wrapper">
<button type="button" id="accordion" #accordion>{{title}}</button>
<div id="panel" #panel>
<ng-content></ng-content>
</div>
</div>

View File

@@ -0,0 +1,41 @@
#wrapper {
background-color: #c9dbf3;
width: 100%;
#accordion {
background-color: #eee;
border: none;
color: #444;
cursor: pointer;
outline: none;
padding: 18px;
text-align: left;
transition: 0.4s;
width: 100%;
&.active, &:hover {
background-color: #ccc;
}
&:after {
content: '\02795';
font-size: 13px;
color: #777;
float: right;
margin-left: 5px;
}
&.active:after {
content: "\2796";
}
}
#panel {
padding: 0 18px;
background-color: white;
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
}

View File

@@ -0,0 +1,27 @@
import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-accordion',
templateUrl: './accordion.component.html',
styleUrls: ['./accordion.component.scss']
})
export class AccordionComponent implements OnInit {
@Input() title: string;
@Input() group: string;
@ViewChild('accordion', {static: true}) accordion: ElementRef;
@ViewChild('panel', {static: true}) panel: ElementRef;
constructor() { }
ngOnInit() {
this.accordion.nativeElement.addEventListener('click', () => {
this.accordion.nativeElement.classList.toggle('active');
const maxHeight = this.panel.nativeElement.style.maxHeight;
console.log(this.panel.nativeElement.style);
this.panel.nativeElement.style.maxHeight = !!maxHeight ? null : `${this.panel.nativeElement.scrollHeight}px`;
});
}
}

View File

@@ -0,0 +1,48 @@
export class User {
constructor(
public id: string,
public name: string,
public email: string
) {}
public static new(): User {
return new User('', '', '');
}
}
export class SignUpDTO {
constructor(
public name: string,
public email: string,
public password: string,
public confirmPassword: string
) {}
public static new(): SignUpDTO {
return new SignUpDTO('', '', '', '');
}
}
export class ConfigurationFile {
constructor(
public id: string,
public content: string,
public htmlName: string
) {}
public static new(): ConfigurationFile {
return new ConfigurationFile('', '', `${Math.random() * 100}`);
}
}
export class Application {
constructor(
public id: string,
public name: string,
public configurationFileList: Array<ConfigurationFile> = []
) {}
public static new(): Application {
return new Application('', '', []);
}
}

View File

@@ -0,0 +1,42 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { NotificationsComponent } from '../notifications/notifications.component';
/**
* Abstract interceptor that do redirection if an error code is intercepted.
*/
export abstract class AbstractInterceptor implements HttpInterceptor {
constructor(
protected router: Router
) {}
/**
* Returns the error code to intercept for redirection.
*/
protected abstract getHandledErrorCode(): number;
/**
* Returns the error message to show in an error notification.
*/
protected abstract supplyErrorMessage(): string;
/**
* Returns the angular route for redirection if the handled error code is intercepted.
*/
protected abstract supplyRedirectionRoute(): string;
handleError = (error: HttpErrorResponse) => {
if (!!error && error.status === this.getHandledErrorCode()) {
NotificationsComponent.error(this.supplyErrorMessage());
this.router.navigate([this.supplyRedirectionRoute()]);
}
return throwError(error);
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(catchError(this.handleError));
}
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { catchError } from 'rxjs/operators';
import { NotificationsComponent } from '../notifications/notifications.component';
import { AbstractInterceptor } from './abstract-interceptor';
@Injectable({providedIn: 'root'})
export class ServiceUnavailableInterceptor extends AbstractInterceptor {
constructor(
protected router: Router
) {
super(router);
}
protected getHandledErrorCode(): number {
return 504;
}
protected supplyErrorMessage(): string {
return 'Le serveur est actuellement indisponible. Veuillez contacter l\'administrateur.';
}
protected supplyRedirectionRoute(): string {
return '/serviceUnavailable';
}
}

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AbstractInterceptor } from './abstract-interceptor';
@Injectable({providedIn: 'root'})
export class UnauthorizedInterceptor extends AbstractInterceptor {
constructor(
protected router: Router
) {
super(router);
}
protected getHandledErrorCode(): number {
return 401;
}
protected supplyErrorMessage(): string {
return 'Impossible d\'accéder à cette ressource car vous n\'êtes pas connecté.';
}
protected supplyRedirectionRoute(): string {
return '/login';
}
}

View File

@@ -0,0 +1,26 @@
/**
* Class which represents a notification class.
* It serves to set the notification appearence.
*/
export class NotificationClass {
/**
* Default constructor.
* @param {string} icon Class name of font-awsome icon.
* @param {string} clazz The class to set notification style.
*/
constructor(
public icon: string,
public clazz: string,
) {}
}
/**
* Constant instances of NotificationClass.
*/
export const NotificationClasses = Object.freeze({
'Error': new NotificationClass('exclamation-circle ', 'alert-danger'),
'Warn': new NotificationClass('exclamation-triangle', 'alert-warning'),
'Info': new NotificationClass('info-circle', 'alert-info'),
'Success': new NotificationClass('check-circle', 'alert-success')
});

View File

@@ -0,0 +1,7 @@
<div id="notification" #notification class="alert {{model.notificationClass.clazz}}" role="alert">
<mdb-icon fas [icon]="model.notificationClass.icon"></mdb-icon> {{model.content}}
<span id="close">
<i class="fa fa-times-circle fas close"
(click)="model.hide()"></i>
</span>
</div>

View File

@@ -0,0 +1,78 @@
import { NotificationClass } from '../notification-class';
import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
/**
* Class which represents a notification in the notifications list.
*/
@Component({
selector: 'app-notification-element',
templateUrl: 'notification-element.component.html',
styles: [`
#notification {
transition: all 0.7s ease-out;
position: relative;
}
.close {
position: absolute;
right: 7px;
top: 12px;
font-size: 19px;
opacity: 0;
}
#notification:hover .close {
opacity: 0.5;
}
`]
})
export class NotificationElement implements OnInit {
/**
* The notification model.
*/
@Input() model: NotificationModel;
/**
* The notification DOM element.
*/
@ViewChild('notification', {static: true}) notification: ElementRef;
/**
* Sets the DOM element in the model object and plays with opacity.
*/
ngOnInit(): void {
this.model.notification = this.notification;
this.notification.nativeElement.style.opacity = 0;
setTimeout(() => {
this.notification.nativeElement.style.opacity = 1;
}, 100);
}
}
/**
* Class which represents the notification model.
*/
export class NotificationModel {
/**
* Element which represents the DOM element of the notification element.
*/
notification: ElementRef;
/**
* Default constructor.
* @param {string} content The message of the notification.
* @param {NotificationClass} notificationClass The category of the notification (info, error...).
*/
constructor(
public content: string,
public notificationClass: NotificationClass
) {}
/**
* Hides the notification DOM element.
*/
public hide(): void {
this.notification.nativeElement.style.opacity = 0;
setTimeout(() => {
this.notification.nativeElement.style.display = 'none';
}, 800);
}
}

View File

@@ -0,0 +1,5 @@
<div id="notification-container">
<app-notification-element *ngFor="let notification of notificationList"
[model]="notification"></app-notification-element>
</div>

View File

@@ -0,0 +1,108 @@
import { NotificationClass, NotificationClasses } from './notification-class';
import { NotificationModel } from './notification-element/notification-element.component';
import { Component, OnInit } from '@angular/core';
/**
* Class which offers the notifications service.
*/
@Component({
selector: 'app-notifications',
templateUrl: 'notifications.component.html',
styles: [`
#notification-container {
position: fixed;
top: 50px;
right: 20px;
width: 300px;
z-index: 1100;
}
`]
})
export class NotificationsComponent implements OnInit {
/**
* Singleton of the notification service.
*/
private static component: NotificationsComponent;
/**
* List of notifications model.
*/
notificationList: Array<NotificationModel> = [];
/**
* Creates an error notification.
* @param {string} message The content of the notification.
*/
public static error(message: string): void {
NotificationsComponent.notif(message, NotificationClasses.Error);
}
/**
* Creates a warning notification.
* @param {string} message The content of the notification.
*/
public static warn(message: string): void {
NotificationsComponent.notif(message, NotificationClasses.Warn);
}
/**
* Creates an info notification.
* @param {string} message The content of the notification.
*/
public static info(message: string): void {
NotificationsComponent.notif(message, NotificationClasses.Info);
}
/**
* Creates a success notification.
* @param {string} message The content of the notification.
*/
public static success(message: string): void {
NotificationsComponent.notif(message, NotificationClasses.Success);
}
/**
* Create a notification. The {@code notifClass} param defines the category of
* the notification (info, error...).
* @param {string} message The content of the notification.
* @param {NotificationClass} notifClass The category of the notification.
*/
private static notif(message: string, notifClass: NotificationClass): void {
const elem = new NotificationModel(message, notifClass);
NotificationsComponent.component.notificationList.push(elem);
setTimeout(() => {
elem.hide();
setTimeout(() => {
NotificationsComponent.clearNotificationList();
}, 900);
}, 4500);
}
/**
* Clears the consumed notifications in the list.
* When a notification is created, a cooldown is set to hide it after a certain time period.
* In this cooldown, the notification have only its display as {@code none}, but the
* notification isn't remove from the list. This method removes it.
*/
private static clearNotificationList(): void {
NotificationsComponent.component.notificationList.forEach(elem => {
if (elem.notification.nativeElement.style.display === 'none') {
const index = NotificationsComponent.component.notificationList.indexOf(elem);
if (index > -1) {
NotificationsComponent.component.notificationList.splice(index, 1);
}
}
});
}
/**
* Set the reference of the singleton here because this component
* is created at the application startup.
*/
ngOnInit(): void {
NotificationsComponent.component = this;
}
}

View File

@@ -0,0 +1,21 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Application } from '../entities';
@Injectable({
providedIn: 'root'
})
export class ApplicationService {
constructor(
private httpClient: HttpClient
) {}
findAll(): Observable<Array<Application>> {
return this.httpClient.get<Array<Application>>(`/api/applications`);
}
findById(appId: string): Observable<Application> {
return this.httpClient.get<Application>(`/api/applications/${appId}`);
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { User } from '../entities';
const PARAM_USER = 'user';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor() {}
public setAuthenticated(user: User): void {
this.setUser(user);
}
public setAnonymous(): void {
localStorage.clear();
}
public isAuthenticated(): boolean {
return this.getUser() != null;
}
public isAdmin(): boolean {
return false;
}
private 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,22 @@
import { AuthService } from './auth.service';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { User } from '../entities';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(
private httpClient: HttpClient
) {}
login(user: User): Observable<User> {
return this.httpClient.post<User>(`/api/users/login`, user);
}
disconnection(): Observable<void> {
return this.httpClient.get<void>(`/api/users/disconnection`);
}
}

View File

@@ -0,0 +1,29 @@
import { Router } from '@angular/router';
import { AuthService } from './../core/services/auth.service';
import { UserService } from './../core/services/user.service';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-disconnection',
template: ''
})
export class DisconnectionComponent implements OnInit {
content = 'Déconnexion...';
constructor(
private authService: AuthService,
private router: Router,
private userService: UserService
) {}
ngOnInit(): void {
this.userService.disconnection().subscribe(() => {
this.authService.setAnonymous();
}, error => {
console.error('Error during disconnection: ', error);
this.content = 'Une erreur est survenue lors de la déconnexion.';
}, () => {
this.router.navigate(['/']);
});
}
}

View File

@@ -0,0 +1,8 @@
<footer class="font-small brown darken-2">
<span id="middle-area">
© 2019 Copyright - Cerberus
</span>
<span id="right-area">
v1.0.0-SNAPSHOT
</span>
</footer>

View File

@@ -0,0 +1,31 @@
footer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 10px 30px;
color: #9d9d9d;
#left-area {
position: absolute;
top: 12px;
}
#middle-area {
display: block;
width: 70%;
margin: auto;
text-align: center;
}
#right-area {
position: absolute;
top: 11px;
right: 15px;
button {
padding-right: 1.14rem;
padding-left: 1.14rem;
}
}
}

View File

@@ -0,0 +1,28 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,25 @@
<header class="fixed-top brown darken-2 white-text">
<span id="left-area">
<button mdbBtn class="flat rounded-circle" (click)="sideNav.open()" mdbWavesEffect>
<i class="fa fa-bars"></i>
</button>
</span>
<span id="middle-area">
<a [routerLink]="['/']" routerLinkActive="router-link-active">
<img src="assets/images/icon.png" alt="Cerberus" />
<span id="title">Cerberus</span>
</a>
</span>
<span id="right-area">
<button mdbBtn class="flat rounded-pill" (click)="loginForm.show()" *ngIf="!isAuthenticated()" mdbWavesEffect>
<i class="fa fa-sign-in-alt"></i>
Connexion
</button>
<a [routerLink]="['/disconnection']" mdbBtn id="disconnection-btn" class="flat rounded-pill" *ngIf="isAuthenticated()" mdbWavesEffect>
<i class="fa fa-sign-out-alt"></i>
Déconnexion
</a>
</span>
</header>
<app-login #loginForm></app-login>
<app-side-nav #sideNav></app-side-nav>

View File

@@ -0,0 +1,48 @@
h1 {
display: inline-block;
}
header {
padding: 10px 30px;
#left-area {
position: absolute;
top: 12px;
button {
font-size: 1rem;
padding: 5px 10px !important;
}
}
#middle-area {
display: block;
width: 70%;
margin: auto;
#title {
display: inline-block;
vertical-align: middle;
font-weight: bolder;
color: white;
font-size: 2em;
padding-left: 10px;
}
}
#right-area {
position: absolute;
top: 5px;
right: 15px;
.rounded-pill {
padding-right: 1.14rem;
padding-left: 1.14rem;
}
#disconnection-btn:hover {
background-color: #b71c1c;
}
}
}

View File

@@ -0,0 +1,28 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
import { SideNavComponent } from './side-nav/side-nav.component';
import { AuthService } from './../core/services/auth.service';
import { NotificationsComponent } from './../core/notifications/notifications.component';
import { Component, OnInit, ViewChild } from '@angular/core';
import { LoginComponent } from '../login/login.component';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent {
constructor(
private authService: AuthService
) {}
isAuthenticated(): boolean {
return this.authService.isAuthenticated();
}
}

View File

@@ -0,0 +1,6 @@
<a [routerLink]="['/' + routerLink]" routerLinkActive="router-link-active">
<div id="element" #element>
<i class="element-icon fa fa-{{icon}}"></i>
{{text}}
</div>
</a>

View File

@@ -0,0 +1,21 @@
a, a:hover, a:visited, a:focus {
color: inherit;
}
#element {
padding: 15px 25px;
padding-left: 60px;
position: relative;
&:hover {
background-color: #402c25;
}
.element-icon {
left: 25px;
margin-right: 15px;
position: absolute;
top: 19px;
}
}

View File

@@ -0,0 +1,20 @@
import { SIDE_NAV_WIDTH } from './../side-nav.component';
import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-side-nav-element',
templateUrl: './side-nav-element.component.html',
styleUrls: ['./side-nav-element.component.scss']
})
export class SideNavElementComponent implements OnInit {
@ViewChild('element', {static: true}) element: ElementRef;
@Input() text: string;
@Input() icon: string;
@Input() routerLink: string;
constructor() {}
ngOnInit(): void {
this.element.nativeElement.style.width = SIDE_NAV_WIDTH;
}
}

View File

@@ -0,0 +1,10 @@
<div id="overlay" #overlay (click)="close()"></div>
<div id="sideNav" #sideNav>
<i id="sideNav-close" class="fa fa-times-circle" (click)="close()"></i>
<div id="header" #header>Menu principal</div>
<div id="content">
<ng-container *ngFor="let elem of appSideNavElementList">
<app-side-nav-element [text]="elem.text" [icon]="elem.icon" [routerLink]="elem.link" (click)="close()"></app-side-nav-element>
</ng-container>
</div>
</div>

View File

@@ -0,0 +1,53 @@
#sideNav {
background-color: #5d4037;
color: white;
height: 100%;
left: 0;
overflow-x: hidden;
padding-top: 78px;
position: fixed;
top: 0;
transition: 0.3s;
width: 0;
z-index: 1050;
#sideNav-close {
background-color: transparent;
border: 0;
position: absolute;
right: 30px;
top: 31px;
z-index: 1;
&:hover {
cursor: pointer;
color: #ddd;
}
}
#header {
font-size: 2rem;
left: 0;
padding: 15px;
position: absolute;
text-align: center;
top: 0;
}
#content {
}
}
#overlay {
background-color: #000;
left: 0;
height: 100%;
opacity: 0;
top: 0;
position: fixed;
transition: 0.5s;
visibility: hidden;
width: 100%;
z-index: 1049;
}

View File

@@ -0,0 +1,42 @@
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
export const SIDE_NAV_WIDTH = '350px';
@Component({
selector: 'app-side-nav',
templateUrl: './side-nav.component.html',
styleUrls: ['./side-nav.component.scss']
})
export class SideNavComponent implements OnInit {
appSideNavElementList = [{
text: 'Gestion des utilisateurs',
icon: 'users-cog',
link: 'management/users'
}, {
text: 'Gestion des applications',
icon: 'window-restore',
link: 'management/applications'
}];
@ViewChild('sideNav', {static: true}) sideNavDiv: ElementRef;
@ViewChild('overlay', {static: true}) overlay: ElementRef;
@ViewChild('header', {static: true}) header: ElementRef;
constructor() {}
ngOnInit() {
this.header.nativeElement.style.width = SIDE_NAV_WIDTH;
}
open(): void {
this.sideNavDiv.nativeElement.style.width = SIDE_NAV_WIDTH;
this.overlay.nativeElement.style.visibility = 'visible';
this.overlay.nativeElement.style.opacity = 0.5;
}
close(): void {
this.sideNavDiv.nativeElement.style.width = '0px';
this.overlay.nativeElement.style.visibility = 'hidden';
this.overlay.nativeElement.style.opacity = 0;
}
}

View File

@@ -0,0 +1,59 @@
<div mdbModal
#loginModal="mdbModal"
class="modal fade"
tabindex="-1"
role="dialog"
aria-labelledby="loginModal"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form (ngSubmit)="onSubmit()" ngNativeValidate>
<div class="modal-header">
<button type="button" class="close pull-right" aria-label="Close" (click)="loginModal.hide()">
<span aria-hidden="true">×</span>
</button>
<img src="assets/images/icon-64.png"/>
<h4 class="modal-title w-100">
Connexion
</h4>
</div>
<div class="modal-body">
<!-- Email address -->
<div class="md-form">
<input mdbInputDirective
type="email"
id="email"
name="email"
class="form-control"
#email="ngModel"
[(ngModel)]="model.email"
data-error="Veuillez saisir une adresse email valide"
[validateSuccess]="false"
required >
<label for="email">Adresse email</label>
</div>
<!-- Password -->
<div class="md-form">
<input mdbInputDirective
type="password"
id="password"
name="password"
class="form-control"
#password="ngModel"
[(ngModel)]="model.password"
data-error="Veuillez saisir votre mot de passe"
[validateSuccess]="false"
required >
<label for="password">Mot de passe</label>
</div>
</div>
<div class="modal-footer">
<button mdbBtn type="submit" color="indigo" size="sm" class="rounded-pill" mdbWavesEffect>
Valider
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
// #form {
// width: 300px;
// margin: auto;
// margin-top: 50px;
// }
// #logo {
// text-align: center;
// margin-bottom: 15px;
// }
// #title {
// text-align: center;
// font-size: 1.5em;
// }
// #buttons-area {
// button {
// width: 90%;
// margin: auto;
// }
// a {
// margin: auto;
// margin-bottom: 20px;
// }
// }
// #signup {
// text-align: center;
// margin-top: 35px;
// }
.modal-dialog {
margin-top: 50px;
width: 300px;
.modal-content {
.modal-header {
img {
position: absolute;
top: -25px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 64px;
}
.modal-title {
margin-top: 30px;
text-align: center;
font-weight: bolder;
}
}
.modal-footer {
padding: 10px;
}
}
}

View File

@@ -0,0 +1,28 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,40 @@
import { AuthService } from './../core/services/auth.service';
import { Component, OnInit, ViewChild } from '@angular/core';
import { User } from './../core/entities';
import { ModalDirective } from 'angular-bootstrap-md';
import { UserService } from '../core/services/user.service';
import { NotificationsComponent } from '../core/notifications/notifications.component';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
model: User = User.new();
@ViewChild('loginModal', {static: true}) loginModal: ModalDirective;
constructor(
private userService: UserService,
private authService: AuthService
) {}
onSubmit(): void {
this.userService.login(this.model).subscribe(user => {
NotificationsComponent.success('Connexion réussie.');
this.authService.setAuthenticated(user);
this.loginModal.hide();
}, ex => {
console.error('Error during login attempt: ', ex);
if (ex.error.status === 400) {
NotificationsComponent.error(`Login ou mot de passe incorrect.`);
} else {
NotificationsComponent.error(`Une erreur technique est survenue :\n${ex.error.message}`);
}
});
}
show(): void {
this.loginModal.show();
}
}

View File

@@ -0,0 +1,107 @@
<!-- <form (ngSubmit)="onSubmit()" ngNativeValidate>
<div class="md-form">
<input mdbInputDirective
type="text"
id="name"
name="name"
class="form-control"
[(ngModel)]="model.name"
data-error="Veuillez saisir un nom d'application"
[validateSuccess]="false"
required >
<label for="email">Nom de l'application</label>
</div>
<button mdbBtn floating="true" type="button" color="green" class="btn-floating" (click)="addConfigurationFile()" mdbWavesEffect>
<i class="fa fa-plus"></i>
</button>
<div *ngFor="let confFile of model.configurationFileList" class="row">
<app-accordion [title]="confFile.htmlName">
<div class="md-form col-9">
<textarea mdbInputDirective
type="text"
[name]="confFile.htmlName"
class="md-textarea form-control"
[(ngModel)]="confFile.content"
data-error="Veuillez saisir le contenu du fichier de configuration."
[validateSuccess]="false"
required>
</textarea>
<label for="text">Contenu de l'article</label>
</div>
<div class="col-2">
<button mdbBtn floating="true" type="button" color="red" size="sm" class="btn-floating" (click)="removeConfigurationFile(confFile)" mdbWavesEffect>
<i class="fa fa-trash"></i>
</button>
</div>
</app-accordion>
</div>
<button mdbBtn type="submit" color="indigo" size="sm" class="rounded-pill" mdbWavesEffect>
<i class="fa fa-save"></i>
</button>
</form> -->
<div mdbModal
#appEditionModal="mdbModal"
class="modal fade"
tabindex="-1"
role="dialog"
aria-labelledby="myModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<form (ngSubmit)="onSubmit()" ngNativeValidate>
<!-- Application name -->
<div class="md-form">
<input mdbInputDirective
type="text"
id="name"
name="name"
class="form-control"
[(ngModel)]="model.name"
data-error="Veuillez saisir un nom d'application"
[validateSuccess]="false"
required >
<label for="email">Nom de l'application</label>
</div>
<!-- Button to add configuration file -->
<button mdbBtn floating="true" type="button" color="green" class="btn-floating" (click)="addConfigurationFile()" mdbWavesEffect>
<i class="fa fa-plus"></i>
</button>
<div *ngFor="let confFile of model.configurationFileList" class="row">
<app-accordion [title]="confFile.htmlName">
<div class="md-form col-9">
<textarea mdbInputDirective
type="text"
[name]="confFile.htmlName"
class="md-textarea form-control"
[(ngModel)]="confFile.content"
data-error="Veuillez saisir le contenu du fichier de configuration."
[validateSuccess]="false"
required>
</textarea>
<label for="text">Contenu de l'article</label>
</div>
<div class="col-2">
<button mdbBtn floating="true" type="button" color="red" size="sm" class="btn-floating" (click)="removeConfigurationFile(confFile)" mdbWavesEffect>
<i class="fa fa-trash"></i>
</button>
</div>
</app-accordion>
</div>
<button mdbBtn type="submit" color="indigo" size="sm" class="rounded-pill" mdbWavesEffect>
<i class="fa fa-save"></i>
</button>
</form>
</div>
</div>

View File

@@ -0,0 +1,3 @@
app-accordion {
width: 100%;
}

View File

@@ -0,0 +1,49 @@
import { NotificationsComponent } from 'src/app/core/notifications/notifications.component';
import { Application, ConfigurationFile } from './../../../core/entities';
import { ApplicationService } from './../../../core/services/application.service';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-app-edition',
templateUrl: './app-edition.component.html',
styleUrls: ['./app-edition.component.scss']
})
export class AppEditionComponent implements OnInit {
model: Application = Application.new();
constructor(
private activatedRoute: ActivatedRoute,
private applicationService: ApplicationService,
private router: Router
) {}
ngOnInit() {
const appId = this.activatedRoute.snapshot.paramMap.get('appId');
if (!!appId) {
this.applicationService.findById(appId).subscribe(application => {
this.model = application;
}, error => {
console.error(error);
NotificationsComponent.error('Une erreur est survenue lors de la récupération des données de l\'application.');
this.router.navigate(['/management/applications']);
});
}
}
onSubmit() {
}
addConfigurationFile(): void {
if (!!this.model.configurationFileList) {
this.model.configurationFileList.push(ConfigurationFile.new());
} else {
this.model.configurationFileList = [ConfigurationFile.new()];
}
}
removeConfigurationFile(confFile: ConfigurationFile): void {
this.model.configurationFileList = this.model.configurationFileList.filter(c => c !== confFile);
}
}

View File

@@ -0,0 +1,27 @@
<span *ngIf="!!loading">
Chargement des applications...
</span>
<ng-container *ngIf="!loading">
<div class="row">
<div class="app-card col-12 col-sm-6 col-lg-4" *ngFor="let app of appList">
<a [routerLink]="['/management/applications/' + app.id]" routerLinkActive="router-link-active">
<mdb-card>
<mdb-card-body>
<mdb-card-title>
<h4>{{app.name}}</h4>
</mdb-card-title>
<!--Text-->
<mdb-card-text>
Description fake.
</mdb-card-text>
<a href="#" mdbBtn color="primary" mdbWavesEffect>Button</a>
</mdb-card-body>
</mdb-card>
</a>
</div>
</div>
</ng-container>
<app-app-edition></app-app-edition>

View File

@@ -0,0 +1,12 @@
.row {
.app-card {
// width: 300px;
// margin: 25px auto;
margin-top: 25px;
margin-bottom: 25px;
&.col-12 {
max-width: 90%;
}
}
}

View File

@@ -0,0 +1,32 @@
import { ApplicationService } from './../../core/services/application.service';
import { Component, OnInit } from '@angular/core';
import { Application } from 'src/app/core/entities';
import { NotificationsComponent } from 'src/app/core/notifications/notifications.component';
@Component({
selector: 'app-app-management',
templateUrl: './app-management.component.html',
styleUrls: ['./app-management.component.scss']
})
export class AppManagementComponent implements OnInit {
appList: Array<Application> = [];
loading: boolean;
constructor(
private applicationService: ApplicationService
) {}
ngOnInit(): void {
this.loading = true;
this.applicationService.findAll().subscribe(appList => {
this.appList = appList;
appList.forEach(a => this.appList.push(a));
appList.forEach(a => this.appList.push(a));
appList.forEach(a => this.appList.push(a));
appList.forEach(a => this.appList.push(a));
}, error => {
console.error(error);
NotificationsComponent.error('Une erreur est survenue lors du chargement des applications.');
}).add(() => this.loading = false);
}
}

View File

@@ -0,0 +1,3 @@
<p>
user-management works!
</p>

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-user-management',
templateUrl: './user-management.component.html',
styleUrls: ['./user-management.component.scss']
})
export class UserManagementComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,18 @@
<div id="content">
<mdb-card>
<!--Card image-->
<mdb-card-img src="assets/images/service-unavailable.jpg" alt="Service unavailable"></mdb-card-img>
<!--Card content-->
<mdb-card-body>
<!--Title-->
<mdb-card-title>
<h4>Service indisponible</h4>
</mdb-card-title>
<!--Text-->
<mdb-card-text>
Le serveur est actuellement indisponible.<br/>
Veuillez contacter l'administrateur.
</mdb-card-text>
</mdb-card-body>
</mdb-card>
</div>

View File

@@ -0,0 +1,4 @@
#content {
width: 450px;
margin: auto;
}

View File

@@ -0,0 +1,28 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ServiceUnavailableComponent } from './service-unavailable.component';
describe('ServiceUnavailableComponent', () => {
let component: ServiceUnavailableComponent;
let fixture: ComponentFixture<ServiceUnavailableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ServiceUnavailableComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ServiceUnavailableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-service-unavailable',
templateUrl: './service-unavailable.component.html',
styleUrls: ['./service-unavailable.component.scss']
})
export class ServiceUnavailableComponent {
constructor() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 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,16 @@
// 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
};
/*
* 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: 2.4 KiB

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cerberus</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,32 @@
// 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/cerberus'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

Some files were not shown because too many files have changed in this diff Show More