commit c587ff09a8bee0d1b483e0861866b28b1f2b9988 Author: Florian Date: Fri Mar 16 22:49:47 2018 +0100 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2af7cef --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c83d1a2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.codiki + codiki + 0.0.1-SNAPSHOT + jar + + codiki + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mindrot + jbcrypt + 0.4 + + + org.postgresql + postgresql + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/src/main/java/org/codiki/CodikiApplication.java b/src/main/java/org/codiki/CodikiApplication.java new file mode 100644 index 0000000..8203781 --- /dev/null +++ b/src/main/java/org/codiki/CodikiApplication.java @@ -0,0 +1,14 @@ +package org.codiki; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableAutoConfiguration +public class CodikiApplication { + + public static void main(String[] args) { + SpringApplication.run(CodikiApplication.class, args); + } +} diff --git a/src/main/java/org/codiki/config/JpaConfiguration.java b/src/main/java/org/codiki/config/JpaConfiguration.java new file mode 100644 index 0000000..eab08c5 --- /dev/null +++ b/src/main/java/org/codiki/config/JpaConfiguration.java @@ -0,0 +1,42 @@ +package org.codiki.config; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EntityScan("org.codiki") +@EnableTransactionManagement +@EnableJpaRepositories("org.codiki") +@PropertySource("classpath:application.properties") +public class JpaConfiguration { + + @Value("${spring.datasource.driverClassName}") + private String driverClassName; + + @Value("${spring.datasource.url}") + private String url; + + @Value("${spring.datasource.username}") + private String username; + + @Value("${spring.datasource.password}") + private String password; + + @Bean(name="dataSource") + public DataSource getDataSource() { + return DataSourceBuilder.create() + .username(username) + .password(password) + .url(url) + .driverClassName(driverClassName) + .build(); + } +} diff --git a/src/main/java/org/codiki/core/AbstractFilter.java b/src/main/java/org/codiki/core/AbstractFilter.java new file mode 100644 index 0000000..92c9bc5 --- /dev/null +++ b/src/main/java/org/codiki/core/AbstractFilter.java @@ -0,0 +1,103 @@ +package org.codiki.core; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.codiki.security.Route; +import org.codiki.utils.StringUtils; + +/** + * Base class for all filters of the application.
+ *
+ * The children classes have to implements the method + * {@link AbstractFilter#getClass()} to set the URLs filtered (with all or some + * http methods), and the method + * {@link AbstractFilter#filter(HttpServletRequest, ServletResponse, FilterChain)} + * to define the filter processing. + * + * @author Takiguchi + * + */ +public abstract class AbstractFilter implements Filter { + + /** Regex url path prefix for method {@link this#isRequestFiltered(String)}. */ + private static final String PREFIX_URL_PATH = "https?:\\/\\/.*(:\\d{0,5})?"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Do nothing + } + + /** + * Returns the list of routes which will be processed by the filter. + * + * @return The routes. + */ + protected abstract List getRoutes(); + + /** + * Filter actions for its processing. + * + * @param request + * The http request. + * @param response + * The response. + * @param chain + * The chain. + */ + protected abstract void filter(HttpServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + + if(isRequestFiltered(httpRequest.getRequestURL().toString(), httpRequest.getMethod())) { + filter(httpRequest, response, chain); + } else { + chain.doFilter(request, response); + } + } + + /** + * Check if the url is allowed with the given method in parameters. + * + * @param pUrlRequest + * The url request. + * @param pMethodRequest + * The http method of the request. + * @return {@code true} if the url is allowed with the method, {@code false} + * otherwise. + */ + boolean isRequestFiltered(final String pUrlRequest, final String pMethodRequest) { + boolean result = false; + + for(final Route route : getRoutes()) { + /* + * Check urls matching, and if the method of the route isn't set, all methods + * are allowed. Otherwise, we check the methods too. + */ + result = Pattern.matches(StringUtils.concat(PREFIX_URL_PATH, route.getUrl()), pUrlRequest) + && (!route.getMethod().isPresent() || pMethodRequest.equals(route.getMethod().get().toString())); + if(result) { + break; + } + } + + return result; + } + + @Override + public void destroy() { + // Do nothing + } +} diff --git a/src/main/java/org/codiki/entities/dto/UserDAO.java b/src/main/java/org/codiki/entities/dto/UserDAO.java new file mode 100644 index 0000000..2754378 --- /dev/null +++ b/src/main/java/org/codiki/entities/dto/UserDAO.java @@ -0,0 +1,103 @@ +package org.codiki.entities.dto; + +import java.util.Date; + +import org.codiki.entities.persistence.Role; +import org.codiki.entities.persistence.User; + +public class UserDAO { + + private String key; + + private String name; + + private String email; + + private String password; + + private String image; + + private Date inscriptionDate; + + private Role role; + + private String token; + + public UserDAO() { + super(); + } + + public UserDAO(final User pUser) { + key = pUser.getKey(); + name = pUser.getName(); + email = pUser.getEmail(); + image = pUser.getImage(); + inscriptionDate = pUser.getInscriptionDate(); + role = pUser.getRole(); + token = pUser.getToken().getValue(); + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public Date getInscriptionDate() { + return inscriptionDate; + } + + public void setInscriptionDate(Date inscriptionDate) { + this.inscriptionDate = inscriptionDate; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/org/codiki/entities/persistence/Category.java b/src/main/java/org/codiki/entities/persistence/Category.java new file mode 100644 index 0000000..6e4688c --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/Category.java @@ -0,0 +1,78 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="category") +@Inheritance(strategy = InheritanceType.JOINED) +public class Category implements Serializable { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + /* ******************* */ + /* Relations */ + /* ******************* */ + @ManyToOne + @JoinColumn(name = "creator_id") + protected User creator; + + @OneToMany(mappedBy = "mainCategory") + private List listSubCategories; + + @OneToMany(mappedBy = "category") + protected List listPosts; + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public User getCreator() { + return creator; + } + + public void setCreator(User creator) { + this.creator = creator; + } + + public List getListSubCategories() { + return listSubCategories; + } +} diff --git a/src/main/java/org/codiki/entities/persistence/Comment.java b/src/main/java/org/codiki/entities/persistence/Comment.java new file mode 100644 index 0000000..4730891 --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/Comment.java @@ -0,0 +1,102 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="comment") +public class Comment implements Serializable { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String key; + + private String text; + + @Column(name = "creation_date") + @Temporal(TemporalType.TIMESTAMP) + private Date creationDate; + + /* ******************* */ + /* Relations */ + /* ******************* */ + @ManyToOne + @JoinColumn(name = "author") + private User author; + + @OneToMany(mappedBy = "comment") + private List history; + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public User getAuthor() { + return author; + } + + public void setAuthor(User author) { + this.author = author; + } + + public List getHistory() { + return history; + } + + public void setHistory(List history) { + this.history = history; + } +} diff --git a/src/main/java/org/codiki/entities/persistence/CommentHistory.java b/src/main/java/org/codiki/entities/persistence/CommentHistory.java new file mode 100644 index 0000000..a601deb --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/CommentHistory.java @@ -0,0 +1,80 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="comment_history") +public class CommentHistory implements Serializable { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String text; + + /* ******************* */ + /* Relations */ + /* ******************* */ + @Column(name = "update_date") + @Temporal(TemporalType.TIMESTAMP) + private Date updateDate; + + @ManyToOne + @JoinColumn(name = "comment_id") + private Comment comment; + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(Date updateDate) { + this.updateDate = updateDate; + } + + public Comment getComment() { + return comment; + } + + public void setComment(Comment comment) { + this.comment = comment; + } + +} diff --git a/src/main/java/org/codiki/entities/persistence/Image.java b/src/main/java/org/codiki/entities/persistence/Image.java new file mode 100644 index 0000000..590b73e --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/Image.java @@ -0,0 +1,72 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.Date; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="image") +public class Image implements Serializable { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String link; + + @Temporal(TemporalType.TIMESTAMP) + private Date date; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + +} diff --git a/src/main/java/org/codiki/entities/persistence/Post.java b/src/main/java/org/codiki/entities/persistence/Post.java new file mode 100644 index 0000000..b599503 --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/Post.java @@ -0,0 +1,177 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.Date; +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.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.codiki.utils.DateUtils; + +@Entity +@Table(name="post") +public class Post implements Serializable { + private static final long serialVersionUID = 1L; + + /** Number of character that compose the text extract. */ + private static final short EXTRACT_LENGTH = 250; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String key; + + private String title; + + private String text; + + @Column(length = 250) + private String description; + + private String image; + + @Column(name = "creation_date") + @Temporal(TemporalType.TIMESTAMP) + private Date creationDate; + + /* ******************* */ + /* Relations */ + /* ******************* */ + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "creator_id") + private User author; + + @ManyToOne + @JoinColumn(name = "category_id") + private Category category; + + @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) + private List history; + + /* ******************* */ + /* Constructors */ + /* ******************* */ + public Post() { + super(); + } + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public String getCreationDateStr() { + if(creationDate == null) { + return ""; + } else { + return DateUtils.format(creationDate); + } + } + + public String getTextExtract() { + String extract = text; + + if(extract != null && extract.length() > EXTRACT_LENGTH) { + extract = extract.substring(0, EXTRACT_LENGTH) + "..."; + } + + return extract; + } + + public User getAuthor() { + return author; + } + + public void setAuthor(User author) { + this.author = author; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public List getHistory() { + return history; + } + + public void setHistory(List history) { + this.history = history; + } +} diff --git a/src/main/java/org/codiki/entities/persistence/PostHistory.java b/src/main/java/org/codiki/entities/persistence/PostHistory.java new file mode 100644 index 0000000..ae20eae --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/PostHistory.java @@ -0,0 +1,82 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="post_history") +public class PostHistory implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + private String text; + + @Column(name = "update_date") + @Temporal(TemporalType.TIMESTAMP) + private Date updateDate; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(Date updateDate) { + this.updateDate = updateDate; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + +} diff --git a/src/main/java/org/codiki/entities/persistence/Role.java b/src/main/java/org/codiki/entities/persistence/Role.java new file mode 100644 index 0000000..7aca3a7 --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/Role.java @@ -0,0 +1,38 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="role") +public class Role implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/src/main/java/org/codiki/entities/persistence/SubCategory.java b/src/main/java/org/codiki/entities/persistence/SubCategory.java new file mode 100644 index 0000000..fc12c62 --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/SubCategory.java @@ -0,0 +1,36 @@ +package org.codiki.entities.persistence; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "sub_category") +public class SubCategory extends Category { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Relations */ + /* ******************* */ + @ManyToOne + @JoinColumn(name = "main_category") + private Category mainCategory; + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Category getMainCategory() { + return mainCategory; + } + + public void setMainCategory(Category mainCategory) { + this.mainCategory = mainCategory; + } + + public List getListPosts() { + return listPosts; + } +} diff --git a/src/main/java/org/codiki/entities/persistence/User.java b/src/main/java/org/codiki/entities/persistence/User.java new file mode 100644 index 0000000..a57cd4f --- /dev/null +++ b/src/main/java/org/codiki/entities/persistence/User.java @@ -0,0 +1,188 @@ +package org.codiki.entities.persistence; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +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.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.codiki.entities.security.Token; + +@Entity +@Table(name="`user`") +public class User implements Serializable { + private static final long serialVersionUID = 1L; + + /* ******************* */ + /* Attributes */ + /* ******************* */ + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="user_id_seq") + @SequenceGenerator(name="user_id_seq", sequenceName="user_id_seq", allocationSize=1) + private Long id; + + private String key; + + private String name; + + private String email; + + private String password; + + private String image; + + @Column(name = "inscription_date") + @Temporal(TemporalType.TIMESTAMP) + private Date inscriptionDate; + + /* ******************* */ + /* Relations */ + /* ******************* */ + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "role_id") + private Role role; + + @OneToMany(mappedBy = "author") + private List listPosts; + + @OneToMany(mappedBy = "author") + private List listComments; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + private List listImages; + + /** Authentication token. */ + private transient Token token; + + /* ******************* */ + /* Constructors */ + /* ******************* */ + public User() { + super(); + token = new Token(); + } + + /* ******************* */ + /* Getters & Setters */ + /* ******************* */ + public Long getId() { + return id; + } + + public void setId(Long id) { + if(this.id != null) { + throw new IllegalAccessError("It's not allowed to rewrite the id entity."); + } + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public Date getInscriptionDate() { + return inscriptionDate; + } + + public void setInscriptionDate(Date inscriptionDate) { + this.inscriptionDate = inscriptionDate; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public List getListPosts() { + return listPosts; + } + + public void setListPosts(List listPosts) { + this.listPosts = listPosts; + } + + public List getListComments() { + return listComments; + } + + public void setListComments(List listComments) { + this.listComments = listComments; + } + + public List getListImages() { + return listImages; + } + + public void addImage(final Image pImage) { + if(listImages == null) { + listImages = new ArrayList<>(); + } + listImages.add(pImage); + + if(pImage.getUser() == null) { + pImage.setUser(this); + } + } + + public void setListImages(List listImages) { + this.listImages = listImages; + } + + public Token getToken() { + return token; + } + +} diff --git a/src/main/java/org/codiki/entities/security/Token.java b/src/main/java/org/codiki/entities/security/Token.java new file mode 100644 index 0000000..04d969c --- /dev/null +++ b/src/main/java/org/codiki/entities/security/Token.java @@ -0,0 +1,82 @@ +package org.codiki.entities.security; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Calendar; + +public class Token { + /** The metric in which the validation delay is defined. */ + private static final int METRIC = Calendar.MINUTE; + /** Number of {@link METRIC} after that the token become invalid. */ + private static final int DELAY = 30; + + /** The Constant BITS_NUMBER. */ + private static final int BITS_NUMBER = 130; + /** The Constant RADIX. */ + private static final int RADIX = 32; + + /** The value. */ + private String value; + + /** + * Last access date. For each request to the server, this date is consulted + * and if the valid delay is ok, this date must be updated. + */ + private Calendar lastAccess; + + /** + * Instantiates a new token. + */ + public Token() { + super(); + value = new BigInteger(BITS_NUMBER, new SecureRandom()).toString(RADIX); + lastAccess = Calendar.getInstance(); + } + + /** + * Gets the value. + * + * @return the value + */ + public String getValue() { + return value; + } + + /** + * Gets the last access date. + * + * @return the last access date + */ + public Calendar getLastAccess() { + return lastAccess; + } + + /** + * Sets the last access date. + */ + public void setLastAccess() { + lastAccess = Calendar.getInstance(); + } + + /** + * Indicate if the token is still valid.
+ * A token is valid is its {@link Token#lastAccess} is after the current + * date minus the {@link Token#DELAY} {@link Token#METRIC}.
+ *
+ * Example:
+ * {@link Token#DELAY} = 30 and {@link Token#METRIC} = + * {@link Calendar#MINUTE}.
+ * A token is valid only on the 30 minutes after its + * {@link Token#lastAccess}.
+ * If the current date-time minus the 30 minutes is before the + * {@link Token#lastAccess}, the token is still valid. + * + * @return {@code true} if the token is still valid, {@code false} + * otherwise. + */ + public boolean isValid() { + final Calendar lastTimeValidation = Calendar.getInstance(); + lastTimeValidation.add(METRIC, -DELAY); + return lastAccess.getTime().after(lastTimeValidation.getTime()); + } +} diff --git a/src/main/java/org/codiki/login/LoginController.java b/src/main/java/org/codiki/login/LoginController.java new file mode 100644 index 0000000..d7af219 --- /dev/null +++ b/src/main/java/org/codiki/login/LoginController.java @@ -0,0 +1,37 @@ +package org.codiki.login; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.codiki.entities.dto.UserDAO; +import org.codiki.security.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/account") +public class LoginController { + + private static final String HEADER_TOKEN = "token"; + + @Autowired + private TokenService tokenService; + + @Autowired + private LoginService loginService; + + @PostMapping("/login") + public UserDAO login(@RequestBody UserDAO pUser, HttpServletResponse response) { + return loginService.checkCredentials(response, pUser); + } + + @GetMapping("/logout") + public void logout(HttpServletRequest pRequest) { + tokenService.removeUser(pRequest.getHeader(HEADER_TOKEN)); + } + +} diff --git a/src/main/java/org/codiki/login/LoginService.java b/src/main/java/org/codiki/login/LoginService.java new file mode 100644 index 0000000..16666ad --- /dev/null +++ b/src/main/java/org/codiki/login/LoginService.java @@ -0,0 +1,50 @@ +package org.codiki.login; + +import java.util.Optional; + +import javax.naming.AuthenticationException; +import javax.servlet.http.HttpServletResponse; + +import org.codiki.entities.dto.UserDAO; +import org.codiki.entities.persistence.User; +import org.codiki.repositories.UserRepository; +import org.codiki.security.TokenService; +import org.codiki.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class LoginService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenService tokenService; + + /** + * Check the user credentials and generate him a token if they are correct. + * + * @param pUser + * The user sent from client. + * @return The user populated with the generated token. + * @throws AuthenticationException + * If the credentials are wrong. + */ + public UserDAO checkCredentials(HttpServletResponse pResponse, UserDAO pUser) { + UserDAO result = null; + + Optional user = userRepository.findByEmail(pUser.getEmail()); + + if(user.isPresent() && StringUtils.compareHash(pUser.getPassword(), user.get().getPassword())) { + tokenService.addUser(user.get()); + result = new UserDAO(user.get()); + } else { + pResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + return result; + } + + +} diff --git a/src/main/java/org/codiki/repositories/UserRepository.java b/src/main/java/org/codiki/repositories/UserRepository.java new file mode 100644 index 0000000..a3a51c6 --- /dev/null +++ b/src/main/java/org/codiki/repositories/UserRepository.java @@ -0,0 +1,13 @@ +package org.codiki.repositories; + +import java.util.Optional; + +import org.codiki.entities.persistence.User; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends CrudRepository { + Optional findByEmail(@Param("email") final String pEmail); +} diff --git a/src/main/java/org/codiki/security/AuthenticationFilter.java b/src/main/java/org/codiki/security/AuthenticationFilter.java new file mode 100644 index 0000000..8707893 --- /dev/null +++ b/src/main/java/org/codiki/security/AuthenticationFilter.java @@ -0,0 +1,35 @@ +package org.codiki.security; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.codiki.core.AbstractFilter; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class AuthenticationFilter extends AbstractFilter { + + @Override + protected List getRoutes() { + return Arrays.asList( + new Route("\\/api\\/account\\/.*") + ); + } + + @Override + protected void filter(HttpServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("Token : " + request.getHeader("token")); + + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/org/codiki/security/Route.java b/src/main/java/org/codiki/security/Route.java new file mode 100644 index 0000000..d697841 --- /dev/null +++ b/src/main/java/org/codiki/security/Route.java @@ -0,0 +1,69 @@ +package org.codiki.security; + +import java.util.Optional; + +import org.springframework.http.HttpMethod; + +/** + * Route for filter matching. + * + * @author Takiguchi + * + */ +public class Route { + /** The regex to match urls. */ + private String url; + /** The http method to match. Use a {@link Optional#empty()} to match all methods. */ + private Optional method; + + /** + * Instanciate a vierge route. + */ + public Route() { + super(); + url = ""; + method = Optional.empty(); + } + + /** + * Instanciate a route for all http methods. + * + * @param pUrl + * The regex to match urls. + */ + public Route(final String pUrl) { + this(); + this.url = pUrl; + } + + /** + * Instanciate a route for methods given in parameters + * + * @param pUrl + * The regex to match urls. + * @param pMethod + * The http method to match. Use a {@link Optional#empty()} to match + * all methods. + */ + public Route(final String pUrl, final HttpMethod pMethod) { + this(pUrl); + this.method = Optional.of(pMethod); + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Optional getMethod() { + return method; + } + + public void setMethod(HttpMethod method) { + this.method = Optional.of(method); + } + +} diff --git a/src/main/java/org/codiki/security/TokenService.java b/src/main/java/org/codiki/security/TokenService.java new file mode 100644 index 0000000..09ad13c --- /dev/null +++ b/src/main/java/org/codiki/security/TokenService.java @@ -0,0 +1,106 @@ +package org.codiki.security; + +import java.util.Map; +import java.util.TreeMap; + +import org.codiki.entities.persistence.User; +import org.springframework.stereotype.Service; + +@Service +public class TokenService { + /** Map of connected users. */ + private static final Map connectedUsers; + + /** + * Class constructor + */ + static { + connectedUsers = new TreeMap<>(); + } + + /** + * Check if the token matches to a user session, and if it is still valid. + * + * @param pToken + * The token to check. + * @return {@code true} if the token is still valid, {@code false} + * otherwise. + */ + public boolean isUserConnected(final String pToken) { + boolean result = false; + + if (pToken != null && connectedUsers.containsKey(pToken)) { + if (connectedUsers.get(pToken).getToken().isValid()) { + result = true; + } else { + connectedUsers.remove(pToken); + } + } + + // clear all the expired sessions + clearExpiredUsers(); + + return result; + } + + /** + * Remove from the connected users map all the elements which their token is + * expired. + */ + @SuppressWarnings("unlikely-arg-type") + private void clearExpiredUsers() { + connectedUsers.entrySet().stream().forEach(user -> { + if(!user.getValue().getToken().isValid()) { + connectedUsers.remove(user).getKey(); + } + }); + } + + /** + * Add the user to the connected users map. + * + * @param pUser + * The user to add. + */ + public void addUser(final User pUser) { + if(connectedUsers.get(pUser.getToken().getValue()) == null) { + connectedUsers.put(pUser.getToken().getValue(), pUser); + } + } + + /** + * Refresh the user token last access date in the token service. + * + * @param pToken + * The user token. + */ + public void refreshUserToken(final String pToken) { + final User user = connectedUsers.get(pToken); + if(user != null) { + user.getToken().setLastAccess(); + } + } + + /** + * Remove the user to the connected users map. + * + * @param pUser + * The user to remove. + */ + public void removeUser(final User pUser) { + removeUser(pUser.getToken().getValue()); + } + + /** + * Remove the user associated to the token given in parameters, from the + * connected users map. + * + * @param pToken + * The user to delete token. + */ + public void removeUser(final String pToken) { + if(pToken != null && connectedUsers.containsKey(pToken)) { + connectedUsers.remove(pToken); + } + } +} diff --git a/src/main/java/org/codiki/utils/DateUtils.java b/src/main/java/org/codiki/utils/DateUtils.java new file mode 100644 index 0000000..083e303 --- /dev/null +++ b/src/main/java/org/codiki/utils/DateUtils.java @@ -0,0 +1,35 @@ +package org.codiki.utils; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class DateUtils { + public static final String FORMAT_DEFAULT = "dd/MM/yyyy HH:mm:ss"; + + public static Date parseDate(String pSource, String pPattern) throws ParseException { + Date result = null; + if (pSource != null && !pSource.isEmpty()) { + DateFormat formatter = getSimpleDateFormat(pPattern); + formatter.setLenient(false); + result = formatter.parse(pSource); + } + return result; + } + + public static String format(Date pDate, String pPattern) { + return getSimpleDateFormat(pPattern).format(pDate); + } + + public static String format(Date pDate) { + return getSimpleDateFormat(FORMAT_DEFAULT).format(pDate); + } + + public static SimpleDateFormat getSimpleDateFormat(String pPattern) { + SimpleDateFormat result = new SimpleDateFormat(pPattern, Locale.FRENCH); + result.setLenient(false); + return result; + } +} diff --git a/src/main/java/org/codiki/utils/RegexUtils.java b/src/main/java/org/codiki/utils/RegexUtils.java new file mode 100644 index 0000000..dd43125 --- /dev/null +++ b/src/main/java/org/codiki/utils/RegexUtils.java @@ -0,0 +1,82 @@ +package org.codiki.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class RegexUtils { + + private static final String EMAIL_REGEX = "^.*@.*\\..{2,}$"; + private static final String LOWER_LETTERS_REGEX = ".*[a-z].*"; + private static final String UPPER_LETTERS_REGEX = ".*[A-Z].*"; + private static final String NUMBER_REGEX = ".*[0-9].*"; + private static final String SPECIAL_CHAR_REGEX = ".*\\W.*"; + private static final String NUMBER_ONLY_REGEX = "^[0-9]+$"; + + // La portée "package" permet à la classe StringUtils d'utiliser les patterns + // suivants : + static final Pattern EMAIL_PATTERN; + static final Pattern LOWER_LETTERS_PATTERN; + static final Pattern UPPER_LETTERS_PATTERN; + static final Pattern NUMBER_PATTERN; + static final Pattern SPECIAL_CHAR_PATTERN; + static final Pattern NUMBER_ONLY_PATTERN; + + static { + EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + LOWER_LETTERS_PATTERN = Pattern.compile(LOWER_LETTERS_REGEX); + UPPER_LETTERS_PATTERN = Pattern.compile(UPPER_LETTERS_REGEX); + NUMBER_PATTERN = Pattern.compile(NUMBER_REGEX); + SPECIAL_CHAR_PATTERN = Pattern.compile(SPECIAL_CHAR_REGEX); + NUMBER_ONLY_PATTERN = Pattern.compile(NUMBER_ONLY_REGEX); + } + + /** + * Chekcs if {@code pString} corresponds to an email address. + * + * @param pString + * The string which should be an email address. + * @return {@code true} if {@link pString} corresponds to an email address, + * {@code false} otherwise. + */ + public static boolean isEmail(final String pString) { + return EMAIL_PATTERN.matcher(pString).find(); + } + + /** + * Replace the sequences of {@code pString} matched by the {@code pRegex} + * with the {@code pReplacingString}. + * + * @param pString + * The string to update. + * @param pRegex + * The regex to match the sentences to replace. + * @param pReplacingString + * The string to replace the sentences which match with the + * regex. + * @return The new string. + */ + public static String replaceSequence(final String pString, + final String pRegex, final String pReplacingString) { + return Pattern.compile(pRegex).matcher(pString) + .replaceAll(pReplacingString); + } + + /** + * Checks if {@code pString} corresponds to a number. + * + * @param pString + * The string which should be a number. + * @return {@code true} if {@code pString} corresponds to a number, + * {@code false} otherwise. + */ + public static boolean isNumber(final String pString) { + return NUMBER_ONLY_PATTERN.matcher(pString).find(); + } + + public static String getGroup(final String regex, final int numeroGroupe, final String chaine) { + final Pattern pattern = Pattern.compile(regex); + final Matcher matcher = pattern.matcher(chaine); + matcher.find(); + return matcher.group(numeroGroupe); + } +} \ No newline at end of file diff --git a/src/main/java/org/codiki/utils/StringUtils.java b/src/main/java/org/codiki/utils/StringUtils.java new file mode 100644 index 0000000..171d7d4 --- /dev/null +++ b/src/main/java/org/codiki/utils/StringUtils.java @@ -0,0 +1,93 @@ +package org.codiki.utils; + +import org.mindrot.jbcrypt.BCrypt; + +/** + * Generic methods about {@link String} class. + * + * @author takiguchi + * + */ +public final class StringUtils { + + /** + * Indicate if {@code pString} is null or just composed of spaces. + * + * @param pString + * The string to test. + * @return {@code true} if {@code pString} is null or just composed of + * spaces, {@code false} otherwise. + */ + public static boolean isNull(final String chaine) { + return chaine == null || chaine.trim().length() == 0; + } + + /** + * Hash the password given into parameters. + * + * @param pPassword The password to hash. + * @return The password hashed. + */ + public static String hashPassword(final String pPassword) { + return hashString(pPassword, 10); + } + + public static String hashString(final String pString, final int pSalt) { + return BCrypt.hashpw(pString, BCrypt.gensalt(pSalt)); + } + + /** + * Compare the password and the hashed string given into parameters. + * + * @param pPassword + * The password to compare to the hashed string. + * @param pHashToCompare + * The hashed string to compare to the password. + * @return {@code true} if the password matches to the hashed string. + */ + public static boolean compareHash(final String pPassword, final String pHashToCompare) { + return BCrypt.checkpw(pPassword, pHashToCompare); + } + + /** + * Concatenate the parameters to form just one single string. + * + * @param pArgs + * The strings to concatenate. + * @return The parameters concatenated. + */ + public static String concat(final Object... pArgs) { + final StringBuilder result = new StringBuilder(); + for (final Object arg : pArgs) { + result.append(arg); + } + return result.toString(); + } + + public static String printStrings(final String... pStrings) { + final StringBuilder result = new StringBuilder(); + for (int i = 0 ; i < pStrings.length ; i++) { + result.append(pStrings[i]); + if(i < pStrings.length - 1) { + result.append(","); + } + } + return result.toString(); + } + + public static boolean containLowercase(final String pString) { + return RegexUtils.LOWER_LETTERS_PATTERN.matcher(pString).find(); + } + + public static boolean containUppercase(final String pString) { + return RegexUtils.UPPER_LETTERS_PATTERN.matcher(pString).find(); + } + + public static boolean containNumber(final String pString) { + return RegexUtils.NUMBER_PATTERN.matcher(pString).find(); + } + + public static boolean containSpecialChar(final String pString) { + return RegexUtils.SPECIAL_CHAR_PATTERN.matcher(pString).find(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8d24795 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,15 @@ +#server.error.whitelabel.enabled=false +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/codiki +spring.datasource.username=codiki +spring.datasource.password=P@ssword + +# Disable feature detection by this undocumented parameter. Check the org.hibernate.engine.jdbc.internal.JdbcServiceImpl.configure method for more details. +spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false + +# Because detection is disabled you have to set correct dialect by hand. +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect + +logging.level.org.hibernate=DEBUG + +cors.enabled=false \ No newline at end of file diff --git a/src/test/java/org/codiki/core/AbstractFilterTest.java b/src/test/java/org/codiki/core/AbstractFilterTest.java new file mode 100644 index 0000000..a7b20e3 --- /dev/null +++ b/src/test/java/org/codiki/core/AbstractFilterTest.java @@ -0,0 +1,72 @@ +package org.codiki.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.codiki.security.Route; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpMethod; + +@RunWith(MockitoJUnitRunner.class) +public class AbstractFilterTest { + + /** + * Class that simplify the test mocks for the method {@link AbstractFilter#getRoutes()}. + * + * @author Takiguchi + * + */ + private class TestFilter extends AbstractFilter { + public List routes; + + @Override + protected List getRoutes() { + return routes; + } + + @Override + protected void filter(HttpServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // Do nothing + } + } + + /** Tested class object */ + private AbstractFilter filter = new TestFilter(); + + @Test + public void test_isRequestFiltered_true() { + ((TestFilter) filter).routes = Arrays.asList(new Route("toto", HttpMethod.GET)); + + Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "GET")); + } + + @Test + public void test_isRequestFiltered_true_without_httpMethod() { + ((TestFilter) filter).routes = Arrays.asList(new Route("toto")); + + Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "GET")); + Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "POST")); + Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "PUT")); + Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "DELETE")); + Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "DumbThing")); + } + + @Test + public void test_isRequestFiltered_false() { + ((TestFilter) filter).routes = Arrays.asList(new Route("toto", HttpMethod.POST)); + + Assert.assertFalse(filter.isRequestFiltered("http://localhost/toto", "GET")); + } +}