Compare commits

..

7 Commits

47 changed files with 340 additions and 361 deletions

View File

@@ -8,7 +8,6 @@ import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableAutoConfiguration
public class CodikiApplication {
public static void main(String[] args) {
SpringApplication.run(CodikiApplication.class, args);
}

View File

@@ -101,8 +101,8 @@ public class AccountController {
}
@PutMapping("/")
public void update(@RequestBody final UserDTO pUser, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
accountService.updateUser(pUser, pRequest, pResponse, pPrincipal);
public void update(@RequestBody UserDTO pUser, HttpServletResponse pResponse, Principal pPrincipal)
throws IOException {
accountService.updateUser(pUser, pResponse, pPrincipal);
}
}

View File

@@ -1,17 +1,5 @@
package org.codiki.account;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.security.Principal;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codiki.core.entities.dto.ImageDTO;
import org.codiki.core.entities.dto.PasswordWrapperDTO;
import org.codiki.core.entities.dto.UserDTO;
@@ -22,38 +10,30 @@ import org.codiki.core.security.CustomAuthenticationProvider;
import org.codiki.core.services.FileUploadService;
import org.codiki.core.services.UserService;
import org.codiki.core.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class AccountService {
/** Logger. */
private static final Logger LOG = LoggerFactory.getLogger(FileUploadService.class);
private final CustomAuthenticationProvider authenticationProvider;
private final UserService userService;
private final UserRepository userRepository;
private final FileUploadService fileUploadService;
private final ImageRepository imageRepository;
private CustomAuthenticationProvider authenticationProvider;
private UserService userService;
private UserRepository userRepository;
private FileUploadService fileUploadService;
private ImageRepository imageRepository;
/**
* Constructor.
* @param authenticationProvider
* @param userService
* @param userRepository
* @param fileUploadService
* @param imageRepository
*/
/** Constructor. */
public AccountService(CustomAuthenticationProvider authenticationProvider,
UserService userService,
UserRepository userRepository,
@@ -93,8 +73,8 @@ public class AccountService {
return resultCode;
}
public String uploadFile(final MultipartFile pFile, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
public String uploadFile(MultipartFile pFile, HttpServletResponse pResponse, Principal pPrincipal)
throws IOException {
final String avatarFileName = fileUploadService.uploadProfileImage(pFile);
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
@@ -117,8 +97,7 @@ public class AccountService {
return fileUploadService.loadAvatar(pAvatarFileName);
}
public List<ImageDTO> getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
final Principal pPrincipal) throws IOException {
public List<ImageDTO> getUserImages(HttpServletResponse pResponse, Principal pPrincipal) throws IOException {
List<ImageDTO> result = new LinkedList<>();
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
@@ -132,7 +111,7 @@ public class AccountService {
return result;
}
public void signin(final User pUser, final HttpServletResponse pResponse) throws IOException {
public void signin(User pUser, HttpServletResponse pResponse) throws IOException {
if(pUser.getName() == null || pUser.getEmail() == null || pUser.getPassword() == null || "".equals(pUser.getPassword().trim())) {
pResponse.sendError(HttpServletResponse.SC_BAD_REQUEST);
} else if(userRepository.findByEmail(pUser.getEmail()).isPresent()) {
@@ -148,8 +127,7 @@ public class AccountService {
}
}
public void updateUser(final UserDTO pUser, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
public void updateUser(UserDTO pUser, HttpServletResponse pResponse, Principal pPrincipal) throws IOException {
final Optional<User> connectedUserOpt = userService.getUserByPrincipal(pPrincipal);
if(connectedUserOpt.isPresent()) {
final User connectedUser = connectedUserOpt.get();

View File

@@ -1,11 +1,7 @@
package org.codiki.categories;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.codiki.core.entities.dto.CategoryDTO;
import com.fasterxml.jackson.annotation.JsonView;
import org.codiki.core.entities.dto.View;
import org.codiki.core.entities.persistence.Category;
import org.codiki.core.repositories.CategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,22 +10,33 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@RestController
@RequestMapping("/api/categories")
public class CategoryController {
private final CategoryRepository categoryRepository;
@Autowired
private CategoryRepository categoryRepository;
@GetMapping("/{id}")
public CategoryDTO findById(@PathVariable("id") final Long pId) {
final Optional<Category> result = categoryRepository.findById(pId);
return result.isPresent() ? new CategoryDTO(result.get()) : null;
/** Constructor. */
public CategoryController(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
@JsonView(View.CategoryDTO.class)
@GetMapping("/{id}")
public Category findById(@PathVariable("id") final Long pId,
HttpServletResponse response) {
return categoryRepository.findByIdWithSubCategories(pId)
.orElseGet(() -> {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return null;
});
}
@JsonView(View.CategoryDTO.class)
@GetMapping("/")
public List<CategoryDTO> getAll() {
return StreamSupport.stream(categoryRepository.findAll().spliterator(), false)
.map(CategoryDTO::new).collect(Collectors.toList());
public List<Category> getAll() {
return categoryRepository.findAllWithSubCategories();
}
}

View File

@@ -1,58 +0,0 @@
package org.codiki.core.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* Controller that catch errors from spring rest or spring security and others, and transform them to JSON response.
*/
@RestController
@RequestMapping("/error")
public class CustomErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
@Autowired
public CustomErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping
public Map<String, Object> error(HttpServletRequest request){
Map<String, Object> body = getErrorAttributes(request, getTraceParameter(request));
String trace = (String) body.get("trace");
if(trace != null){
String[] lines = trace.split("\n\t");
body.put("trace", lines);
}
return body;
}
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
private Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
return errorAttributes.getErrorAttributes(new ServletWebRequest(request), includeStackTrace);
}
}

View File

@@ -17,21 +17,11 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories("org.codiki.core.repositories")
@PropertySource("classpath:application.yml")
public class JpaConfiguration {
@Value("${spring.jpa.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.jpa.datasource.url}")
private String url;
@Value("${spring.jpa.datasource.username}")
private String username;
@Value("${spring.jpa.datasource.password}")
private String password;
@Bean(name = "dataSource")
public DataSource getDataSource() {
public DataSource getDataSource(@Value("${spring.jpa.datasource.driverClassName}") String driverClassName,
@Value("${spring.jpa.datasource.url}") String url,
@Value("${spring.jpa.datasource.username}") String username,
@Value("${spring.jpa.datasource.password}") String password) {
return DataSourceBuilder.create()
.username(username)
.password(password)

View File

@@ -10,7 +10,6 @@ import java.io.IOException;
@RestController
public class RobotsTxtController {
private static final Logger LOG = LoggerFactory.getLogger(RobotsTxtController.class);
@RequestMapping(value = "/robots.txt")

View File

@@ -3,4 +3,5 @@ package org.codiki.core.entities.dto;
public class View {
public interface UserDTO {}
public interface PostDTO {}
public interface CategoryDTO {}
}

View File

@@ -20,10 +20,12 @@ import org.codiki.core.entities.dto.CategoryDTO;
import org.codiki.core.entities.dto.View;
import com.fasterxml.jackson.annotation.JsonView;
import org.hibernate.annotations.Proxy;
@Entity
@Table(name="category")
@Inheritance(strategy = InheritanceType.JOINED)
@Proxy(lazy = false)
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
@@ -32,10 +34,10 @@ public class Category implements Serializable {
/* ******************* */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonView(View.PostDTO.class)
@JsonView({View.PostDTO.class, View.CategoryDTO.class})
private Long id;
@JsonView(View.PostDTO.class)
@JsonView({View.PostDTO.class, View.CategoryDTO.class})
private String name;
/* ******************* */
@@ -45,6 +47,7 @@ public class Category implements Serializable {
@JoinColumn(name = "creator_id")
protected User creator;
@JsonView({View.CategoryDTO.class})
@OneToMany(mappedBy = "mainCategory")
private List<SubCategory> listSubCategories;

View File

@@ -1,5 +1,7 @@
package org.codiki.core.entities.persistence;
import org.hibernate.annotations.Proxy;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@@ -19,6 +21,7 @@ import javax.persistence.TemporalType;
@Entity
@Table(name="comment")
@Proxy(lazy = false)
public class Comment implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -1,5 +1,7 @@
package org.codiki.core.entities.persistence;
import org.hibernate.annotations.Proxy;
import java.io.Serializable;
import java.util.Date;
@@ -17,6 +19,7 @@ import javax.persistence.TemporalType;
@Entity
@Table(name="comment_history")
@Proxy(lazy = false)
public class CommentHistory implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -1,5 +1,7 @@
package org.codiki.core.entities.persistence;
import org.hibernate.annotations.Proxy;
import java.io.Serializable;
import java.util.Date;
@@ -16,6 +18,7 @@ import javax.persistence.TemporalType;
@Entity
@Table(name="image")
@Proxy(lazy = false)
public class Image implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -25,9 +25,11 @@ import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime;
import com.fasterxml.jackson.annotation.JsonView;
import org.hibernate.annotations.Proxy;
@Entity
@Table(name="post")
@Proxy(lazy = false)
public class Post implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -1,5 +1,7 @@
package org.codiki.core.entities.persistence;
import org.hibernate.annotations.Proxy;
import java.io.Serializable;
import java.util.Date;
@@ -17,6 +19,7 @@ import javax.persistence.TemporalType;
@Entity
@Table(name="post_history")
@Proxy(lazy = false)
public class PostHistory implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -11,9 +11,11 @@ import javax.persistence.Table;
import org.codiki.core.entities.dto.View;
import com.fasterxml.jackson.annotation.JsonView;
import org.hibernate.annotations.Proxy;
@Entity
@Table(name="role")
@Proxy(lazy = false)
public class Role implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -1,5 +1,8 @@
package org.codiki.core.entities.persistence;
import com.fasterxml.jackson.annotation.JsonView;
import org.hibernate.annotations.Proxy;
import java.util.List;
import javax.persistence.Entity;
@@ -10,6 +13,7 @@ import javax.persistence.Table;
@Entity
@Table(name = "sub_category")
@Proxy(lazy = false)
public class SubCategory extends Category {
private static final long serialVersionUID = 1L;
@@ -34,4 +38,15 @@ public class SubCategory extends Category {
public List<Post> getListPosts() {
return listPosts;
}
/**
* {@inheritDoc Category#getListSubCategories}.
*
* This getter is overrided to set {@link JsonView} annotation to avoid Lazy initialization for subcategories.
*/
@JsonView()
@Override
public List<SubCategory> getListSubCategories() {
return super.getListSubCategories();
}
}

View File

@@ -25,9 +25,11 @@ import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime;
import com.fasterxml.jackson.annotation.JsonView;
import org.hibernate.annotations.Proxy;
@Entity
@Table(name="`user`")
@Proxy(lazy = false)
public class User implements Serializable {
private static final long serialVersionUID = 1L;

View File

@@ -1,5 +1,7 @@
package org.codiki.core.entities.persistence;
import org.hibernate.annotations.Proxy;
import java.io.Serializable;
import javax.persistence.Entity;
@@ -16,6 +18,7 @@ import javax.persistence.Table;
*/
@Entity
@Table(name="version")
@Proxy(lazy = false)
public class Version implements Serializable {
private static final long serialVersionUID = -8803744005903941330L;

View File

@@ -1,5 +1,7 @@
package org.codiki.core.entities.persistence;
import org.hibernate.annotations.Proxy;
import java.io.Serializable;
import javax.persistence.Entity;
@@ -13,6 +15,7 @@ import javax.persistence.Table;
@Entity
@Table(name="version_revision")
@Proxy(lazy = false)
public class VersionRevision implements Serializable {
private static final long serialVersionUID = 1837590467941917225L;

View File

@@ -1,14 +1,19 @@
package org.codiki.core.repositories;
import java.util.List;
import java.util.Optional;
import org.codiki.core.entities.persistence.Category;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface CategoryRepository extends CrudRepository<Category, Long> {
@Query("SELECT c FROM Category c JOIN FETCH c.listSubCategories")
@Query("SELECT c FROM Category c LEFT JOIN FETCH c.listSubCategories WHERE c.id = :id")
Optional<Category> findByIdWithSubCategories(@Param("id") Long id);
@Query("SELECT DISTINCT(c) FROM Category c LEFT JOIN FETCH c.listSubCategories ORDER BY c.name")
List<Category> findAllWithSubCategories();
}

View File

@@ -19,7 +19,7 @@ import org.springframework.stereotype.Component;
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
public void init(FilterConfig filterConfig) {
// Do nothing
}
@@ -40,5 +40,4 @@ public class CorsFilter implements Filter {
public void destroy() {
// Do nothing
}
}

View File

@@ -1,23 +1,21 @@
package org.codiki.core.security;
import java.util.ArrayList;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Creation of the authentication bean with its roles
Authentication auth = new UsernamePasswordAuthenticationToken(authentication.getName(),
authentication.getCredentials(), new ArrayList<GrantedAuthority>());
authentication.getCredentials(), new ArrayList<>());
// Set the auth bean in spring security context
SecurityContextHolder.getContext().setAuthentication(auth);
@@ -29,5 +27,4 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

View File

@@ -1,15 +1,13 @@
package org.codiki.core.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Authentication entry point configured in
* {@link SecurityConfiguration#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)}

View File

@@ -26,11 +26,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private CustomAuthenticationProvider authenticationProvider;
private RestAuthenticationEntryPoint authenticationEntryPoint;
/**
* Constructor.
* @param authenticationProvider
* @param authenticationEntryPoint
*/
/** Constructor. */
public SecurityConfiguration(CustomAuthenticationProvider authenticationProvider,
RestAuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationProvider = authenticationProvider;

View File

@@ -1,15 +1,6 @@
package org.codiki.core.services;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import org.apache.commons.lang.RandomStringUtils;
import org.codiki.core.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@@ -18,6 +9,12 @@ import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
@Service
public class FileUploadService {
/** Logger. */
@@ -25,12 +22,17 @@ public class FileUploadService {
/** Length of uploaded file names. */
private static final int DESTINATION_IMAGE_NAME_LENGTH = 30;
@Value("${codiki.files.upload}")
private String folderUpload;
@Value("${codiki.files.profile-images}")
private String folderProfileImages;
@Value("${codiki.files.images}")
private String folderImages;
private final String folderUpload;
private final String folderProfileImages;
private final String folderImages;
public FileUploadService(@Value("${codiki.files.upload}") String folderUpload,
@Value("${codiki.files.profile-images}") String folderProfileImages,
@Value("${codiki.files.images}")String folderImages) {
this.folderUpload = folderUpload;
this.folderProfileImages = folderProfileImages;
this.folderImages = folderImages;
}
/**
* Builds the destination file name, which is a random string with 30 char

View File

@@ -9,7 +9,6 @@ import org.springframework.stereotype.Service;
@Service
public class ParserService {
private static final String REG_CODE = "\\[code lg=&quot;([a-z]+)&quot;\\](.*)\\[\\/code\\]\\n";
private static final String REG_CODE_REPLACE = "<pre class=\"line-numbers\"><code class=\"language-\\1\">\\2</code></pre>";
private static final String REG_IMAGES = "\\[img src=&quot;([^\"| ]+)&quot;( alt=&quot;([^\"| ]+)&quot;)? \\/\\]";
@@ -17,12 +16,12 @@ public class ParserService {
private static final String REG_LINKS = "\\[link href=&quot;([^\"| ]+)&quot; txt=&quot;([^\"| ]+)&quot; \\/\\]";
private static final String REG_LINKS_REPLACE = "<a href=\"\\1\">\\2</a>";
static final Pattern PATTERN_CODE;
static final Pattern PATTERN_CODE_REPLACE;
static final Pattern PATTERN_IMAGES;
static final Pattern PATTERN_IMAGES_REPLACE;
static final Pattern PATTERN_LINKS;
static final Pattern PATTERN_LINKS_REPLACE;
private static final Pattern PATTERN_CODE;
private static final Pattern PATTERN_CODE_REPLACE;
private static final Pattern PATTERN_IMAGES;
private static final Pattern PATTERN_IMAGES_REPLACE;
private static final Pattern PATTERN_LINKS;
private static final Pattern PATTERN_LINKS_REPLACE;
static {
PATTERN_CODE = Pattern.compile(REG_CODE);
@@ -45,7 +44,7 @@ public class ParserService {
return pSource.replace("&pound;&oslash;", "$");
}
protected String parseHeaders(final String pSource) {
String parseHeaders(final String pSource) {
String result = pSource;
for(int i = 1 ; i <= 3 ; i++) {
result = parseHeadersHX(result, i);
@@ -53,7 +52,7 @@ public class ParserService {
return result;
}
protected String parseHeadersHX(final String pSource, final int pNumHeader) {
String parseHeadersHX(final String pSource, final int pNumHeader) {
String result = pSource;
// (.*)(\[hX\](.*)\[\/hX\])+(.*)
@@ -73,11 +72,11 @@ public class ParserService {
return result;
}
protected String parseBackSpaces(final String pSource) {
String parseBackSpaces(final String pSource) {
return pSource.replaceAll("\r?\n", "<br/>").replaceAll("\\[\\/code\\]<br\\/><br\\/>", "[/code]\n");
}
protected String parseImages(final String pSource) {
String parseImages(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_IMAGES.matcher(result);
@@ -96,7 +95,7 @@ public class ParserService {
return result;
}
protected String parseLinks(final String pSource) {
String parseLinks(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_LINKS.matcher(result);

View File

@@ -17,15 +17,18 @@ import javax.swing.text.html.Option;
public class UserService {
private static final String MSG_BAD_CREDENTIALS = "Adresse email ou mot de passe incorrect.";
@Autowired
private UserRepository userRepository;
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User checkCredentials(final String email, final String password) throws BadCredentialsException {
final Optional<User> optUser = userRepository.findByEmail(email);
// If no user exists with the given email of if the given password doesn't match
// to the user password, we throw an exception.
if(!optUser.isPresent() || !StringUtils.compareHash(password, optUser.get().getPassword())) {
if(optUser.isEmpty() || !StringUtils.compareHash(password, optUser.get().getPassword())) {
throw new BadCredentialsException(MSG_BAD_CREDENTIALS);
}

View File

@@ -7,7 +7,7 @@ import java.util.Date;
import java.util.Locale;
public class DateUtils {
public static final String FORMAT_DEFAULT = "dd/MM/yyyy HH:mm:ss";
private static final String FORMAT_DEFAULT = "dd/MM/yyyy HH:mm:ss";
public static Date parseDate(String pSource, String pPattern) throws ParseException {
Date result = null;
@@ -27,7 +27,7 @@ public class DateUtils {
return getSimpleDateFormat(FORMAT_DEFAULT).format(pDate);
}
public static SimpleDateFormat getSimpleDateFormat(String pPattern) {
private static SimpleDateFormat getSimpleDateFormat(String pPattern) {
SimpleDateFormat result = new SimpleDateFormat(pPattern, Locale.FRENCH);
result.setLenient(false);
return result;

View File

@@ -52,9 +52,7 @@ public class ImageController {
}
@PostMapping
public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile pFile,
final HttpServletResponse pResponse,
final Principal pPrincipal) throws IOException {
public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile pFile, final Principal pPrincipal) {
ResponseEntity<String> result;
try {
result = ResponseEntity.status(HttpStatus.OK)

View File

@@ -37,7 +37,7 @@ public class ImageService {
* @param fileUploadService File upload service.
* @param imageRepository Image repository.
*/
public ImageService(UserService userService,
ImageService(UserService userService,
UserRepository userRepository,
FileUploadService fileUploadService,
ImageRepository imageRepository) {
@@ -86,11 +86,11 @@ public class ImageService {
return fileUploadService.loadAvatar(pAvatarFileName);
}
public Optional<Resource> loadImage(final String pImageLink) {
Optional<Resource> loadImage(final String pImageLink) {
return fileUploadService.loadImage(pImageLink);
}
public List<ImageDTO> getUserImages(final Principal pPrincipal) {
List<ImageDTO> getUserImages(final Principal pPrincipal) {
return imageRepository.getByUserId(userService.getUserByPrincipal(pPrincipal)
.map(User::getId)
.orElseThrow(NoSuchElementException::new))

View File

@@ -8,7 +8,6 @@ import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codiki.core.entities.dto.PostDTO;
@@ -19,7 +18,6 @@ import org.codiki.core.repositories.PostRepository;
import org.codiki.core.services.ParserService;
import org.codiki.core.services.UserService;
import org.codiki.core.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -35,7 +33,6 @@ import com.fasterxml.jackson.annotation.JsonView;
@RestController
@RequestMapping("/api/posts")
public class PostController {
private static final int LIMIT_POSTS_HOME = 20;
/** Service to parse post content. */
private ParserService parserService;
@@ -112,14 +109,11 @@ public class PostController {
@JsonView(View.PostDTO.class)
@GetMapping("/myPosts")
public List<Post> getMyPosts(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
final Principal pPrincipal) throws IOException {
public List<Post> getMyPosts(final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
List<Post> result = new LinkedList<>();
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) {
// result = postRepository.getByCreator(connectedUser.get().getId())
// .stream().map(PostDTO::new).collect(Collectors.toList());
result = postRepository.getByCreator(connectedUser.get().getId());
} else {
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
@@ -142,20 +136,21 @@ public class PostController {
@JsonView(View.PostDTO.class)
@PostMapping("/")
public Post insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) {
return postService.insert(pPost, pRequest, pResponse, pPrincipal).orElse(null);
public Post insert(@RequestBody final Post pPost,
final HttpServletResponse pResponse,
final Principal pPrincipal) {
pResponse.setStatus(postService.insert(pPost, pPrincipal));
return pPost;
}
@PutMapping("/")
public void update(@RequestBody final Post pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
postService.update(pPost, pRequest, pResponse, pPrincipal);
public void update(@RequestBody final Post pPost,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
postService.update(pPost, pResponse, pPrincipal);
}
@DeleteMapping("/{postKey}")
public void delete(@PathVariable("postKey") final String pPostKey, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
postService.delete(pPostKey, pRequest, pResponse, pPrincipal);
public void delete(@PathVariable("postKey") final String pPostKey, final Principal pPrincipal) {
postService.delete(pPostKey, pPrincipal);
}
}

View File

@@ -1,72 +1,59 @@
package org.codiki.posts;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.codiki.core.entities.business.SearchEntity;
import org.codiki.core.entities.dto.PostDTO;
import org.codiki.core.entities.persistence.Category;
import org.codiki.core.entities.persistence.Post;
import org.codiki.core.entities.persistence.User;
import org.codiki.core.repositories.CategoryRepository;
import org.codiki.core.repositories.PostRepository;
import org.codiki.core.services.IllegalAccessException;
import org.codiki.core.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class PostService {
static final int SCORE_TITLE = 50;
private static final int SCORE_TITLE = 50;
private static final int SCORE_DESCRIPTION = 5;
private static final int SCORE_TEXT = 1;
private static final int SCORE_CATEGORY = 30;
private static final int SCORE_AUTHOR_NAME = 40;
private static final int SCORE_AUTHOR_EMAIL = 40;
static final int SCORE_DESCRIPTION = 5;
private final PostRepository postRepository;
static final int SCORE_TEXT = 1;
private final CategoryRepository categoryRepository;
static final int SCORE_CATEGORY = 30;
private final UserService userService;
static final int SCORE_AUTHOR_NAME = 40;
static final int SCORE_AUTHOR_EMAIL = 40;
@Autowired
private PostRepository postRepository;
@Autowired
private CategoryRepository categoryRepository;
@Autowired
private UserService userService;
public Optional<Post> insert(final PostDTO pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) {
Optional<Post> result = Optional.empty();
final Optional<User> user = userService.getUserByPrincipal(pPrincipal);
if(user.isPresent()) {
final Post postToSave = new Post(pPost);
postToSave.setAuthor(user.get());
postRepository.save(postToSave);
result = Optional.of(postToSave);
} else {
pResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
return result;
/** Constructor. */
public PostService(PostRepository postRepository,
CategoryRepository categoryRepository,
UserService userService) {
this.postRepository = postRepository;
this.categoryRepository = categoryRepository;
this.userService = userService;
}
public void update(final Post pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
public int insert(final Post pPost, final Principal pPrincipal) {
return userService.getUserByPrincipal(pPrincipal)
.map(user -> {
pPost.setCreationDate(new Date());
pPost.setAuthor(user);
postRepository.save(pPost);
return HttpServletResponse.SC_OK;
})
.orElse(HttpServletResponse.SC_UNAUTHORIZED);
}
public void update(final Post pPost, final HttpServletResponse pResponse, final Principal pPrincipal)
throws IOException {
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent() && connectedUser.get().getKey().equals(pPost.getAuthor().getKey())) {
@@ -96,23 +83,19 @@ public class PostService {
}
}
public void delete(final String pPostKey, final HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final Optional<Post> postToDelete = postRepository.getByKey(pPostKey);
if(postToDelete.isPresent()) {
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) {
if(connectedUser.get().getKey().equals(postToDelete.get().getAuthor().getKey())) {
postRepository.delete(postToDelete.get());
} else {
pResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
}
} else {
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
} else {
pResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
public void delete(final String pPostKey, final Principal pPrincipal){
String userKey = userService.getUserByPrincipal(pPrincipal)
.map(User::getKey)
.orElseThrow(NoSuchElementException::new);
Post postToDelete = postRepository.getByKey(pPostKey)
.orElseThrow(NoSuchElementException::new);
if(postToDelete.getAuthor() == null || !postToDelete.getAuthor().getKey().equals(userKey)) {
throw new IllegalAccessException();
}
postRepository.delete(postToDelete);
}
public List<Post> search(String pSearchCriteria) {
@@ -123,12 +106,12 @@ public class PostService {
calculateScore(e, criteriasArray);
listSearchEntities.add(e);
});
Collections.sort(listSearchEntities, (e1, e2) -> e2.getScore() - e1.getScore());
listSearchEntities.sort((e1, e2) -> e2.getScore() - e1.getScore());
return listSearchEntities.stream().map(SearchEntity::getPost).collect(Collectors.toList());
}
void calculateScore(final SearchEntity searchPost, final String[] pCriteriasArray) {
private void calculateScore(final SearchEntity searchPost, final String[] pCriteriasArray) {
for(final String criteria : pCriteriasArray) {
String formattedCriteria = formatForComp(criteria);
@@ -141,41 +124,41 @@ public class PostService {
}
}
String formatForComp(final String pElem) {
private String formatForComp(final String pElem) {
return StringUtils.stripAccents(pElem.toLowerCase());
}
void calculateScoreForTitle(final SearchEntity pSearchPost, final String pCriteria) {
private void calculateScoreForTitle(final SearchEntity pSearchPost, final String pCriteria) {
if(formatForComp(pSearchPost.getPost().getTitle()).contains(pCriteria)) {
pSearchPost.increaseScore(SCORE_TITLE);
}
}
void calculateScoreForDescription(final SearchEntity pSearchPost, final String pCriteria) {
private void calculateScoreForDescription(final SearchEntity pSearchPost, final String pCriteria) {
final int criteriaOccurence = StringUtils.countMatches(formatForComp(pSearchPost.getPost().getDescription()),
pCriteria);
pSearchPost.increaseScore(criteriaOccurence * SCORE_DESCRIPTION);
}
void calculateScoreForText(final SearchEntity pSearchPost, final String pCriteria) {
private void calculateScoreForText(final SearchEntity pSearchPost, final String pCriteria) {
final int criteriaOccurence = StringUtils.countMatches(formatForComp(pSearchPost.getPost().getText()),
pCriteria);
pSearchPost.increaseScore(criteriaOccurence * SCORE_TEXT);
}
void calculateScoreForCategory(final SearchEntity pSearchPost, final String pCriteria) {
private void calculateScoreForCategory(final SearchEntity pSearchPost, final String pCriteria) {
if(formatForComp(pSearchPost.getPost().getCategory().getName()).contains(pCriteria)) {
pSearchPost.increaseScore(SCORE_CATEGORY);
}
}
void calculateScoreForAuthorName(final SearchEntity pSearchPost, final String pCriteria) {
private void calculateScoreForAuthorName(final SearchEntity pSearchPost, final String pCriteria) {
if(formatForComp(pSearchPost.getPost().getAuthor().getName()).contains(pCriteria)) {
pSearchPost.increaseScore(SCORE_AUTHOR_NAME);
}
}
void calculateScoreForAuthorEmail(final SearchEntity pSearchPost, final String pCriteria) {
private void calculateScoreForAuthorEmail(final SearchEntity pSearchPost, final String pCriteria) {
if(formatForComp(pSearchPost.getPost().getAuthor().getEmail()).contains(pCriteria)) {
pSearchPost.increaseScore(SCORE_AUTHOR_EMAIL);
}

View File

@@ -13,12 +13,14 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/versionrevisions")
public class VersionRevisionController {
private final VersionRevisionRepository repository;
private final VersionRepository versionRepository;
@Autowired
private VersionRevisionRepository repository;
@Autowired
private VersionRepository versionRepository;
public VersionRevisionController(VersionRevisionRepository repository,
VersionRepository versionRepository) {
this.repository = repository;
this.versionRepository = versionRepository;
}
@GetMapping("/versions")
public Iterable<Version> getVersions() {

View File

@@ -40,8 +40,7 @@ spring:
url: jdbc:postgresql://localhost:5432/codiki
username: codiki
password: P@ssword
# TODO: Delete all Lazy relationships and set following property to false
# open-in-view: false
open-in-view: false
# Because detection is disabled you have to set correct dialect by hand.
database-platform: org.hibernate.dialect.PostgreSQL9Dialect
# Disable feature detection by this undocumented parameter.

View File

@@ -3,4 +3,5 @@ INSERT INTO version (number) VALUES ('1.2.0');
INSERT INTO version_revision (version_id, text, bugfix) VALUES
(4, 'Migration vers Angular 8.2.', FALSE),
(4, 'Migration vers Java 11.', FALSE),
(4, 'Corrections mineures et améliorations de code.', TRUE);
(4, 'Corrections mineures et améliorations de code.', TRUE),
(4, 'Ajout d''un gestionnaire de notifications pop-ups.', FALSE);

View File

@@ -6,6 +6,8 @@ import { catchError } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';
import { NotificationsComponent } from '../notifications/notifications.component';
const REG_URL_LOGIN = /^https?:\/\/.*(:\d{1,5})?\/api\/account\/login$/;
@Injectable()
export class UnauthorizedInterceptor implements HttpInterceptor {
constructor(
@@ -18,7 +20,9 @@ export class UnauthorizedInterceptor implements HttpInterceptor {
if (err.status === 401) {
this.authService.setAnonymous();
this.router.navigate(['/login']);
window.setTimeout(() => NotificationsComponent.error('Veuillez vous authentifier pour réaliser cette action.'), 500);
if (!err.url.match(REG_URL_LOGIN)) {
window.setTimeout(() => NotificationsComponent.error('Veuillez vous authentifier pour réaliser cette action.'), 500);
}
}
const error = (err.error && err.error.message) || err.statusText;

View File

@@ -17,8 +17,16 @@
</a>
</li>
<li class="nav-item dropdown" dropdown *ngIf="isAuthenticated()">
<a dropdownToggle mdbRippleRadius type="button" class="nav-link dropdown-toggle waves-light" mdbRippleRadius>
<i class="fa fa-user-circle"></i> Mon Compte
<a dropdownToggle
mdbRippleRadius
type="button"
class="nav-link waves-light"
mdbTooltip="Mon compte"
placement="bottom"
mdbRippleRadius>
<img id="user-profile-img"
[src]="getConnectedUserImage()"
(error)="onUserProfileLoadingError($event)"/>
</a>
<div *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown dropdown-primary" role="menu">
<a class="dropdown-item waves-light" mdbRippleRadius routerLink="/myPosts">
@@ -47,7 +55,10 @@
<i class="fa fa-chevron-left"></i>
</a>
<div *ngFor="let category of listCategories">
<a [id]="'category-' + category.id" class="collapsible" *ngIf="category.listSubCategories.length" (click)="openCategoriesLinks(category)">
<a [id]="'category-' + category.id"
class="collapsible"
*ngIf="!!category.listSubCategories && category.listSubCategories.length"
(click)="openCategoriesLinks(category)">
{{category.name}} <i class="fa {{accordionOpenned[category.id] ? 'fa-chevron-down' : 'fa-chevron-right'}} float-right"></i>
</a>
<div class="categoriesLinks" >

View File

@@ -27,42 +27,48 @@
overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 20px; /* Place content 60px from the top */
transition: 0.3s; /* 0.5 second transition effect to slide in the sidenav */
}
/* The navigation menu links */
.sidenav a {
padding: 8px 32px 8px 32px;
text-decoration: none;
color: white;
display: block;
transition: 0.3s;
}
/* The navigation menu links */
a {
padding: 8px 32px 8px 32px;
text-decoration: none;
color: white;
display: block;
transition: 0.3s;
/* When you mouse over the navigation links, change their color */
.sidenav a:hover {
color: #ccc;
background-color: #5c6bc0;
}
/* When you mouse over the navigation links, change their color */
&:hover {
color: #ccc;
background-color: #5c6bc0;
}
}
.sidenav h3 {
padding: 8px 8px 8px 32px;
color: white;
padding-bottom: 25px;
border-bottom: 1px solid #5c6bc0;
}
/* Position and style the close button (top right corner) */
.sidenav .closebtn {
position: absolute;
top: 25px;
right: 25px;
margin-left: 50px;
padding: 8px;
h3 {
padding: 8px 8px 8px 32px;
color: white;
padding-bottom: 25px;
border-bottom: 1px solid #5c6bc0;
}
/* Position and style the close button (top right corner) */
.closebtn {
position: absolute;
top: 25px;
right: 25px;
margin-left: 50px;
padding: 8px;
}
}
/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
.sidenav {padding-top: 15px;}
.sidenav a {font-size: 18px;}
.sidenav {
padding-top: 15px;
a {
font-size: 18px;
}
}
}
#overlay {
@@ -74,8 +80,8 @@
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5); /* Black background with opacity */
z-index: 999; /* Specify a stack order in case you're using a different order for other elements */
background-color: rgba(0, 0, 0, 0.5); /* Black background with opacity */
z-index: 1031; /* Specify a stack order in case you're using a different order for other elements */
transition: 0.5s;
}
@@ -84,6 +90,10 @@
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
a {
padding-left: 50px;
}
}
.dropdown-item.danger-color-dark:hover {
@@ -93,3 +103,15 @@
.collapsible i.fa {
padding-top: 4px;
}
#user-profile-img {
filter: opacity(1);
transition: filter 0.3s;
border-radius: 50%;
width: 35px;
height: 35px;
&:hover {
filter: opacity(0.5);
}
}

View File

@@ -6,6 +6,7 @@ import { HeaderService } from './header.service';
import { environment } from '../../environments/environment';
const SIDENAV_WIDTH = '300px';
const DEFAULT_USER_ICON = '../../assets/images/default_user_icon.png';
@Component({
selector: 'app-header',
@@ -80,4 +81,13 @@ export class HeaderComponent implements OnInit {
this.accordionOpenned[category.id] = true;
}
}
getConnectedUserImage() {
const userImage = this.authService.getUser().image;
return !!userImage ? `./api/images/loadAvatar/${userImage}` : DEFAULT_USER_ICON;
}
onUserProfileLoadingError(event): void {
event.target.src = DEFAULT_USER_ICON;
}
}

View File

@@ -67,7 +67,7 @@ export class CreateUpdatePostComponent implements OnInit {
this.listCategories = [];
this.activatedTab = Tabs.EDITION;
this.createUpdatePostService.getCategories().subscribe(listCategories => {
this.listCategories = listCategories.filter(category => !category.listSubCategories.length);
this.listCategories = listCategories.filter(category => !category.listSubCategories);
});
const postKey = this.activatedRoute.snapshot.paramMap.get('postKey');
@@ -166,6 +166,7 @@ export class CreateUpdatePostComponent implements OnInit {
});
} else {
this.createUpdatePostService.addPost(this.model).subscribe(post => {
NotificationsComponent.success('Article enregistré');
this.router.navigate([`/posts/update/${post.key}`]);
}, error => {
console.log(error);

View File

@@ -7,5 +7,9 @@
<span *ngIf="listPosts?.length === 0">
Aucun article.
</span>
<a routerLink="/posts/new" class="fixed-action-btn green white-text">+</a>
<a routerLink="/posts/new"
class="fixed-action-btn green white-text"
mdbTooltip="Créer un article" placement="left">
+
</a>
</div>

View File

@@ -43,9 +43,6 @@
<div class="modal-body">
<div class="text-center">
<p>Êtes vous sûr de vouloir supprimer cet article ?</p>
<p *ngIf="postDeletionFailed" class="red-text">
Une erreur est survenue lors de la suppression de l'article.
</p>
</div>
</div>
<div class="modal-footer">

View File

@@ -1,3 +1,4 @@
import { NotificationsComponent } from 'src/app/core/notifications/notifications.component';
import { Component, OnInit, SecurityContext, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
@@ -38,9 +39,7 @@ export class PostComponent implements OnInit {
notFound: boolean;
owned: boolean;
@ViewChild('alertDelete', {static: true}) alertDelete;
postDeletionFailed: boolean;
@ViewChild('alertDelete', {static: false}) alertDelete;
constructor(
private activatedRoute: ActivatedRoute,
@@ -51,7 +50,6 @@ export class PostComponent implements OnInit {
) {
this.loaded = false;
this.owned = false;
this.postDeletionFailed = false;
}
ngOnInit(): void {
@@ -91,16 +89,13 @@ export class PostComponent implements OnInit {
}
deletePost(): void {
this.postDeletionFailed = false;
this.postService.deletePost(this.post).subscribe(() => {
NotificationsComponent.success('Article supprimé.');
this.alertDelete.hide();
this.router.navigate(['/myPosts']);
}, error => {
this.postDeletionFailed = true;
setTimeout(() => {
this.postDeletionFailed = false;
}, 3500);
console.error(error);
NotificationsComponent.error('Une erreur est survenue lors de la suppression de l\'article.');
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -6,7 +6,7 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<base href="/" />
<link rel="icon" type="image/x-icon" href="assets/images/favicon.png" />
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link href="./assets/css/prism.css" rel="stylesheet" />
</head>