Compare commits

...

6 Commits

Author SHA1 Message Date
Fabio Formosa
615761f0f3 updated README 2018-06-22 13:46:31 +02:00
Fabio Formosa
dc081a1bed added some javadocs 2018-06-22 13:46:23 +02:00
Fabio Formosa
e04311914b fixed init configuration for scheduler 2018-06-22 13:45:46 +02:00
fabio.formosa
bacd6401a3 first step into angular5 project conversion 2018-03-13 00:01:25 +01:00
fabio.formosa
f88bd50768 introduced layout of manager page 2018-02-16 01:12:35 +01:00
fabio.formosa
0b4dc4a1de first commit 2018-02-13 00:10:07 +01:00
221 changed files with 5289 additions and 8014 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.project

View File

@@ -1,22 +1,43 @@
# quartz-manager #QUARTZ MANAGER
GUI Manager for Quartz Scheduler. GUI Manager for Quartz Scheduler.
Through this webapp you can launch and control your scheduled job.
GUI Console is composed by a managament panel to set trigger, start/stop scheduler and by a log panel with progress bar to get job output. Through this webapp you can launch and control your scheduled job. The GUI Console is composed by a managament panel to set trigger, start/stop scheduler and a log panel with a progress bar to display the job output.
## SCREENSHOT ## SCREENSHOT
![Alt text](http://www.fabioformosa.it/quartz-manager/quartz-manager-screenshot_800.png "Quartz Manager Screenshot") ![Alt text](http://www.fabioformosa.it/quartz-manager/quartz-manager-2-screenshot_800.png "Quartz Manager Screenshot")
## HOW IT WORKS ## HOW IT WORKS
* Set up the trigger into the left sidebar in terms of: daily frequency and and max occurrences. * Set up the trigger into the left sidebar in terms of: daily frequency and and max occurrences.
* Press the start button * Press the start button
* The GUI manager updates the progress bar and reports all logs of your quartz job. * The GUI manager updates the progress bar and reports all logs of your quartz job.
## HOW IT RUNS ## QUICK START
1. Quartz-Manager is a Spring Boot Application. To run by CLI `mvn spring-boot:run` or through your IDE. For more details [spring boot ref.](http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-running-your-application.html) **[requirements]** Make sure you have installed
1. Open quartz-manager at the link: [http://localhost:9000/quartz-manager/manager](http://localhost:9000/quartz-manager/manager) * [Java 8](https://java.com/download/) or greater
1. Log in with default credentials: `admin/admin` * [Maven](https://maven.apache.org/)
* [npm](https://www.npmjs.com/get-npm), [node](https://nodejs.org) and [angular-cli](https://cli.angular.io/)
## HOW TO RUN YOUR JOB ```
#CLONE REPOSITORY
git clone https://github.com/fabioformosa/quartz-manager.git
# START QUARTZ-MANAGER-BACKEND
cd quartz-manager/quartz-manager-backend
mvn spring-boot:run
# START QUARTZ-MANAGER-FRONTEND
cd quartz-manager/quartz-manager-backend
npm install
npm start
```
1. Open browser at [http://localhost:4200](http://localhost:4200)
1. Log in with **default credentials**: `admin/admin`
If you are not confident with maven CLI, you can start it by your IDE. For more details [spring boot ref.](http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-running-your-application.html)
## HOW TO RUN YOUR SCHEDULED JOB
By default, quartz-manager executes the dummy job that logs "hello world!". By default, quartz-manager executes the dummy job that logs "hello world!".
Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) with yours. Follow these steps: Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) with yours. Follow these steps:
@@ -26,7 +47,7 @@ Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) wi
## HOW TO CHANGE SETTINGS ## HOW TO CHANGE SETTINGS
* Num of Threads: `/quartz-manager/src/main/resources/quartz.properties` * Num of Threads: `/quartz-manager/src/main/resources/quartz.properties`
* Credentials: `it.fabioformosa.quartzmanager.configuration.WebSecurityConfig` * Credentials: `it.fabioformosa.quartzmanager.configuration.WebSecurityConfig`
* Server context path (default `/quartz-manager`) and port (default `9000`): `/quartz-manager/src/main/resources/application.properties` * quartz-manager backend context path (default `/quartz-manager`) and port (default `8080`): `/quartz-manager/src/main/resources/application.properties`
## Tech Overview ## Tech Overview
@@ -34,8 +55,18 @@ Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) wi
**Application Server** Tomcat (embedded) **Application Server** Tomcat (embedded)
**Frontend** Angularjs 1.6.7, Thymeleaf 2.1.4, Web-Socket (sockjs 0.3.4, stompjs) **Frontend** Angular 5.2.0, Web-Socket (stompjs 2.3.3)
**Style** Bootstrap 3.3.4, animated.css, FontAwesome **Style** angular material, FontAwesome 5
From quartz manager ver 2.x.x, the new structure of project is:
* REST backend (java based, using [http://www.quartz-scheduler.org/](http://www.quartz-scheduler.org/)
* Single Page Application frontend (angular 5)
(The previous version of quartz manager was a monolithic backend that provided also frontend developed with angularjs 1.6.x. You can find it at the branch 1.x.x)
## Credits
* this project has been created from [angular-spring-starter](https://github.com/bfwg/angular-spring-starter)

View File

@@ -5,7 +5,7 @@
<groupId>it.fabioformosa</groupId> <groupId>it.fabioformosa</groupId>
<artifactId>quartz-manager</artifactId> <artifactId>quartz-manager</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>2.0.1-SNAPSHOT</version>
<packaging>war</packaging> <packaging>war</packaging>
<name>quartz-manager</name> <name>quartz-manager</name>
@@ -20,13 +20,14 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version> <java.version>1.8</java.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -38,32 +39,25 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-velocity</artifactId> <artifactId>spring-boot-devtools</artifactId>
<version>1.4.7.RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.codehaus.groovy</groupId> <groupId>org.springframework</groupId>
<artifactId>groovy</artifactId> <artifactId>spring-messaging</artifactId>
</dependency> </dependency>
<dependency>
<dependency> <groupId>org.springframework</groupId>
<groupId>net.sourceforge.nekohtml</groupId> <artifactId>spring-tx</artifactId>
<artifactId>nekohtml</artifactId> </dependency>
</dependency> <dependency>
<dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId> <artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope> <scope>provided</scope>
@@ -75,23 +69,45 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>spring-boot-starter-websocket</artifactId> <artifactId>jjwt</artifactId>
</dependency> <version>0.9.0</version>
<dependency> </dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.webjars</groupId> <groupId>joda-time</groupId>
<artifactId>bootstrap</artifactId> <artifactId>joda-time</artifactId>
<version>3.3.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>3.0.5</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.quartz-scheduler</groupId> <groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId> <artifactId>quartz</artifactId>
@@ -102,11 +118,7 @@
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>1.3.2</version> <version>1.3.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!-- Reactor --> <!-- Reactor -->
<dependency> <dependency>
<groupId>io.projectreactor</groupId> <groupId>io.projectreactor</groupId>
@@ -129,6 +141,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@@ -1,12 +1,12 @@
package it.fabioformosa; package it.fabioformosa;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class QuartManagerApplication { public class QuartManagerApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(QuartManagerApplication.class, args); SpringApplication.run(QuartManagerApplication.class, args);
} }
} }

View File

@@ -0,0 +1,93 @@
package it.fabioformosa.quartzmanager.configuration;
import java.io.IOException;
import java.util.Properties;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import it.fabioformosa.quartzmanager.jobs.myjobs.SampleJob;
import it.fabioformosa.quartzmanager.scheduler.AutowiringSpringBeanJobFactory;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitor;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitorImpl;
@Configuration
@ConditionalOnProperty(name = "quartz.enabled")
public class SchedulerConfig {
private static JobDetailFactoryBean createJobDetail(Class<?> jobClass) {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(jobClass);
factoryBean.setDurability(false);
return factoryBean;
}
private static SimpleTriggerFactoryBean createTrigger(JobDetail jobDetail, long pollFrequencyMs,
int repeatCount) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(jobDetail);
factoryBean.setStartDelay(0L);
factoryBean.setRepeatInterval(pollFrequencyMs);
factoryBean.setRepeatCount(repeatCount);
factoryBean
.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT);// in case of misfire, ignore all missed triggers and continue
return factoryBean;
}
@Bean(name = "triggerMonitor")
public TriggerMonitor createTriggerMonitor(@Qualifier("jobTrigger") Trigger trigger) {
TriggerMonitor triggerMonitor = new TriggerMonitorImpl();
triggerMonitor.setTrigger(trigger);
return triggerMonitor;
}
@Bean
public JobDetailFactoryBean jobDetail() {
return createJobDetail(SampleJob.class);
}
@Bean
public JobFactory jobFactory(ApplicationContext applicationContext) {
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
@Bean(name = "jobTrigger")
public SimpleTriggerFactoryBean sampleJobTrigger(@Qualifier("jobDetail") JobDetail jobDetail,
@Value("${job.frequency}") long frequency, @Value("${job.repeatCount}") int repeatCount) {
return createTrigger(jobDetail, frequency, repeatCount);
}
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory,
@Qualifier("jobTrigger") Trigger sampleJobTrigger) throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setJobFactory(jobFactory);
factory.setQuartzProperties(quartzProperties());
factory.setTriggers(sampleJobTrigger);
factory.setAutoStartup(false);
return factory;
}
}

View File

@@ -1,65 +1,75 @@
package it.fabioformosa.quartzmanager.configuration; package it.fabioformosa.quartzmanager.configuration;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import it.fabioformosa.quartzmanager.security.AjaxAuthenticationFilter; import it.fabioformosa.quartzmanager.security.ComboEntryPoint;
import it.fabioformosa.quartzmanager.security.ComboEntryPoint; import it.fabioformosa.quartzmanager.security.auth.AuthenticationFailureHandler;
import it.fabioformosa.quartzmanager.security.auth.AuthenticationSuccessHandler;
@Configuration
@EnableWebSecurity @Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Order(1)
public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter { @Configuration
@Override @Order(1)
protected void configure(HttpSecurity http) throws Exception { public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter {
http.csrf().disable() // @Override
.antMatcher("/notifications").authorizeRequests().anyRequest().hasAnyRole("ADMIN").and() protected void configure(HttpSecurity http) throws Exception {
.httpBasic(); http.csrf().disable() //
.antMatcher("/notifications").authorizeRequests().anyRequest().hasAnyRole("ADMIN").and()
// http.antMatcher("/logs/**").authorizeRequests().anyRequest() .httpBasic();
// .permitAll();
} // http.antMatcher("/logs/**").authorizeRequests().anyRequest()
} // .permitAll();
}
@Configuration }
@Order(2)
public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter { @Configuration
@Order(2)
@Resource public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter {
private ComboEntryPoint comboEntryPoint;
@Resource
@Override private ComboEntryPoint comboEntryPoint;
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(comboEntryPoint).and().csrf().disable()// @Autowired
.authorizeRequests().anyRequest().authenticated().and()// private AuthenticationSuccessHandler authenticationSuccessHandler;
.addFilterBefore(new AjaxAuthenticationFilter(authenticationManager()),
UsernamePasswordAuthenticationFilter.class)// @Autowired
.formLogin().loginPage("/login").permitAll().and().logout() private AuthenticationFailureHandler authenticationFailureHandler;
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/manager");
} @Override
public void configure(WebSecurity web) throws Exception {
@Override web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
public void configure(WebSecurity web) throws Exception { }
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
} @Override
} protected void configure(HttpSecurity http) throws Exception {
// http.csrf().ignoringAntMatchers("/api/login", "/api/signup").and() //
@Autowired http.cors().and().csrf().disable()
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { .exceptionHandling().authenticationEntryPoint(comboEntryPoint).and()//
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); .authorizeRequests().anyRequest().authenticated().and()//
} .formLogin().loginPage("/api/login").successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and().logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/api/logout"))
} .logoutSuccessUrl("/manager");
}
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}
}

View File

@@ -0,0 +1,105 @@
package it.fabioformosa.quartzmanager.configuration;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import it.fabioformosa.quartzmanager.security.ComboEntryPoint;
import it.fabioformosa.quartzmanager.security.auth.AuthenticationFailureHandler;
import it.fabioformosa.quartzmanager.security.auth.AuthenticationSuccessHandler;
import it.fabioformosa.quartzmanager.security.auth.LogoutSuccess;
import it.fabioformosa.quartzmanager.security.auth.TokenAuthenticationFilter;
import it.fabioformosa.quartzmanager.security.service.impl.CustomUserDetailsService;
/**
* JWT Temporary disabled
*
* @author Fabio.Formosa
*
*/
//@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigJWT extends WebSecurityConfigurerAdapter {
@Value("${jwt.cookie}")
private String TOKEN_COOKIE;
@Autowired
private CustomUserDetailsService jwtUserDetailsService;
// @Autowired
// private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Resource
private ComboEntryPoint comboEntryPoint;
@Autowired
private LogoutSuccess logoutSuccess;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder)
throws Exception {
authenticationManagerBuilder.userDetailsService(jwtUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public TokenAuthenticationFilter jwtAuthenticationTokenFilter() throws Exception {
return new TokenAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.csrf().ignoringAntMatchers("/api/login", "/api/signup")
// .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
http.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling().authenticationEntryPoint(comboEntryPoint).and()
.addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter.class)
.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/api/login")
.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/api/logout"))
.logoutSuccessHandler(logoutSuccess).deleteCookies(TOKEN_COOKIE);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}

View File

@@ -1,25 +1,25 @@
package it.fabioformosa.quartzmanager.configuration; package it.fabioformosa.quartzmanager.configuration;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration @Configuration
@EnableWebSocketMessageBroker @EnableWebSocketMessageBroker
public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer { public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry config) { public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/job"); config.setApplicationDestinationPrefixes("/job");
} }
@Override @Override
public void registerStompEndpoints(StompEndpointRegistry registry) { public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/logs").withSockJS(); registry.addEndpoint("/logs").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint("/progress").withSockJS(); registry.addEndpoint("/progress").setAllowedOrigins("/**").withSockJS();
} }
} }

View File

@@ -0,0 +1,82 @@
package it.fabioformosa.quartzmanager.controllers;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import it.fabioformosa.quartzmanager.security.TokenHelper;
import it.fabioformosa.quartzmanager.security.model.UserTokenState;
import it.fabioformosa.quartzmanager.security.service.impl.CustomUserDetailsService;
/**
* JWT Temporary disabled
*
* @author Fabio.Formosa
*
*/
//@RestController
//@RequestMapping( value = "/api", produces = MediaType.APPLICATION_JSON_VALUE )
public class AuthenticationController {
static class PasswordChanger {
public String oldPassword;
public String newPassword;
}
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
TokenHelper tokenHelper;
@Value("${jwt.expires_in}")
private int EXPIRES_IN;
@Value("${jwt.cookie}")
private String TOKEN_COOKIE;
@RequestMapping(value = "/changePassword", method = RequestMethod.POST)
@PreAuthorize("hasRole('USER')")
public ResponseEntity<?> changePassword(@RequestBody PasswordChanger passwordChanger) {
userDetailsService.changePassword(passwordChanger.oldPassword, passwordChanger.newPassword);
Map<String, String> result = new HashMap<>();
result.put( "result", "success" );
return ResponseEntity.accepted().body(result);
}
@RequestMapping(value = "/refresh", method = RequestMethod.GET)
public ResponseEntity<?> refreshAuthenticationToken(HttpServletRequest request, HttpServletResponse response) {
String authToken = tokenHelper.getToken( request );
if (authToken != null && tokenHelper.canTokenBeRefreshed(authToken)) {
// TODO check user password last update
String refreshedToken = tokenHelper.refreshToken(authToken);
Cookie authCookie = new Cookie( TOKEN_COOKIE, refreshedToken );
authCookie.setPath( "/" );
authCookie.setHttpOnly( true );
authCookie.setMaxAge( EXPIRES_IN );
// Add cookie to response
response.addCookie( authCookie );
UserTokenState userTokenState = new UserTokenState(refreshedToken, EXPIRES_IN);
return ResponseEntity.ok(userTokenState);
} else {
UserTokenState userTokenState = new UserTokenState();
return ResponseEntity.accepted().body(userTokenState);
}
}
}

View File

@@ -0,0 +1,152 @@
package it.fabioformosa.quartzmanager.controllers;
import java.util.Collections;
import java.util.Map;
import javax.annotation.Resource;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.triggers.SimpleTriggerImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import it.fabioformosa.quartzmanager.dto.SchedulerConfigParam;
import it.fabioformosa.quartzmanager.dto.TriggerProgress;
import it.fabioformosa.quartzmanager.enums.SchedulerStates;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitor;
/**
* This controller provides scheduler info about config and status. It provides
* also methods to set new config and start/stop/resume the scheduler.
*
* @author Fabio.Formosa
*
*/
@RestController
@RequestMapping("/scheduler")
public class SchedulerController {
private static final int MILLS_IN_A_DAY = 1000 * 60 * 60 * 24;
private static final int SEC_IN_A_DAY = 60 * 60 * 24;
private final Logger log = LoggerFactory.getLogger(SchedulerController.class);
@Resource
private Scheduler scheduler;
@Resource
private TriggerMonitor triggerMonitor;
@RequestMapping(value = "/config", method = RequestMethod.GET)
public SchedulerConfigParam getConfig() {
log.debug("SCHEDULER - GET CONFIG params");
SimpleTrigger simpleTrigger = (SimpleTrigger) triggerMonitor.getTrigger();
int maxCount = simpleTrigger.getRepeatCount() + 1;
long triggersPerDay = fromMillsIntervalToTriggerPerDay(simpleTrigger.getRepeatInterval());
return new SchedulerConfigParam(triggersPerDay, maxCount);
}
@RequestMapping("/progress")
public TriggerProgress getProgressInfo() throws SchedulerException {
log.trace("SCHEDULER - GET PROGRESS INFO");
TriggerProgress progress = new TriggerProgress();
SimpleTriggerImpl jobTrigger = (SimpleTriggerImpl) scheduler.getTrigger(triggerMonitor.getTrigger().getKey());
if (jobTrigger != null && jobTrigger.getJobKey() != null) {
progress.setJobKey(jobTrigger.getJobKey().getName());
progress.setJobClass(jobTrigger.getClass().getSimpleName());
progress.setTimesTriggered(jobTrigger.getTimesTriggered());
progress.setRepeatCount(jobTrigger.getRepeatCount());
progress.setFinalFireTime(jobTrigger.getFinalFireTime());
progress.setNextFireTime(jobTrigger.getNextFireTime());
progress.setPreviousFireTime(jobTrigger.getPreviousFireTime());
}
return progress;
}
@GetMapping(produces = "application/json")
public Map<String, String> getStatus() throws SchedulerException {
log.trace("SCHEDULER - GET STATUS");
String schedulerState = "";
if (scheduler.isShutdown() || !scheduler.isStarted())
schedulerState = SchedulerStates.STOPPED.toString();
else if (scheduler.isStarted() && scheduler.isInStandbyMode())
schedulerState = SchedulerStates.PAUSED.toString();
else
schedulerState = SchedulerStates.RUNNING.toString();
return Collections.singletonMap("data", schedulerState.toLowerCase());
}
@RequestMapping("/pause")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void pause() throws SchedulerException {
log.info("SCHEDULER - PAUSE COMMAND");
scheduler.standby();
}
@RequestMapping(value = "/config", method = RequestMethod.POST)
public SchedulerConfigParam postConfig(@RequestBody SchedulerConfigParam config) throws SchedulerException {
log.info("SCHEDULER - NEW CONFIG {}", config);
SimpleTrigger trigger = (SimpleTrigger) triggerMonitor.getTrigger();
TriggerBuilder<SimpleTrigger> triggerBuilder = trigger.getTriggerBuilder();
int intervalInMills = fromTriggerPerDayToMillsInterval(config.getTriggerPerDay());
Trigger newTrigger = triggerBuilder.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(intervalInMills).withRepeatCount(config.getMaxCount() - 1)).build();
scheduler.rescheduleJob(triggerMonitor.getTrigger().getKey(), newTrigger);
triggerMonitor.setTrigger(newTrigger);
return config;
}
@RequestMapping("/resume")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void resume() throws SchedulerException {
log.info("SCHEDULER - RESUME COMMAND");
scheduler.start();
}
@RequestMapping("/run")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void run() throws SchedulerException {
log.info("SCHEDULER - START COMMAND");
scheduler.start();
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@RequestMapping("/stop")
public void stop() throws SchedulerException {
log.info("SCHEDULER - STOP COMMAND");
scheduler.shutdown(true);
}
private long fromMillsIntervalToTriggerPerDay(long repeatIntervalInMills) {
return (int) Math.ceil(MILLS_IN_A_DAY / repeatIntervalInMills);
}
private int fromTriggerPerDayToMillsInterval(long triggerPerDay) {
return (int) Math.ceil(Long.valueOf(MILLS_IN_A_DAY) / triggerPerDay); // with ceil the triggerPerDay is a max value
}
@SuppressWarnings("unused")
private int fromTriggerPerDayToSecInterval(long triggerPerDay) {
return (int) Math.ceil(Long.valueOf(SEC_IN_A_DAY) / triggerPerDay);
}
}

View File

@@ -0,0 +1,35 @@
package it.fabioformosa.quartzmanager.controllers;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
@RequestMapping("/session")
public class SessionController {
private final Logger log = LoggerFactory.getLogger(SessionController.class);
@RequestMapping("/invalidate")
@PreAuthorize("hasAuthority('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void invalidateSession(HttpSession session) {
session.invalidate();
log.info("Invalidated current session!");
}
@RequestMapping("/refresh")
@PreAuthorize("hasAuthority('ADMIN')")
public HttpEntity<Void> refreshSession(HttpSession session) {
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@@ -0,0 +1,72 @@
package it.fabioformosa.quartzmanager.controllers;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
/**
* JWT Temporary disabled
*
* @author Fabio.Formosa
*
*/
// @Autowired
// private UserService userService;
// @RequestMapping(method = POST, value = "/signup")
// public ResponseEntity<?> addUser(@RequestBody UserRequest userRequest,
// UriComponentsBuilder ucBuilder) {
//
// User existUser = this.userService.findByUsername(userRequest.getUsername());
// if (existUser != null)
// throw new ResourceConflictException(userRequest.getId(), "Username already exists");
// User user = this.userService.save(userRequest);
// HttpHeaders headers = new HttpHeaders();
// headers.setLocation(ucBuilder.path("/api/user/{userId}").buildAndExpand(user.getId()).toUri());
// return new ResponseEntity<>(user, HttpStatus.CREATED);
// }
//
// @RequestMapping(method = GET, value = "/user/all")
// public List<User> loadAll() {
// return this.userService.findAll();
// }
//
// @RequestMapping(method = GET, value = "/user/{userId}")
// public User loadById(@PathVariable Long userId) {
// return this.userService.findById(userId);
// }
//
//
// @RequestMapping(method = GET, value = "/user/reset-credentials")
// public ResponseEntity<Map> resetCredentials() {
// this.userService.resetCredentials();
// Map<String, String> result = new HashMap<>();
// result.put("result", "success");
// return ResponseEntity.accepted().body(result);
// }
/*
* We are not using userService.findByUsername here(we could), so it is good that we are making
* sure that the user has role "ROLE_USER" to access this endpoint.
*/
// @RequestMapping("/whoami")
// // @PreAuthorize("hasRole('USER')")
// public User user() {
// return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// }
@RequestMapping("/whoami")
@PreAuthorize("isAuthenticated()")
public Object user() {
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}

View File

@@ -0,0 +1,40 @@
package it.fabioformosa.quartzmanager.dto;
public class SchedulerConfigParam {
public long triggerPerDay;
public int maxCount;
public SchedulerConfigParam() {
super();
}
public SchedulerConfigParam(long triggerPerDay, int maxCount) {
super();
this.triggerPerDay = triggerPerDay;
this.maxCount = maxCount;
}
public int getMaxCount() {
return maxCount;
}
public long getTriggerPerDay() {
return triggerPerDay;
}
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
public void setTriggerPerDay(long triggerPerDay) {
this.triggerPerDay = triggerPerDay;
}
@Override
public String toString() {
return "SchedulerConfigParam [triggerPerDay=" + triggerPerDay
+ ", maxCount=" + maxCount + "]";
}
}

View File

@@ -0,0 +1,5 @@
package it.fabioformosa.quartzmanager.enums;
public enum SchedulerStates {
RUNNING, STOPPED, PAUSED
}

View File

@@ -0,0 +1,18 @@
package it.fabioformosa.quartzmanager.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class ExceptionHandlingController {
@ExceptionHandler(ResourceConflictException.class)
public ResponseEntity<ExceptionResponse> resourceConflict(ResourceConflictException ex) {
ExceptionResponse response = new ExceptionResponse();
response.setErrorCode("Conflict");
response.setErrorMessage(ex.getMessage());
return new ResponseEntity<ExceptionResponse>(response, HttpStatus.CONFLICT);
}
}

View File

@@ -0,0 +1,25 @@
package it.fabioformosa.quartzmanager.exceptions;
public class ExceptionResponse {
private String errorCode;
private String errorMessage;
public ExceptionResponse() {}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}

View File

@@ -0,0 +1,21 @@
package it.fabioformosa.quartzmanager.exceptions;
public class ResourceConflictException extends RuntimeException {
private static final long serialVersionUID = 1791564636123821405L;
private Long resourceId;
public ResourceConflictException(Long resourceId, String message) {
super(message);
setResourceId(resourceId);
}
public Long getResourceId() {
return resourceId;
}
public void setResourceId(Long resourceId) {
this.resourceId = resourceId;
}
}

View File

@@ -0,0 +1,57 @@
package it.fabioformosa.quartzmanager.jobs;
import javax.annotation.Resource;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import it.fabioformosa.quartzmanager.aspects.ProgressUpdater;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord;
/**
* Extends this class to create a job that produces LogRecord to be displayed
* into the GUI panel
*
* @author Fabio.Formosa
*
*/
public abstract class AbstractLoggingJob implements Job {
private static final Logger log = LoggerFactory.getLogger(AbstractLoggingJob.class);
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@Resource
private ProgressUpdater progressUpdater;
/**
*
* @param jobExecutionContext
* @return final log
*/
public abstract LogRecord doIt(JobExecutionContext jobExecutionContext);
@Override
public final void execute(JobExecutionContext jobExecutionContext) {
try {
LogRecord logMsg = doIt(jobExecutionContext);
logAndSend(logMsg);
progressUpdater.update();
} catch (SchedulerException e) {
log.error("Error updating progress " + e.getMessage());
}
}
public void logAndSend(LogRecord logRecord) {
log.info(logRecord.getMessage());
logRecord.setThreadName(Thread.currentThread().getName());
messagingTemplate.convertAndSend("/topic/logs", logRecord);
}
}

View File

@@ -0,0 +1,67 @@
package it.fabioformosa.quartzmanager.jobs.entities;
import java.util.Date;
/**
* Log record produced by a job at the end of each run
*
* @author Fabio.Formosa
*
*/
public class LogRecord {
public enum LogType {
INFO, WARN, ERROR;
}
private Date date;
private LogType type;
private String message;
private String threadName;
public LogRecord(LogType type, String msg) {
super();
this.type = type;
message = msg;
date = new Date();
}
public Date getDate() {
return date;
}
public String getMessage() {
return message;
}
public String getThreadName() {
return threadName;
}
public LogType getType() {
return type;
}
public void setDate(Date date) {
this.date = date;
}
public void setMessage(String msg) {
message = msg;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
public void setType(LogType type) {
this.type = type;
}
@Override
public String toString() {
return "LogRecord [date=" + date + ", type=" + type + ", message=" + message + "]";
}
}

View File

@@ -1,7 +1,8 @@
package it.fabioformosa.quartzmanager.jobs; package it.fabioformosa.quartzmanager.jobs.myjobs;
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionContext;
import it.fabioformosa.quartzmanager.jobs.AbstractLoggingJob;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord; import it.fabioformosa.quartzmanager.jobs.entities.LogRecord;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord.LogType; import it.fabioformosa.quartzmanager.jobs.entities.LogRecord.LogType;

View File

@@ -0,0 +1,37 @@
package it.fabioformosa.quartzmanager.jobs.tests;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import it.fabioformosa.quartzmanager.jobs.AbstractLoggingJob;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord.LogType;
/**
* This job can be used to test the misfire policy. It pretends to be a long
* processing job (sleeping for a while)
*
* @author Fabio.Formosa
*
*/
public class MisfireTestJob extends AbstractLoggingJob {
private Logger log = LoggerFactory.getLogger(MisfireTestJob.class);
@Override
public LogRecord doIt(JobExecutionContext jobExecutionContext) {
try {
log.info("{} is going to sleep...", Thread.currentThread().getName());
Thread.sleep(10 * 1000);
log.info("{} woke up!", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
return new LogRecord(LogType.INFO, "Hello!");
}
}

View File

@@ -0,0 +1,161 @@
package it.fabioformosa.quartzmanager.security;
import java.util.Date;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetailsService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* JWT Temporary disabled
*
* @author Fabio.Formosa
*
*/
//@Component
public class TokenHelper {
@Value("${app.name}")
private String APP_NAME;
@Value("${jwt.secret}")
private String SECRET;
@Value("${jwt.expires_in}")
private int EXPIRES_IN;
@Value("${jwt.header}")
private String AUTH_HEADER;
@Value("${jwt.cookie}")
private String AUTH_COOKIE;
@Autowired
UserDetailsService userDetailsService;
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
public Boolean canTokenBeRefreshed(String token) {
try {
final Date expirationDate = getClaimsFromToken(token).getExpiration();
// String username = getUsernameFromToken(token);
// UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return expirationDate.compareTo(generateCurrentDate()) > 0;
} catch (Exception e) {
return false;
}
}
public String generateToken(String username) {
return Jwts.builder()
.setIssuer( APP_NAME )
.setSubject(username)
.setIssuedAt(generateCurrentDate())
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
}
/**
* Find a specific HTTP cookie in a request.
*
* @param request
* The HTTP request object.
* @param name
* The cookie name to look for.
* @return The cookie, or <code>null</code> if not found.
*/
public Cookie getCookieValueByName(HttpServletRequest request, String name) {
if (request.getCookies() == null)
return null;
for (int i = 0; i < request.getCookies().length; i++)
if (request.getCookies()[i].getName().equals(name))
return request.getCookies()[i];
return null;
}
public String getToken( HttpServletRequest request ) {
/**
* Getting the token from Cookie store
*/
Cookie authCookie = getCookieValueByName( request, AUTH_COOKIE );
if ( authCookie != null )
return authCookie.getValue();
/**
* Getting the token from Authentication header
* e.g Bearer your_token
*/
String authHeader = request.getHeader(AUTH_HEADER);
if ( authHeader != null && authHeader.startsWith("Bearer "))
return authHeader.substring(7);
return null;
}
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.setIssuedAt(generateCurrentDate());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
private Date generateCurrentDate() {
return new Date(getCurrentTimeMillis());
}
private Date generateExpirationDate() {
return new Date(getCurrentTimeMillis() + EXPIRES_IN * 1000);
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
private long getCurrentTimeMillis() {
return DateTime.now().getMillis();
}
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
}
}

View File

@@ -0,0 +1,44 @@
package it.fabioformosa.quartzmanager.security.auth;
import org.springframework.security.authentication.AbstractAuthenticationToken;
public class AnonAuthentication extends AbstractAuthenticationToken {
public AnonAuthentication() {
super( null );
}
@Override
public boolean equals( Object obj ) {
if ( this == obj )
return true;
if ( obj == null )
return false;
if ( getClass() != obj.getClass() )
return false;
return true;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
@Override
public int hashCode() {
int hash = 7;
return hash;
}
@Override
public boolean isAuthenticated() {
return true;
}
}

View File

@@ -0,0 +1,22 @@
package it.fabioformosa.quartzmanager.security.auth;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
@Component
public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -0,0 +1,65 @@
package it.fabioformosa.quartzmanager.security.auth;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
// @Value("${jwt.expires_in}")
// private int EXPIRES_IN;
//
// @Value("${jwt.cookie}")
// private String TOKEN_COOKIE;
//
// @Autowired
// TokenHelper tokenHelper;
//
@Autowired
ObjectMapper objectMapper;
//
// @Override
// public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
// Authentication authentication ) throws IOException, ServletException {
// clearAuthenticationAttributes(request);
// User user = (User)authentication.getPrincipal();
//
// String jws = tokenHelper.generateToken( user.getUsername() );
//
// // Create token auth Cookie
// Cookie authCookie = new Cookie( TOKEN_COOKIE, jws );
//
// authCookie.setHttpOnly( true );
//
// authCookie.setMaxAge( EXPIRES_IN );
//
// authCookie.setPath( "/" );
// // Add cookie to response
// response.addCookie( authCookie );
//
// // JWT is also in the response
// UserTokenState userTokenState = new UserTokenState(jws, EXPIRES_IN);
// String jwtResponse = objectMapper.writeValueAsString( userTokenState );
// response.setContentType("application/json");
// response.getWriter().write( jwtResponse );
//
// }
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication ) throws IOException, ServletException {
// clearAuthenticationAttributes(request);
response.setContentType("application/json");
response.getWriter().write( objectMapper.writeValueAsString("OK"));
}
}

View File

@@ -0,0 +1,35 @@
package it.fabioformosa.quartzmanager.security.auth;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class LogoutSuccess implements LogoutSuccessHandler {
@Autowired
ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
Map<String, String> result = new HashMap<>();
result.put( "result", "success" );
response.setContentType("application/json");
response.getWriter().write( objectMapper.writeValueAsString( result ) );
response.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -0,0 +1,25 @@
package it.fabioformosa.quartzmanager.security.auth;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}

View File

@@ -0,0 +1,91 @@
package it.fabioformosa.quartzmanager.security.auth;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
import it.fabioformosa.quartzmanager.security.TokenHelper;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
/*
* The below paths will get ignored by the filter
*/
public static final String ROOT_MATCHER = "/";
public static final String FAVICON_MATCHER = "/favicon.ico";
public static final String HTML_MATCHER = "/**/*.html";
public static final String CSS_MATCHER = "/**/*.css";
public static final String JS_MATCHER = "/**/*.js";
public static final String IMG_MATCHER = "/images/*";
public static final String LOGIN_MATCHER = "/auth/login";
public static final String LOGOUT_MATCHER = "/auth/logout";
private final Log logger = LogFactory.getLog(this.getClass());
@Autowired
TokenHelper tokenHelper;
@Autowired
UserDetailsService userDetailsService;
private List<String> pathsToSkip = Arrays.asList(
ROOT_MATCHER,
HTML_MATCHER,
FAVICON_MATCHER,
CSS_MATCHER,
JS_MATCHER,
IMG_MATCHER,
LOGIN_MATCHER,
LOGOUT_MATCHER
);
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String authToken = tokenHelper.getToken(request);
if (authToken != null && !skipPathRequest(request, pathsToSkip))
// get username from token
try {
String username = tokenHelper.getUsernameFromToken(authToken);
// get user
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication());
}
else
SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication());
chain.doFilter(request, response);
}
private boolean skipPathRequest(HttpServletRequest request, List<String> pathsToSkip ) {
Assert.notNull(pathsToSkip, "path cannot be null.");
List<RequestMatcher> m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList());
OrRequestMatcher matchers = new OrRequestMatcher(m);
return matchers.matches(request);
}
}

View File

@@ -0,0 +1,40 @@
package it.fabioformosa.quartzmanager.security.auth;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
public class TokenBasedAuthentication extends AbstractAuthenticationToken {
private String token;
private final UserDetails principle;
public TokenBasedAuthentication( UserDetails principle ) {
super( principle.getAuthorities() );
this.principle = principle;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public UserDetails getPrincipal() {
return principle;
}
public String getToken() {
return token;
}
@Override
public boolean isAuthenticated() {
return true;
}
public void setToken( String token ) {
this.token = token;
}
}

View File

@@ -0,0 +1,55 @@
package it.fabioformosa.quartzmanager.security.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.security.core.GrantedAuthority;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* Temporary enabled only inMemoryAuthentication
*
* @author Fabio.Formosa
*
*/
@Entity
@Table(name="Authority")
public class Authority implements GrantedAuthority {
@Id
@Column(name="id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name="name")
String name;
@Override
public String getAuthority() {
return name;
}
@JsonIgnore
public Long getId() {
return id;
}
@JsonIgnore
public String getName() {
return name;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,135 @@
package it.fabioformosa.quartzmanager.security.model;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* Temporary enabled only inMemoryAuthentication
*
* @author Fabio.Formosa
*
*/
@Entity
@Table(name = "USER")
public class User implements UserDetails, Serializable {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@JsonIgnore
@Column(name = "password")
private String password;
@Column(name = "firstname")
private String firstname;
@Column(name = "lastname")
private String lastname;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_authority",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
private List<Authority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public String getFirstname() {
return firstname;
}
public Long getId() {
return id;
}
public String getLastname() {
return lastname;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// We can add the below fields in the users table.
// For now, they are hardcoded.
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
public void setAuthorities(List<Authority> authorities) {
this.authorities = authorities;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setId(Long id) {
this.id = id;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@@ -0,0 +1,56 @@
package it.fabioformosa.quartzmanager.security.model;
public class UserRequest {
private Long id;
private String username;
private String password;
private String firstname;
private String lastname;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,32 @@
package it.fabioformosa.quartzmanager.security.model;
public class UserTokenState {
private String access_token;
private Long expires_in;
public UserTokenState() {
this.access_token = null;
this.expires_in = null;
}
public UserTokenState(String access_token, long expires_in) {
this.access_token = access_token;
this.expires_in = expires_in;
}
public String getAccess_token() {
return access_token;
}
public Long getExpires_in() {
return expires_in;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public void setExpires_in(Long expires_in) {
this.expires_in = expires_in;
}
}

View File

@@ -0,0 +1,9 @@
package it.fabioformosa.quartzmanager.security.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import it.fabioformosa.quartzmanager.security.model.Authority;
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
Authority findByName(String name);
}

View File

@@ -0,0 +1,10 @@
package it.fabioformosa.quartzmanager.security.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import it.fabioformosa.quartzmanager.security.model.User;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername( String username );
}

View File

@@ -0,0 +1,12 @@
package it.fabioformosa.quartzmanager.security.service;
import java.util.List;
import it.fabioformosa.quartzmanager.security.model.Authority;
public interface AuthorityService {
List<Authority> findById(Long id);
List<Authority> findByname(String name);
}

View File

@@ -0,0 +1,18 @@
package it.fabioformosa.quartzmanager.security.service;
import java.util.List;
import it.fabioformosa.quartzmanager.security.model.User;
import it.fabioformosa.quartzmanager.security.model.UserRequest;
public interface UserService {
List<User> findAll();
User findById(Long id);
User findByUsername(String username);
void resetCredentials();
User save(UserRequest user);
}

View File

@@ -0,0 +1,37 @@
package it.fabioformosa.quartzmanager.security.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import it.fabioformosa.quartzmanager.security.model.Authority;
import it.fabioformosa.quartzmanager.security.repository.AuthorityRepository;
import it.fabioformosa.quartzmanager.security.service.AuthorityService;
//@Service
public class AuthorityServiceImpl implements AuthorityService {
@Autowired
private AuthorityRepository authorityRepository;
@Override
public List<Authority> findById(Long id) {
// TODO Auto-generated method stub
Authority auth = this.authorityRepository.findOne(id);
List<Authority> auths = new ArrayList<>();
auths.add(auth);
return auths;
}
@Override
public List<Authority> findByname(String name) {
// TODO Auto-generated method stub
Authority auth = this.authorityRepository.findByName(name);
List<Authority> auths = new ArrayList<>();
auths.add(auth);
return auths;
}
}

View File

@@ -0,0 +1,66 @@
package it.fabioformosa.quartzmanager.security.service.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import it.fabioformosa.quartzmanager.security.model.User;
import it.fabioformosa.quartzmanager.security.repository.UserRepository;
//@Service
public class CustomUserDetailsService implements UserDetailsService {
protected final Log LOGGER = LogFactory.getLog(getClass());
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
public void changePassword(String oldPassword, String newPassword) {
Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
String username = currentUser.getName();
if (authenticationManager != null) {
LOGGER.debug("Re-authenticating user '"+ username + "' for password change request.");
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword));
} else {
LOGGER.debug("No authentication manager set. can't change Password!");
return;
}
LOGGER.debug("Changing password for user '"+ username + "'");
User user = (User) loadUserByUsername(username);
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null)
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
else
return user;
}
}

View File

@@ -0,0 +1,73 @@
package it.fabioformosa.quartzmanager.security.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import it.fabioformosa.quartzmanager.security.model.Authority;
import it.fabioformosa.quartzmanager.security.model.User;
import it.fabioformosa.quartzmanager.security.model.UserRequest;
import it.fabioformosa.quartzmanager.security.repository.UserRepository;
import it.fabioformosa.quartzmanager.security.service.AuthorityService;
import it.fabioformosa.quartzmanager.security.service.UserService;
//@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthorityService authService;
@Override
@PreAuthorize("hasRole('ADMIN')")
public List<User> findAll() throws AccessDeniedException {
List<User> result = userRepository.findAll();
return result;
}
@Override
@PreAuthorize("hasRole('ADMIN')")
public User findById(Long id) throws AccessDeniedException {
User u = userRepository.findOne(id);
return u;
}
@Override
// @PreAuthorize("hasRole('USER')")
public User findByUsername(String username) throws UsernameNotFoundException {
User u = userRepository.findByUsername(username);
return u;
}
@Override
public void resetCredentials() {
List<User> users = userRepository.findAll();
for (User user : users) {
user.setPassword(passwordEncoder.encode("123"));
userRepository.save(user);
}
}
@Override
public User save(UserRequest userRequest) {
User user = new User();
user.setUsername(userRequest.getUsername());
user.setPassword(passwordEncoder.encode(userRequest.getPassword()));
user.setFirstname(userRequest.getFirstname());
user.setLastname(userRequest.getLastname());
List<Authority> auth = authService.findByname("ROLE_USER");
user.setAuthorities(auth);
this.userRepository.save(user);
return user;
}
}

View File

@@ -0,0 +1,30 @@
server:
context-path: /quartz-manager
port: 8080
session.timeout : 28800
spring:
thymeleaf:
cache: false
mode: LEGACYHTML5
quartz:
enabled: true
job:
frequency: 4000
repeatCount: 19
logging:
level:
org.springframework.web: WARN
it.fabioformosa: INFO
app:
name: quartz-manager
jwt:
header: Authorization
expires_in: 600 # 10 minutes
secret: queenvictoria
cookie: AUTH-TOKEN

View File

@@ -0,0 +1,8 @@
____ _ __ __
/ __ \ | | | \/ |
| | | |_ _ __ _ _ __| |_ ____ | \ / | __ _ _ __ __ _ __ _ ___ _ __
| | | | | | |/ _` | '__| __|_ / | |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
| |__| | |_| | (_| | | | |_ / / | | | | (_| | | | | (_| | (_| | __/ |
\___\_\\__,_|\__,_|_| \__/___| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_|
__/ |
|___/

View File

@@ -0,0 +1,11 @@
-- the password hash is generated by BCrypt Calculator Generator(https://www.dailycred.com/article/bcrypt-calculator)
INSERT INTO user (id, username, password, firstname, lastname) VALUES (1, 'user', '$2a$04$Vbug2lwwJGrvUXTj6z7ff.97IzVBkrJ1XfApfGNl.Z695zqcnPYra', 'John', 'Doe');
INSERT INTO user (id, username, password, firstname, lastname) VALUES (2, 'admin', '$2a$04$Vbug2lwwJGrvUXTj6z7ff.97IzVBkrJ1XfApfGNl.Z695zqcnPYra', 'Admin', 'Admin');
INSERT INTO authority (id, name) VALUES (1, 'ROLE_USER');
INSERT INTO authority (id, name) VALUES (2, 'ROLE_ADMIN');
INSERT INTO user_authority (user_id, authority_id) VALUES (1, 1);
INSERT INTO user_authority (user_id, authority_id) VALUES (2, 1);
INSERT INTO user_authority (user_id, authority_id) VALUES (2, 2);

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%.7thread] %-5level [%-40.40logger{49}:%-3L] --- %m%n
</Pattern>
</layout>
</appender>
<logger name="it.fabioformosa" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.springframework" level="WARN" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -0,0 +1,57 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "angular-spring-starter"
},
"apps": [
{
"root": "src",
"outDir": "../server/src/main/resources/static",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json"
},
{
"project": "src/tsconfig.spec.json"
},
{
"project": "e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}

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

45
quartz-manager-frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# 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
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db
#package-lock.json
package-lock.json

View File

@@ -0,0 +1,14 @@
import { WebUiPage } from './app.po';
describe('web-ui App', () => {
let page: WebUiPage;
beforeEach(() => {
page = new WebUiPage();
});
it('should display message saying app works', () => {
page.navigateTo();
expect(page.getParagraphText()).toContain('ANGULAR-SPRING-JWT-STARTER');
});
});

View File

@@ -0,0 +1,11 @@
import { browser, element, by } from 'protractor';
export class WebUiPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root app-header span')).getText();
}
}

View File

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

View File

@@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
files: [
{ pattern: './src/test.ts', watched: false }
],
preprocessors: {
'./src/test.ts': ['@angular/cli']
},
mime: {
'text/x-typescript': ['ts','tsx']
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: config.angularCli && config.angularCli.codeCoverage
? ['progress', 'coverage-istanbul']
: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View File

@@ -0,0 +1,61 @@
{
"name": "quartz-manager-ui",
"version": "0.1.1",
"license": "MIT",
"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": "5.2.8",
"@angular/cdk": "5.2.4",
"@angular/common": "5.2.8",
"@angular/compiler": "5.2.8",
"@angular/core": "5.2.8",
"@angular/flex-layout": "5.0.0-beta.13",
"@angular/forms": "5.2.8",
"@angular/http": "5.2.8",
"@angular/material": "5.2.4",
"@angular/platform-browser": "5.2.8",
"@angular/platform-browser-dynamic": "5.2.8",
"@angular/platform-server": "5.2.8",
"@angular/router": "5.2.8",
"@fortawesome/fontawesome": "^1.1.4",
"@fortawesome/fontawesome-free-regular": "^5.0.8",
"@fortawesome/fontawesome-free-solid": "^5.0.8",
"@stomp/ng2-stompjs": "^0.6.3",
"core-js": "2.5.1",
"hammerjs": "2.0.8",
"rxjs": "5.5.2",
"stompjs": "^2.3.3",
"zone.js": "0.8.18"
},
"devDependencies": {
"@angular-devkit/core": "^0.2.0",
"@angular/cli": "1.5.3",
"@angular/compiler-cli": "5.2.8",
"@angular/language-service": "5.2.8",
"@types/hammerjs": "2.0.34",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
"@types/node": "6.0.90",
"codelyzer": "4.2.1",
"jasmine-core": "2.6.4",
"jasmine-spec-reporter": "4.1.1",
"karma": "1.7.1",
"karma-chrome-launcher": "2.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "1.3.0",
"karma-jasmine": "1.1.0",
"karma-jasmine-html-reporter": "0.2.2",
"protractor": "5.1.2",
"ts-node": "3.0.6",
"tslint": "5.7.0",
"typescript": "2.4.2"
}
}

View File

@@ -0,0 +1,30 @@
// 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: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
beforeLaunch: function() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
},
onPrepare() {
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@@ -0,0 +1,7 @@
{
"/quartz-manager": {
"target": "http://localhost:8080",
"secure": false,
"ws":true
}
}

View File

@@ -0,0 +1,55 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { LoginComponent } from './login';
import { LoginGuard } from './guard';
import { GuestGuard, AdminGuard } from './guard';
import { NotFoundComponent } from './not-found';
import { ChangePasswordComponent } from './change-password';
import { ForbiddenComponent } from './forbidden';
import { ManagerComponent } from './manager';
export const routes: Routes = [
{
path: '',
component: ManagerComponent,
canActivate: [AdminGuard],
pathMatch: 'full'
},
{
path: 'manager',
component: ManagerComponent,
canActivate: [AdminGuard],
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent,
canActivate: [GuestGuard]
},
// {
// path: 'change-password',
// component: ChangePasswordComponent,
// canActivate: [LoginGuard]
// },
{
path: '404',
component: NotFoundComponent
},
{
path: '403',
component: ForbiddenComponent
},
{
path: '**',
redirectTo: '/404'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule { }

View File

@@ -0,0 +1,5 @@
<app-header></app-header>
<div class="content">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>

View File

@@ -0,0 +1,21 @@
:host {
display: block;
color: rgba(0,0,0,.54);
font-family: Roboto,"Helvetica Neue";
}
.content {
margin: 50px 70px;
}
@media screen and (min-width: 600px) and (max-width: 1279px) {
.content {
margin: 20px 30px;
}
}
@media screen and (max-width: 599px) {
.content {
margin: 8px 12px;
}
}

View File

@@ -0,0 +1,66 @@
import { TestBed, async } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { ManagerComponent } from './manager';
import { LoginComponent } from './login';
import { MockApiService } from './service/mocks/api.service.mock';
import { LoginGuard } from './guard';
import { NotFoundComponent } from './not-found';
import {
ApiCardComponent,
FooterComponent,
GithubComponent,
} from './component';
import {
MatToolbarModule,
MatIconRegistry
} from '@angular/material';
import {
ApiService,
AuthService,
UserService,
FooService,
ConfigService
} from './service';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
FooterComponent,
],
imports: [
RouterTestingModule,
MatToolbarModule
],
providers: [
MatIconRegistry,
{
provide: ApiService,
useClass: MockApiService
},
AuthService,
UserService,
FooService,
ConfigService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
});

View File

@@ -0,0 +1,15 @@
import {Component} from '@angular/core';
// I remove temporary fontawesome5 and downgrade to fontawesome4
import fontawesome from '@fortawesome/fontawesome';
import solid from '@fortawesome/fontawesome-free-solid/';
fontawesome.library.add(solid);
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
}

View File

@@ -0,0 +1,146 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { HttpClientModule } from '@angular/common/http';
// material
import {
MatButtonModule,
MatMenuModule,
MatIconModule,
MatToolbarModule,
MatTooltipModule,
MatCardModule,
MatChipsModule,
MatInputModule,
MatIconRegistry,
MatProgressSpinnerModule,
MatProgressBarModule,
} from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from '@angular/flex-layout';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ManagerComponent } from './manager';
import { LoginComponent } from './login';
import { LoginGuard, GuestGuard, AdminGuard } from './guard';
import { NotFoundComponent } from './not-found';
import { AccountMenuComponent } from './component/header/account-menu/account-menu.component';
import {
HeaderComponent,
FooterComponent,
GithubComponent,
SchedulerConfigComponent,
SchedulerControlComponent,
LogsPanelComponent,
ProgressPanelComponent
} from './component';
import {
ApiService,
AuthService,
UserService,
FooService,
SchedulerService,
ConfigService,
ProgressWebsocketService,
LogsWebsocketService
} from './service';
import { ChangePasswordComponent } from './change-password/change-password.component';
import { ForbiddenComponent } from './forbidden/forbidden.component';
export function initUserFactory(userService: UserService) {
return () => userService.jsessionInitUser();
}
// const stompConfig: StompConfig = {
// // Which server?
// url: 'ws://localhost:8080/quartz-manager/progress',
// // Headers
// // Typical keys: login, passcode, host
// headers: {
// login: 'admin',
// passcode: 'admin'
// },
// // How often to heartbeat?
// // Interval in milliseconds, set to 0 to disable
// heartbeat_in: 0, // Typical value 0 - disabled
// heartbeat_out: 20000, // Typical value 20000 - every 20 seconds
// // Wait in milliseconds before attempting auto reconnect
// // Set to 0 to disable
// // Typical value 5000 (5 seconds)
// reconnect_delay: 5000,
// // Will log diagnostics on console
// debug: true
// };
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
ManagerComponent,
GithubComponent,
LoginComponent,
NotFoundComponent,
AccountMenuComponent,
SchedulerConfigComponent,
SchedulerControlComponent,
LogsPanelComponent,
ProgressPanelComponent,
ChangePasswordComponent,
ForbiddenComponent
],
imports: [
BrowserAnimationsModule,
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
HttpClientModule,
AppRoutingModule,
MatMenuModule,
MatTooltipModule,
MatButtonModule,
MatChipsModule,
MatIconModule,
MatInputModule,
MatToolbarModule,
MatCardModule,
MatProgressSpinnerModule,
MatProgressBarModule,
FlexLayoutModule
],
providers: [
LoginGuard,
GuestGuard,
AdminGuard,
FooService,
SchedulerService,
ProgressWebsocketService,
LogsWebsocketService,
AuthService,
ApiService,
UserService,
ConfigService,
MatIconRegistry,
{
'provide': APP_INITIALIZER,
'useFactory': initUserFactory,
'deps': [UserService],
'multi': true
}
// StompService,
// ServerSocket
// {
// provide: StompConfig,
// useValue: stompConfig
// }
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,18 @@
<div class="content" fxLayout="row" fxLayoutAlign="center">
<mat-card elevation="5" fxFlex>
<mat-card-subtitle>Change Your Password</mat-card-subtitle>
<p [class]="notification.msgType" *ngIf="notification">{{notification.msgBody}}</p>
<mat-card-content>
<form *ngIf="!submitted" [formGroup]="form" (ngSubmit)="onSubmit()" #changePasswordForm="ngForm">
<mat-input-container>
<input matInput formControlName="oldPassword" required type="password" placeholder="old password">
</mat-input-container>
<mat-input-container>
<input matInput formControlName="newPassword" required type="password" placeholder="new password">
</mat-input-container>
<button type="submit" [disabled]="!changePasswordForm.form.valid" mat-raised-button color="primary">Change Password</button>
</form>
<mat-spinner *ngIf="submitted" mode="indeterminate"></mat-spinner>
</mat-card-content>
</mat-card>
</div>

View File

@@ -0,0 +1,45 @@
.content {
width: 100%;
}
mat-card {
max-width: 350px;
text-align: center;
animation: fadein 1s;
-o-animation: fadein 1s; /* Opera */
-moz-animation: fadein 1s; /* Firefox */
-webkit-animation: fadein 1s; /* Safari and Chrome */
}
mat-input-container {
width: 100%;
}
mat-spinner {
width: 25px;
height: 25px;
margin: 20px auto 0 auto;
}
.error {
color: #D50000;
}
.success {
color: #8BC34A;
}
@media screen and (max-width: 599px) {
.content {
/* https://github.com/angular/flex-layout/issues/295 */
display: block !important;
}
mat-card {
/* https://github.com/angular/flex-layout/issues/295 */
display: block !important;
max-width: 999px;
}
}

View File

@@ -0,0 +1,52 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import {
ApiService,
AuthService,
UserService,
ConfigService
} from '../service';
import { MockApiService } from '../service/mocks';
import { ChangePasswordComponent } from './change-password.component';
describe('ChangePasswordComponent', () => {
let component: ChangePasswordComponent;
let fixture: ComponentFixture<ChangePasswordComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
ChangePasswordComponent
],
providers: [
{
provide: ApiService,
useClass: MockApiService
},
AuthService,
UserService,
ConfigService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChangePasswordComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,65 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from 'app/service';
import { Router } from '@angular/router';
import { DisplayMessage } from '../shared/models/display-message';
@Component({
selector: 'app-change-password',
templateUrl: './change-password.component.html',
styleUrls: ['./change-password.component.scss']
})
export class ChangePasswordComponent implements OnInit {
form: FormGroup;
/**
* Boolean used in telling the UI
* that the form has been submitted
* and is awaiting a response
*/
submitted = false;
/**
* Diagnostic message from received
* form request error
*/
notification: DisplayMessage;
constructor(
private authService: AuthService,
private router: Router,
private formBuilder: FormBuilder
) {
}
ngOnInit() {
this.form = this.formBuilder.group({
oldPassword: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(64)])],
newPassword: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(32)])]
});
}
onSubmit() {
/**
* Innocent until proven guilty
*/
this.notification = undefined;
this.submitted = true;
this.authService.changePassowrd(this.form.value)
// show me the animation
.delay(1000)
.mergeMap(() => this.authService.logout())
.subscribe(() => {
this.router.navigate(['/login', { msgType: 'success', msgBody: 'Success! Please sign in with your new password.'}]);
}, error => {
this.submitted = false;
this.notification = { msgType: 'error', msgBody: 'Invalid old password.'};
});
}
}

View File

@@ -0,0 +1 @@
export * from './change-password.component';

View File

@@ -0,0 +1,7 @@
<p style="margin: 0px auto; padding: 0px; color: rgba(255, 255, 255, 0.541176); max-width: 356px;">
Hand crafted with love by
<a href="https://github.com/fabioformosa" style="color: rgba(255, 255, 255, 0.870588);">Fabio Formosa</a>
</p>
<a style="margin-top: 22px;" mat-icon-button href="https://github.com/fabioformosa/quartz-manager">
<img src="assets/image/github.png"/>
</a>

View File

@@ -0,0 +1,27 @@
:host {
display: block;
font-weight: 300;
font-size: 15px;
display: block;
background-color: rgb(33, 33, 33);
height: 236px;
padding: 72px 24px;
box-sizing: border-box;
text-align: center;
a {
text-decoration: none;
cursor: auto;
color: #FFFFFF;
margin-top: 32px;
}
h3 {
margin: 0px;
padding: 0px;
font-weight: 300;
font-size: 22px;
}
}

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 @@
export * from './footer.component';

View File

@@ -0,0 +1,4 @@
<h3>Want to help make this project? Check out repo.</h3>
<a href="https://github.com/fabioformosa/quartz-manager" color="accent" mat-raised-button mat-ripple>
<span>GITHUB</span>
</a>

View File

@@ -0,0 +1,20 @@
:host {
display: block;
height: 236px;
padding: 72px 24px;
box-sizing: border-box;
background-color: rgb(238, 238, 238);
text-align: center
}
:host h3 {
margin: 0px;
padding: 0px;
font-weight: 300;
font-size: 22px;
}
:host a {
color: #000;
margin-top: 32px;
}

View File

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

View File

@@ -0,0 +1 @@
export * from './github.component';

View File

@@ -0,0 +1,2 @@
<button mat-menu-item [routerLink]="['/change-password']">CHANGE PASSWORD</button>
<button mat-menu-item (click)="logout()">SIGN OUT</button>

View File

@@ -0,0 +1,53 @@
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import {
AuthService,
ConfigService,
ApiService,
UserService
} from '../../../service';
import {
MockUserService,
MockApiService
} from '../../../service/mocks';
import { AccountMenuComponent } from './account-menu.component';
describe('AccountMenuComponent', () => {
let component: AccountMenuComponent;
let fixture: ComponentFixture<AccountMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
providers: [
{
provide: UserService,
useClass: MockUserService
},
{
provide: ApiService,
useClass: MockApiService
},
AuthService,
ConfigService
],
declarations: [AccountMenuComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AccountMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { Component, OnInit, Input } from '@angular/core';
import {
ConfigService,
AuthService,
UserService
} from '../../../service';
import { Router } from '@angular/router';
@Component({
selector: 'app-account-menu',
templateUrl: './account-menu.component.html',
styleUrls: ['./account-menu.component.scss']
})
export class AccountMenuComponent implements OnInit {
// TODO define user interface
user: any;
constructor(
private config: ConfigService,
private authService: AuthService,
private router: Router,
private userService: UserService
) {}
ngOnInit() {
this.user = this.userService.currentUser;
}
logout() {
this.authService.logout().subscribe(res => {
this.router.navigate(['/login']);
});
}
}

View File

@@ -0,0 +1,40 @@
<mat-toolbar color="primary" class="app-navbar">
<button mat-button mat-ripple routerLink="/">
<img alt="Quartz Manager" class="app-angular-logo" src="assets/image/angular-white-transparent.svg">
<span>Quartz Manager</span>
</button>
<div class="right">
<div fxFlex="1 1 auto" fxLayout="row" fxLayoutAlign="flex-end center">
<button *ngIf="!hasSignedIn()" routerLink="/signup" mat-button mat-ripple>
<span>Sign up</span>
</button>
<button *ngIf="!hasSignedIn()" routerLink="/login" mat-button mat-ripple>
<span>Login</span>
</button>
<button
class="greeting-button"
*ngIf="hasSignedIn()"
mat-button mat-ripple
[matMenuTriggerFor]="accountMenu">
<span>Hi, {{userName()}}</span>
</button>
<button
class="greeting-hamburger"
*ngIf="hasSignedIn()"
mat-icon-button mat-ripple
[matMenuTriggerFor]="accountMenu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #accountMenu
class="app-header-accountMenu"
yposition="below"
[overlapTrigger]="false">
<app-account-menu ></app-account-menu>
</mat-menu>
</div>
</div>
</mat-toolbar>

View File

@@ -0,0 +1,59 @@
:host {
position: relative;
z-index: 10;
color: #fff;
}
// The menu popup is rendered outside the header component
// so we will restyle a couple things inside a global /deep/ selector
.app-navbar {
width: 100%;
display: flex;
flex-wrap: wrap;
.right {
margin-left: auto;
float: right;
}
}
.app-navbar span {
text-transform: uppercase !important;
}
.app-angular-logo {
margin: 0 4px 3px 0;
height: 26px;
}
.greeting-hamburger {
display: none;
}
/deep/ {
.app-header-accountMenu.mat-menu-panel {
border-radius: 3px;
max-width: initial;
overflow: visible;
.mat-menu-content {
max-width: initial;
padding: 0;
overflow: hidden;
display: inline-block;
margin-bottom: -6px;
}
}
}
@media screen and (max-width: 600px) {
.greeting-hamburger {
display: block;
}
.greeting-button {
display: none;
}
}

View File

@@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import {
UserService,
AuthService
} from '../../service';
import { Router } from '@angular/router';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
constructor(
private userService: UserService,
private authService: AuthService,
private router: Router
) { }
ngOnInit() {
}
logout() {
this.authService.logout().subscribe(res => {
this.router.navigate(['/login']);
});
}
hasSignedIn() {
return !!this.userService.currentUser;
}
userName() {
const user = this.userService.currentUser;
return user.username;
}
}

View File

@@ -0,0 +1 @@
export * from './header.component';

View File

@@ -0,0 +1,7 @@
export * from './header';
export * from './github';
export * from './footer';
export * from './logs-panel';
export * from './scheduler-config';
export * from './scheduler-control';
export * from './progress-panel';

View File

@@ -0,0 +1 @@
export * from './logs-panel.component';

View File

@@ -0,0 +1,29 @@
<mat-card>
<mat-card-header>
<mat-card-title><b>JOB LOGS</b></mat-card-title>
</mat-card-header>
<mat-card-content>
<div id="logs">
<div *ngFor = "let log of logs" fxLayout="row" fxLayout.xs="column">
<div fxFlex="1 1 20%">
<span [ngClass]="{'animated zoomIn firstLog': $first}"> [{{log.time|date:'medium'}}]</span>
</div>
<div fxFlex="1 1 10%">
<span [ngClass]="{'animated zoomIn firstLog': $first}">
<i class = "fas" [ngClass]="{'fa-check-circle green': log.type == 'INFO',
'fa-exclamation-triangle yellow': log.type == 'WARN',
'fa-times-circle red': log.type == 'ERROR'}"></i>
</span>
</div>
<div fxFlex="1 1 20%">
<span [ngClass]="{'animated zoomIn firstLog': $first}">
{{log.threadName}}
</span>
</div>
<div fxFlex="1 1 50%">
<span [ngClass]="{'animated zoomIn firstLog': $first}"> {{log.msg}}</span>
</div>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,10 @@
.red{
color: red;
}
.green{
color: green;
}
.yellow{
color: gold;
}

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