├── README.md ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── querydsl.gradle ├── src ├── main │ ├── java │ │ └── info │ │ │ └── thecodinglive │ │ │ └── member │ │ │ ├── model │ │ │ ├── MemberRole.java │ │ │ ├── Member.java │ │ │ ├── AbstractEntityModel.java │ │ │ └── RememberMeToken.java │ │ │ ├── service │ │ │ ├── MemberService.java │ │ │ ├── MemberServiceImpl.java │ │ │ ├── MemberAccessDeniedHandler.java │ │ │ ├── MyUserService.java │ │ │ └── RememberMeTokenService.java │ │ │ ├── repository │ │ │ ├── MemberRepository.java │ │ │ └── RememberMeTokenRepository.java │ │ │ ├── MemberApp.java │ │ │ ├── support │ │ │ ├── PasswordEncoderUtil.java │ │ │ └── MemberValidator.java │ │ │ ├── config │ │ │ ├── AuditConfig.java │ │ │ └── SecurityConfig.java │ │ │ └── controller │ │ │ └── MemberController.java │ └── resources │ │ ├── templates │ │ ├── admin.html │ │ ├── fragments │ │ │ ├── footer.html │ │ │ ├── config.html │ │ │ └── header.html │ │ ├── layout │ │ │ └── base.html │ │ ├── welcome.html │ │ ├── home.html │ │ ├── error │ │ │ └── 403.html │ │ ├── login.html │ │ └── signup.html │ │ ├── sql │ │ └── data-H2.sql │ │ ├── validation.properties │ │ ├── application.properties │ │ └── static │ │ └── css │ │ ├── home.css │ │ └── normalize.css └── test │ └── java │ └── PasswordEncodeTest.java ├── settings.gradle ├── .gitignore ├── gradlew.bat └── gradlew /README.md: -------------------------------------------------------------------------------- 1 | 헤로쿠로 배포 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodinglive/memberApp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/model/MemberRole.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.model; 2 | 3 | 4 | public enum MemberRole { 5 | USER, //0 6 | ADMIN //1 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | admin 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 21 16:33:40 KST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https://services.gradle.org/distributions/gradle-4.1-bin.zip 7 | -------------------------------------------------------------------------------- /src/main/resources/sql/data-H2.sql: -------------------------------------------------------------------------------- 1 | insert into tbl_member (email, password, password_confirm, username, role_name) VALUES ('jins@info.com', '$2a$10$PjLG8Nr50vGQXKR63.CdMeQvvf.ZAy0ZX3BJXZnJg3FmODO9ysmt2', 2 | '$2a$10$PjLG8Nr50vGQXKR63.CdMeQvvf.ZAy0ZX3BJXZnJg3FmODO9ysmt2', '윤석진', 'ADMIN'); 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/service/MemberService.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.service; 2 | 3 | import info.thecodinglive.member.model.Member; 4 | 5 | public interface MemberService { 6 | void save(Member member); 7 | Member findByUserName(String username); 8 | Member findByUserEmail(String email); 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/PasswordEncodeTest.java: -------------------------------------------------------------------------------- 1 | import info.thecodinglive.member.support.PasswordEncoderUtil; 2 | import org.junit.Test; 3 | 4 | /** 5 | * Created by ysj on 2018-03-11. 6 | */ 7 | public class PasswordEncodeTest { 8 | 9 | final String pwd = "qwer1234!"; 10 | 11 | @Test 12 | public void getBcrpr(){ 13 | PasswordEncoderUtil pwdu = new PasswordEncoderUtil(); 14 | System.out.println(pwdu.encode(pwd)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/repository/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import info.thecodinglive.member.model.Member; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface MemberRepository extends JpaRepository{ 10 | Member findByEmail(String email); 11 | Member findByUsername(String username); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/repository/RememberMeTokenRepository.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.repository; 2 | 3 | import info.thecodinglive.member.model.RememberMeToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface RememberMeTokenRepository extends JpaRepository{ 11 | RememberMeToken findBySeries(String series); 12 | List findByUsername(String username); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This settings file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * In a single project build this file can be empty or even removed. 6 | * 7 | * Detailed information about configuring a multi-project build in Gradle can be found 8 | * in the user guide at https://docs.gradle.org/3.5/userguide/multi_project_builds.html 9 | */ 10 | 11 | /* 12 | // To declare projects as part of a multi-project build use the 'include' method 13 | include 'shared' 14 | include 'api' 15 | include 'services:webservice' 16 | */ 17 | 18 | rootProject.name = 'bootBoard' 19 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/MemberApp.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 7 | 8 | @EntityScan( 9 | basePackageClasses = {Jsr310JpaConverters.class}, 10 | basePackages = {"info.thecodinglive.member.model"}) 11 | @SpringBootApplication 12 | public class MemberApp { 13 | public static void main(String ar[]){ 14 | SpringApplication.run(MemberApp.class, ar); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 |

home

10 | userName 11 | role: 12 | sign out 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/validation.properties: -------------------------------------------------------------------------------- 1 | NotEmpty=\uD544\uC218\uAC12\uC785\uB2C8\uB2E4. 2 | Size.userForm.username=\uC774\uB984\uC740 3\uAE00\uC790 \uC774\uC0C1 32\uC790 \uBBF8\uB9CC\uC73C\uB85C \uC785\uB825\uD558\uC138\uC694 3 | Duplicate.userForm.username=\uC911\uBCF5\uB41C \uC774\uB984\uC785\uB2C8\uB2E4. 4 | Size.userForm.password=\uD328\uC2A4\uC6CC\uB4DC\uB294 7\uAE00\uC790 \uC774\uC0C1 32\uC790 \uBBF8\uB9CC\uC73C\uB85C \uC785\uB825\uD558\uC138\uC694 5 | Diff.userForm.passwordConfirm=\uD328\uC2A4\uC6CC\uB4DC\uAC00 \uD655\uC778\uC6A9 \uD328\uC2A4\uC6CC\uB4DC\uC640 \uC11C\uB85C \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. 6 | Duplicate.userForm.email=\uC774\uBBF8 \uC0AC\uC6A9\uC911\uC778 \uC774\uBA54\uC77C\uC785\uB2C8\uB2E4. -------------------------------------------------------------------------------- /src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |

SpringBoot Tutorial

25 |

Jpub Java webService

26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:testdb;CACHE_SIZE=10240;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=15000;MODE=MySQL; 2 | spring.datasource.platform=h2 3 | spring.datasource.separator=; 4 | spring.datasource.username=sa 5 | spring.datasource.driver-class-name=org.h2.Driver 6 | spring.datasource.sql-script-encoding=UTF-8 7 | spring.jpa.database-platform=H2 8 | spring.jpa.hibernate.ddl-auto=update 9 | spring.datasource.data=classpath:sql/data-H2.sql 10 | spring.jpa.show-sql=true 11 | 12 | spring.thymeleaf.cache=false 13 | spring.thymeleaf.mode=LEGACYHTML5 14 | 15 | spring.devtools.livereload.enabled=true 16 | spring.messages.basename=validation 17 | spring.messages.encoding=UTF-8 18 | 19 | logging.level.org.springframework.security = debug -------------------------------------------------------------------------------- /gradle/querydsl.gradle: -------------------------------------------------------------------------------- 1 | def querydslOutput = file('src/main/generated') 2 | 3 | task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') { 4 | file(new File(projectDir, "/src/main/generated")).deleteDir() 5 | file(new File(projectDir, "/src/main/generated")).mkdirs() 6 | source = sourceSets.main.java 7 | classpath = configurations.compile + configurations.compileOnly 8 | options.compilerArgs = [ 9 | "-proc:only", 10 | "-processor", 'com.querydsl.apt.jpa.JPAAnnotationProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor' 11 | ] 12 | destinationDir = file('src/main/generated') 13 | } 14 | 15 | compileJava { 16 | dependsOn generateQueryDSL 17 | } 18 | 19 | clean.doFirst{ 20 | delete querydslOutput 21 | } -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/support/PasswordEncoderUtil.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.support; 2 | 3 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 4 | import org.springframework.security.crypto.password.PasswordEncoder; 5 | 6 | public class PasswordEncoderUtil implements PasswordEncoder { 7 | private PasswordEncoder passwordEncoder; 8 | 9 | public PasswordEncoderUtil() { 10 | this.passwordEncoder = new BCryptPasswordEncoder(); 11 | } 12 | 13 | public PasswordEncoderUtil(PasswordEncoder passwordEncoder) { 14 | this.passwordEncoder = passwordEncoder; 15 | } 16 | 17 | @Override 18 | public String encode(CharSequence rawPassword) { 19 | return passwordEncoder.encode(rawPassword); 20 | } 21 | 22 | @Override 23 | public boolean matches(CharSequence rawPassword, String encodedPassword) { 24 | return passwordEncoder.matches(rawPassword, encodedPassword); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/model/Member.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.model; 2 | 3 | import javax.jdo.annotations.Unique; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.EnumType; 7 | import javax.persistence.Enumerated; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | 13 | import lombok.Getter; 14 | import lombok.Setter; 15 | import lombok.ToString; 16 | 17 | @Getter 18 | @Setter 19 | @Entity 20 | @Table(name = "tbl_member") 21 | @ToString 22 | public class Member extends AbstractEntityModel{ 23 | 24 | @Id 25 | @GeneratedValue(strategy=GenerationType.IDENTITY) 26 | private long id; 27 | private String username; 28 | 29 | private String password; 30 | private String passwordConfirm; 31 | @Unique 32 | private String email; 33 | 34 | @Column(name = "role_name") 35 | @Enumerated(EnumType.STRING) 36 | private MemberRole role; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/service/MemberServiceImpl.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.service; 2 | 3 | import info.thecodinglive.member.repository.MemberRepository; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import org.springframework.stereotype.Service; 8 | 9 | import info.thecodinglive.member.model.Member; 10 | 11 | @Service("MemberService") 12 | @Slf4j 13 | public class MemberServiceImpl implements MemberService{ 14 | @Autowired 15 | private MemberRepository memberRepository; 16 | @Autowired 17 | private BCryptPasswordEncoder bCryptPasswordEncoder; 18 | 19 | @Override 20 | public Member findByUserEmail(String email) { 21 | return memberRepository.findByEmail(email); 22 | } 23 | 24 | @Override 25 | public Member findByUserName(String username) { 26 | return memberRepository.findByUsername(username); 27 | } 28 | 29 | @Override 30 | public void save(Member member) { 31 | member.setPassword(bCryptPasswordEncoder.encode(member.getPassword())); 32 | member.setPasswordConfirm(bCryptPasswordEncoder.encode(member.getPasswordConfirm())); 33 | memberRepository.save(member); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/model/AbstractEntityModel.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.model; 2 | 3 | import java.io.Serializable; 4 | import java.time.LocalDateTime; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.EntityListeners; 8 | import javax.persistence.MappedSuperclass; 9 | 10 | import org.springframework.data.annotation.CreatedBy; 11 | import org.springframework.data.annotation.CreatedDate; 12 | import org.springframework.data.annotation.LastModifiedBy; 13 | import org.springframework.data.annotation.LastModifiedDate; 14 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 15 | 16 | @EntityListeners(AuditingEntityListener.class) 17 | @MappedSuperclass 18 | public abstract class AbstractEntityModel implements Serializable{ 19 | @CreatedDate 20 | @Column(name = "reg_date", updatable = false) 21 | private LocalDateTime createdDateTime; 22 | 23 | @LastModifiedDate 24 | @Column(name = "update_date", updatable = true) 25 | private LocalDateTime lastModifiedDateTime; 26 | 27 | @Column(name = "created_by_user", nullable = false) 28 | @CreatedBy 29 | private String createdByUser; 30 | 31 | @Column(name = "modified_by_user", nullable = false) 32 | @LastModifiedBy 33 | private String modifiedByUser; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/model/RememberMeToken.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.model; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | import java.io.Serializable; 13 | import java.util.Date; 14 | 15 | @Entity 16 | @Table(name = "persistent_logins") 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | public class RememberMeToken implements Serializable{ 21 | @Id 22 | @Column(name = "SERIES") 23 | private String series; 24 | 25 | @Column(name = "USERNAME", nullable = false) 26 | private String username; 27 | 28 | @Column(name = "TOKEN", nullable = false) 29 | private String token; 30 | 31 | @Column(name = "LAST_USED", nullable = false) 32 | private Date lastUsed; 33 | 34 | public RememberMeToken(PersistentRememberMeToken token) { 35 | this.series = token.getSeries(); 36 | this.username = token.getUsername(); 37 | this.token = token.getTokenValue(); 38 | this.lastUsed = token.getDate(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/service/MemberAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.security.access.AccessDeniedException; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.security.web.access.AccessDeniedHandler; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.servlet.ServletException; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | @Slf4j 16 | @Component 17 | public class MemberAccessDeniedHandler implements AccessDeniedHandler { 18 | @Override 19 | public void handle(HttpServletRequest req, HttpServletResponse res, AccessDeniedException accessDeniedException) throws IOException, ServletException { 20 | Authentication auth 21 | = SecurityContextHolder.getContext().getAuthentication(); 22 | 23 | if (auth != null) { 24 | log.info("User '" + auth.getName() 25 | + "' attempted to access the protected URL: " 26 | + req.getRequestURI()); 27 | } 28 | 29 | res.sendRedirect(req.getContextPath() + "/403"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/config/AuditConfig.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.domain.AuditorAware; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | 11 | @Configuration 12 | @EnableJpaAuditing 13 | public class AuditConfig { 14 | @Bean 15 | public AuditorAware createAuditorProvider() { 16 | return new SecurityAuditor(); 17 | } 18 | 19 | @Bean 20 | public AuditingEntityListener createAuditingListener() { 21 | return new AuditingEntityListener(); 22 | } 23 | 24 | public static class SecurityAuditor implements AuditorAware { 25 | @Override 26 | public String getCurrentAuditor() { 27 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 28 | if (null == auth || !auth.isAuthenticated()) { 29 | return null; 30 | } 31 | String username = auth.getName(); 32 | return username; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/static/css/home.css: -------------------------------------------------------------------------------- 1 | 2 | .intro-2 { 3 | background: url("https://mdbootstrap.com/img/Photos/Others/architecture.jpg")no-repeat center center; 4 | background-size: cover; 5 | } 6 | .btn .fa { 7 | margin-left: 3px; 8 | } 9 | .top-nav-collapse { 10 | background-color: #424f95 !important; 11 | } 12 | .navbar:not(.top-nav-collapse) { 13 | background: transparent !important; 14 | } 15 | @media (max-width: 768px) { 16 | .navbar:not(.top-nav-collapse) { 17 | background: #424f95 !important; 18 | } 19 | } 20 | h6 { 21 | line-height: 1.7; 22 | } 23 | .hm-gradient .full-bg-img { 24 | background: -moz-linear-gradient(45deg, rgba(42, 27, 161, 0.6), rgba(29, 210, 177, 0.6) 100%); 25 | background: -webkit-linear-gradient(45deg, rgba(42, 27, 161, 0.6), rgba(29, 210, 177, 0.6) 100%); 26 | background: -webkit-gradient(linear, 45deg, from(rgba(42, 27, 161, 0.6)), to(rgba(29, 210, 177, 0.6))); 27 | background: -o-linear-gradient(45deg, rgba(42, 27, 161, 0.6), rgba(29, 210, 177, 0.6) 100%); 28 | background: linear-gradient(to 45deg, rgba(42, 27, 161, 0.6), rgba(29, 210, 177, 0.6) 100%); 29 | } 30 | @media (max-width: 450px) { 31 | .margins { 32 | margin-right: 1rem; 33 | margin-left: 1rem; 34 | } 35 | } 36 | @media (max-width: 740px) { 37 | .full-height, 38 | .full-height body, 39 | .full-height header, 40 | .full-height header .view { 41 | height: 1040px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/config.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | jpub book example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/support/MemberValidator.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.support; 2 | 3 | import info.thecodinglive.member.model.Member; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.validation.Errors; 7 | import org.springframework.validation.ValidationUtils; 8 | import org.springframework.validation.Validator; 9 | 10 | import info.thecodinglive.member.service.MemberService; 11 | 12 | @Component 13 | public class MemberValidator implements Validator{ 14 | @Autowired 15 | private MemberService memberService; 16 | 17 | @Override 18 | public boolean supports(Class clazz) { 19 | return Member.class.equals(clazz); 20 | } 21 | 22 | @Override 23 | public void validate(Object target, Errors errors) { 24 | Member member = (Member)target; 25 | 26 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "NotEmpty"); 27 | if (member.getUsername().length() < 3 || member.getUsername().length() > 32) { 28 | errors.rejectValue("username", "Size.userForm.username"); 29 | } 30 | 31 | if (memberService.findByUserName(member.getUsername()) != null) { 32 | errors.rejectValue("username", "Duplicate.userForm.username"); 33 | } 34 | 35 | if (memberService.findByUserEmail(member.getEmail()) != null) { 36 | errors.rejectValue("email", "Duplicate.userForm.email"); 37 | } 38 | 39 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "NotEmpty"); 40 | if (member.getPassword().length() < 7 || member.getPassword().length() > 32) { 41 | errors.rejectValue("password", "Size.userForm.password"); 42 | } 43 | 44 | if (!member.getPasswordConfirm().equals(member.getPassword())) { 45 | errors.rejectValue("passwordConfirm", "Diff.userForm.passwordConfirm"); 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spring Boot and Thymeleaf - 403 Error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 |

해당 페이지에 접근할 권한이 없습니다.

23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/service/MyUserService.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.service; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import info.thecodinglive.member.model.Member; 7 | import info.thecodinglive.member.repository.MemberRepository; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 12 | import org.springframework.security.core.userdetails.User; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.core.userdetails.UserDetailsService; 15 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | import javax.annotation.PostConstruct; 20 | 21 | @Slf4j 22 | @Service 23 | public class MyUserService implements UserDetailsService{ 24 | @Autowired 25 | MemberRepository memberRepository; 26 | 27 | private static final String ROLE_PREFIX = "ROLE_"; 28 | 29 | @PostConstruct 30 | private void created(){ 31 | log.debug("체크 로그인"); 32 | } 33 | 34 | @Override 35 | @Transactional(readOnly = true) 36 | public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { 37 | log.info("loadUserByUsername user id {} ", email); 38 | 39 | Member member = memberRepository.findByEmail(email); 40 | log.debug("member", member.toString()); 41 | Set grantedAuthorities = new HashSet<>(); 42 | 43 | grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + member.getRole().name())); 44 | log.debug("권한체크:", grantedAuthorities.toString()); 45 | return new User(member.getUsername(), member.getPassword(), grantedAuthorities); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/service/RememberMeTokenService.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.service; 2 | 3 | import info.thecodinglive.member.model.RememberMeToken; 4 | import info.thecodinglive.member.repository.RememberMeTokenRepository; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; 8 | 9 | import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.transaction.Transactional; 13 | import java.util.Date; 14 | import java.util.List; 15 | 16 | @Service 17 | @Transactional 18 | @Slf4j 19 | public class RememberMeTokenService implements PersistentTokenRepository { 20 | 21 | @Autowired 22 | private RememberMeTokenRepository rememberMeTokenRepository; 23 | 24 | @Override 25 | public void createNewToken(PersistentRememberMeToken token) { 26 | RememberMeToken newToken = new RememberMeToken(token); 27 | this.rememberMeTokenRepository.save(newToken); 28 | } 29 | 30 | @Override 31 | public void updateToken(String series, String tokenValue, Date lastUsed) { 32 | RememberMeToken token = this.rememberMeTokenRepository.findBySeries(series); 33 | if (token != null){ 34 | token.setToken(tokenValue); 35 | token.setLastUsed(lastUsed); 36 | this.rememberMeTokenRepository.save(token); 37 | } 38 | } 39 | 40 | @Override 41 | public PersistentRememberMeToken getTokenForSeries(String seriesId) { 42 | RememberMeToken token = this.rememberMeTokenRepository.findBySeries(seriesId); 43 | if(token != null){ 44 | return new PersistentRememberMeToken(token.getToken(), token.getSeries(), token.getToken(), token.getLastUsed()); 45 | } 46 | return null; 47 | } 48 | 49 | @Override 50 | public void removeUserTokens(String username) { 51 | List tokens = this.rememberMeTokenRepository.findByUsername(username); 52 | if(tokens != null){ 53 | this.rememberMeTokenRepository.delete(tokens); 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | *.class 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | ### JetBrains template 15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 16 | 17 | 18 | 19 | ## Directory-based project format: 20 | .idea/ 21 | # if you remove the above rule, at least ignore the following: 22 | 23 | # User-specific stuff: 24 | # .idea/workspace.xml 25 | # .idea/tasks.xml 26 | # .idea/dictionaries 27 | 28 | # Sensitive or high-churn files: 29 | # .idea/dataSources.ids 30 | # .idea/dataSources.xml 31 | # .idea/sqlDataSources.xml 32 | # .idea/dynamic.xml 33 | # .idea/uiDesigner.xml 34 | 35 | # Gradle: 36 | # .idea/gradle.xml 37 | # .idea/libraries 38 | 39 | # Mongo Explorer plugin: 40 | # .idea/mongoSettings.xml 41 | 42 | ## File-based project format: 43 | 44 | *.iws 45 | 46 | ## Plugin-specific files: 47 | 48 | # IntelliJ 49 | /out/ 50 | 51 | # mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # JIRA plugin 55 | atlassian-ide-plugin.xml 56 | 57 | # Crashlytics plugin (for Android Studio and IntelliJ) 58 | com_crashlytics_export_strings.xml 59 | crashlytics.properties 60 | crashlytics-build.properties 61 | ### Gradle template 62 | .gradle 63 | build/ 64 | 65 | # Ignore Gradle GUI config 66 | gradle-app.setting 67 | 68 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 69 | !gradle-wrapper.jar 70 | ### Eclipse template 71 | *.pydevproject 72 | .metadata 73 | bin/ 74 | tmp/ 75 | *.tmp 76 | *.bak 77 | *.swp 78 | *~.nib 79 | local.properties 80 | .settings/ 81 | .loadpath 82 | 83 | # Eclipse Core 84 | .project 85 | 86 | # External tool builders 87 | .externalToolBuilders/ 88 | 89 | # Locally stored "Eclipse launch configurations" 90 | *.launch 91 | 92 | # CDT-specific 93 | .cproject 94 | 95 | # JDT-specific (Eclipse Java Development Tools) 96 | .classpath 97 | 98 | # Java annotation processor (APT) 99 | .factorypath 100 | 101 | # PDT-specific 102 | .buildpath 103 | 104 | # sbteclipse plugin 105 | .target 106 | 107 | # TeXlipse plugin 108 | .texlipse 109 | 110 | # Created by .ignore support plugin (hsz.mobi) 111 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/controller/MemberController.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.controller; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.validation.BindingResult; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | import info.thecodinglive.member.model.Member; 12 | import info.thecodinglive.member.model.MemberRole; 13 | import info.thecodinglive.member.service.MemberService; 14 | import info.thecodinglive.member.support.MemberValidator; 15 | 16 | import javax.validation.Valid; 17 | 18 | @Slf4j 19 | @Controller 20 | public class MemberController { 21 | @Autowired 22 | private MemberService memberService; 23 | @Autowired 24 | private MemberValidator memberValidator; 25 | 26 | @RequestMapping(value = "/signup", method = RequestMethod.GET) 27 | public String registration(Model model) { 28 | model.addAttribute("userForm", new Member()); 29 | 30 | return "signup"; 31 | } 32 | 33 | @RequestMapping(value = "/signup", method = RequestMethod.POST) 34 | public String registration(@ModelAttribute("userForm") @Valid Member userForm, BindingResult bindingResult) { 35 | 36 | memberValidator.validate(userForm, bindingResult); 37 | 38 | if (bindingResult.hasErrors()) { 39 | log.debug("valid error"); 40 | return "signup"; 41 | } 42 | 43 | userForm.setRole(MemberRole.USER); 44 | memberService.save(userForm); 45 | log.debug("userInfo" + userForm.toString()); 46 | log.debug("email" + userForm.getEmail() + "|" + userForm.getPassword()); 47 | 48 | return "redirect:/welcome"; 49 | } 50 | 51 | @RequestMapping(value = "/login", method = RequestMethod.GET) 52 | public ModelAndView login(Model model, String error, String logout) { 53 | if (error != null) 54 | model.addAttribute("error", "아이디 또는 패스워드가 잘못 되었습니다."); 55 | 56 | if (logout != null) 57 | model.addAttribute("message", "로그아웃 되었습니다."); 58 | 59 | return new ModelAndView("login"); 60 | } 61 | 62 | 63 | @RequestMapping(value = {"/"}, method = RequestMethod.GET) 64 | public String home(Model model) { 65 | return "home"; 66 | } 67 | 68 | @GetMapping("/403") 69 | public String error403() { 70 | return "/error/403"; 71 | } 72 | 73 | @GetMapping("/welcome") 74 | public String welcome(){ 75 | return "welcome"; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sb 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 60 |
61 |
62 |
63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/info/thecodinglive/member/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package info.thecodinglive.member.config; 2 | 3 | import info.thecodinglive.member.service.MemberAccessDeniedHandler; 4 | import info.thecodinglive.member.service.MyUserService; 5 | import info.thecodinglive.member.service.RememberMeTokenService; 6 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 10 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 14 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | 17 | import org.springframework.beans.factory.annotation.Qualifier; 18 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 19 | 20 | import javax.servlet.DispatcherType; 21 | import javax.servlet.Filter; 22 | import java.util.EnumSet; 23 | 24 | @Configuration 25 | @EnableWebSecurity 26 | @EnableGlobalMethodSecurity(prePostEnabled = true) 27 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 28 | @Override 29 | protected void configure(HttpSecurity http) throws Exception { 30 | http 31 | .authorizeRequests() 32 | //접근 허용 33 | .antMatchers("/css/**", "/js/**", "/images/**", "/resources/**", "/h2-console/**", "/webjars/**", "/signup", "/home", "/").permitAll() 34 | .anyRequest().authenticated() 35 | .and() 36 | //iframe 허용 37 | .headers(). 38 | frameOptions().disable() 39 | .and() 40 | //DB 어드민 접속 허용 41 | .csrf() 42 | .ignoringAntMatchers("/h2-console/**") 43 | .and() 44 | //로그인 45 | .formLogin() 46 | .loginPage("/login") 47 | .loginProcessingUrl("/sign-in") 48 | .defaultSuccessUrl("/welcome") 49 | .failureUrl("/login?error") 50 | .usernameParameter("email") 51 | .passwordParameter("passwd") 52 | .permitAll() 53 | .and() 54 | //예외처ㅣㄹ 55 | .exceptionHandling().accessDeniedHandler(MemberAccessDeniedHandler()) 56 | .and() 57 | .rememberMe() 58 | .key("jpub") 59 | .rememberMeParameter("remember-me") 60 | .rememberMeCookieName("jpubcookie") 61 | .tokenValiditySeconds(86400) //1day 62 | .tokenRepository(rememberMeTokenService()).userDetailsService(myUserService()) 63 | .and() 64 | //로그아웃 65 | .logout() 66 | .invalidateHttpSession(true) 67 | .clearAuthentication(true) 68 | .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) 69 | .logoutSuccessUrl("/login?logout") 70 | .permitAll(); 71 | } 72 | 73 | 74 | @Override 75 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 76 | auth.userDetailsService(myUserService()).passwordEncoder(bCryptPasswordEncoder()); 77 | } 78 | 79 | @Bean 80 | public FilterRegistrationBean getSpringSecurityFilterChainBindedToError( 81 | @Qualifier("springSecurityFilterChain") Filter springSecurityFilterChain) { 82 | 83 | FilterRegistrationBean registration = new FilterRegistrationBean(); 84 | registration.setFilter(springSecurityFilterChain); 85 | registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class)); 86 | return registration; 87 | } 88 | 89 | 90 | @Bean 91 | public BCryptPasswordEncoder bCryptPasswordEncoder() { 92 | return new BCryptPasswordEncoder(); 93 | } 94 | 95 | 96 | @Bean 97 | public MyUserService myUserService() throws Exception { 98 | return new MyUserService(); 99 | } 100 | 101 | @Bean 102 | public RememberMeTokenService rememberMeTokenService() throws Exception{ 103 | return new RememberMeTokenService(); 104 | } 105 | 106 | @Bean 107 | public MemberAccessDeniedHandler MemberAccessDeniedHandler() throws Exception{ 108 | return new MemberAccessDeniedHandler(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/resources/templates/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sb 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 |

회원가입

24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | 37 |
38 |
39 | User Name Error 40 |
41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 | 56 |
57 |
58 | email Error 59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 | password Error 77 |
78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |
88 |
89 |
90 |
91 | 92 | 93 |
94 |
95 | passwordConfirm Error 96 |
97 |
98 |
99 |
100 | 101 | 102 |
103 |
104 |
105 | 106 |
107 |
108 |
109 |
110 | 111 | 112 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/header.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
7 | 8 |
9 | 10 |
11 | 105 |
106 |
107 | -------------------------------------------------------------------------------- /src/main/resources/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS and IE text size adjust after device orientation change, 6 | * without disabling user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability of focused elements when they are also in an 95 | * active/hover state. 96 | */ 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | /* Text-level semantics 104 | ========================================================================== */ 105 | 106 | /** 107 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | */ 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | /** 115 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | /** 124 | * Address styling not present in Safari and Chrome. 125 | */ 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | /** 132 | * Address variable `h1` font-size and margin within `section` and `article` 133 | * contexts in Firefox 4+, Safari, and Chrome. 134 | */ 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | /** 142 | * Address styling not present in IE 8/9. 143 | */ 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | /** 151 | * Address inconsistent and variable font size in all browsers. 152 | */ 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | /** 159 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | */ 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | /* Embedded content 179 | ========================================================================== */ 180 | 181 | /** 182 | * Remove border when inside `a` element in IE 8/9/10. 183 | */ 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | /** 190 | * Correct overflow not hidden in IE 9/10/11. 191 | */ 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | /* Grouping content 198 | ========================================================================== */ 199 | 200 | /** 201 | * Address margin not present in IE 8/9 and Safari. 202 | */ 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | /** 209 | * Address differences between Firefox and other browsers. 210 | */ 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | */ 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; /* 1 */ 358 | box-sizing: content-box; /* 2 */ 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | * Safari (but not Chrome) clips the cancel button when the search input has 364 | * padding (and `textfield` appearance). 365 | */ 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | /** 373 | * Define consistent border, margin, and padding. 374 | */ 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | /** 383 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | */ 386 | 387 | legend { 388 | border: 0; /* 1 */ 389 | padding: 0; /* 2 */ 390 | } 391 | 392 | /** 393 | * Remove default vertical scrollbar in IE 8/9/10/11. 394 | */ 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | /** 401 | * Don't inherit the `font-weight` (applied by a rule above). 402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | */ 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | /* Tables 410 | ========================================================================== */ 411 | 412 | /** 413 | * Remove most spacing between table cells. 414 | */ 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | --------------------------------------------------------------------------------