27 Commits

Author SHA1 Message Date
cb340a7a78 Add categories and set v1.2.2. 2020-03-25 19:20:18 +01:00
c0fa7c40a6 Update app version in front app. 2020-01-21 21:56:52 +01:00
05a6b48901 Solve ssh connection problems for pipeline. 2020-01-18 15:51:29 +01:00
8dfdaedafb Update the codiki-properties credential id for git clone in pipeline. 2020-01-18 15:40:51 +01:00
12363fe362 Update codiki-properties repository path in pipeline. 2020-01-18 15:36:16 +01:00
3daeb789e1 Add version 1.2.1. 2020-01-18 15:29:58 +01:00
02e0bd4aa2 Update codiki-properties repository path in pipeline. 2020-01-18 15:15:06 +01:00
78dd79f792 Update jenkins tools in pipeline. 2020-01-18 15:11:07 +01:00
16471e9962 Big code cleaning. 2020-01-18 15:01:55 +01:00
6e7ae115e5 Add default user icon. 2020-01-18 14:31:19 +01:00
95af38bf72 Remove old error panel to use notifications. 2020-01-18 14:31:11 +01:00
5e5fbd9377 Add a control to avoid error notif at login failure. 2020-01-18 14:30:17 +01:00
84b1dd6352 Add a tooltip for post creation button. 2020-01-18 14:29:59 +01:00
1f8a11a73c Set a favicon.ico 2020-01-18 14:29:35 +01:00
7f0ff8157f Replace "my account" button by the user icon. 2020-01-18 14:29:15 +01:00
8ec806440b Add rule for resize images. 2019-08-08 22:24:55 +02:00
55f8ed986e Merge branch 'develop' 2019-08-08 22:16:45 +02:00
781eee090f Add robots.txt and anti-indexation tags. 2019-08-08 21:33:47 +02:00
e7583a59c2 [#1] Add error notification when an error attempts when post saving. 2019-08-08 20:59:17 +02:00
8205168bce Add a "JSONified" error management. 2019-08-08 20:52:31 +02:00
6bbb618f12 Refactor the post-card component html. 2019-08-08 20:25:27 +02:00
6027878622 Some front editions. 2019-08-08 20:24:53 +02:00
063d1de074 [#2] Correction of popup color. 2019-08-08 20:24:11 +02:00
2f63b798e5 Add unit tests for parser service. 2019-08-06 18:09:30 +02:00
dbfa55e0c4 Update user documentation for version 1.2.0. 2019-08-06 18:09:04 +02:00
aba59dd11f Update user documentation for version 1.2.0. 2019-08-06 17:37:40 +02:00
6bd293f08e Mettre à jour 'src/main/ts/src/environments/environment.prod.ts' 2019-08-06 13:26:44 +02:00
60 changed files with 788 additions and 357 deletions

Binary file not shown.

View File

@@ -2,8 +2,8 @@ pipeline {
agent any
tools {
maven 'Maven-3.5.0'
jdk 'OpenJdk-11.0.2'
maven 'Maven-3.6.0'
jdk 'OpenJdk-11.0.4'
}
environment {
@@ -14,7 +14,7 @@ pipeline {
stage('Configuration') {
steps {
dir('codiki-properties') {
git url:'https://gogs.takiguchi.ovh/florian/codiki-properties.git', branch: 'master', credentialsId: 'a6494064-8130-42fd-9d3d-e7734518c79e'
git url:'https://gitea.takiguchi.ovh/Codiki/codiki-properties.git', branch: 'master', credentialsId: 'Jenkins-gitea'
}
script {
sh 'cp ./codiki-properties/application-prod.yml ./src/main/resources/application.yml'
@@ -88,9 +88,9 @@ pipeline {
cleanRemote: false,
excludes: '',
execCommand: """
mv /opt/codiki-2/codiki.jar /opt/codiki-2/bin/codiki.jar
sudo /opt/codiki-2/bin/codiki-jenkins.sh
sudo service codiki-2 restart
mv /home/codiki/codiki.jar /opt/codiki/bin/codiki.jar
chmod 700 /opt/codiki/bin/codiki.jar
sudo service codiki restart
""",
execTimeout: 120000,
flatten: false,

View File

@@ -5,7 +5,7 @@
<groupId>org.codiki</groupId>
<artifactId>codiki</artifactId>
<version>1.2.0</version>
<version>1.2.2</version>
<packaging>jar</packaging>
<name>codiki</name>

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 {
@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;
private final CategoryRepository categoryRepository;
/** 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

@@ -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

@@ -0,0 +1,23 @@
package org.codiki.core.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
public class RobotsTxtController {
private static final Logger LOG = LoggerFactory.getLogger(RobotsTxtController.class);
@RequestMapping(value = "/robots.txt")
public void robots(HttpServletResponse response) {
try {
response.getWriter().write("User-agent: *\nDisallow: /\n");
} catch (IOException ex) {
LOG.info("Error during robots.txt serving.", ex);
}
}
}

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;
/* ******************* */
@@ -44,7 +46,8 @@ public class Category implements Serializable {
@ManyToOne(fetch = FetchType.LAZY)
@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)}
@@ -23,6 +21,6 @@ public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}

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;
@@ -45,27 +41,31 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// Permits all
.antMatchers(
"/api/account/login",
"/api/account/logout",
"/api/account/signin"
"/api/account/signin",
"/robots.txt"
).permitAll()
.antMatchers(
HttpMethod.GET,
"/api/categories",
"/api/images",
"/api/posts",
"/api/categories/**",
"/api/images/**",
"/api/posts/**"
).permitAll()
.antMatchers(
"/api/images/uploadAvatar",
"/api/images/myImages",
"/api/posts/myPosts",
"/api/account/changePassword",
"/api/account/"
"/api/account/",
"/api/posts/myPosts",
"/api/posts/preview",
"/api/posts/"
).authenticated()
.antMatchers(
HttpMethod.GET,
"/api/categories",
"/api/images",
"/api/posts",
"/api/categories/**",
"/api/images/**",
"/api/posts/**"
).permitAll()
.anyRequest().permitAll()
.and()
// Allow to avoid login form at authentication failure from Angular app

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,20 +9,31 @@ 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. */
private static final Logger LOG = LoggerFactory.getLogger(FileUploadService.class);
/** 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
* length.

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

@@ -16,16 +16,19 @@ import javax.swing.text.html.Option;
@Service
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;
static final int SCORE_DESCRIPTION = 5;
static final int SCORE_TEXT = 1;
static final int SCORE_CATEGORY = 30;
static final int SCORE_AUTHOR_NAME = 40;
static final int SCORE_AUTHOR_EMAIL = 40;
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;
@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;
private final PostRepository postRepository;
private final CategoryRepository categoryRepository;
private final UserService userService;
/** Constructor. */
public PostService(PostRepository postRepository,
CategoryRepository categoryRepository,
UserService userService) {
this.postRepository = postRepository;
this.categoryRepository = categoryRepository;
this.userService = userService;
}
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 HttpServletRequest pRequest,
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
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,13 +13,15 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/versionrevisions")
public class VersionRevisionController {
private final VersionRevisionRepository repository;
private final VersionRepository versionRepository;
public VersionRevisionController(VersionRevisionRepository repository,
VersionRepository versionRepository) {
this.repository = repository;
this.versionRepository = versionRepository;
}
@Autowired
private VersionRevisionRepository repository;
@Autowired
private VersionRepository versionRepository;
@GetMapping("/versions")
public Iterable<Version> getVersions() {
return versionRepository.findAllOrderByNumber();

View File

@@ -1,7 +1,7 @@
app:
name: Codiki
description: A wiki application.
version: 1.2.0
version: 1.2.2
platform: develop
codiki:
@@ -21,6 +21,9 @@ logging:
server:
# use-forward-headers=true
error:
whitelabel:
enabled: false # Disable html error responses.
port: 8080
# ssl:
# key-store: /home/takiguchi/Developpement/Java/codiki/keystore.p12
@@ -37,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);

5
src/main/sql/update_1.2.1.sql Executable file
View File

@@ -0,0 +1,5 @@
INSERT INTO version (number) VALUES ('1.2.1');
INSERT INTO version_revision (version_id, text, bugfix) VALUES
(5, 'Nettoyage de code.', TRUE),
(5, 'Modifications visuelles, notamment sur l''image de l''entête du site et le menu principal.', FALSE);

View File

@@ -0,0 +1,12 @@
INSERT INTO category (name, creator_id) VALUES
('Windows', 1),
('Outillage', 1);
INSERT INTO sub_category VALUES
(12, 2),
(13, 4);
INSERT INTO version (number) VALUES ('1.2.2');
INSERT INTO version_revision (version_id, text, bugfix) VALUES
(6, 'Ajout des catégories "Windows" et "Outillage".', FALSE);

View File

@@ -39,9 +39,9 @@
<a routerLink="/accountSettings" class="indigo-text">
Annuler
</a>
<button class="float-right waves-effect waves-light indigo btn"
<button class="float-right waves-effect waves-light indigo white-text btn"
type="submit" [disabled]="!profilEditionForm.form.valid">
Suivant
Enregistrer
</button>
</div>
</form>

View File

@@ -80,7 +80,7 @@ export class ProfilEditionComponent implements OnInit {
onSubmit(): void {
this.profilEditionService.updateUser(this.model).subscribe(() => {
NotificationsComponent.success('Modification enregistrée.');
NotificationsComponent.success('Modifications enregistrées.');
}, error => {
NotificationsComponent.error('L\'adresse mail saisie n\'est pas disponible.');
});

View File

@@ -4,6 +4,9 @@ import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/c
import { Observable, throwError } from 'rxjs';
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 {
@@ -17,6 +20,9 @@ export class UnauthorizedInterceptor implements HttpInterceptor {
if (err.status === 401) {
this.authService.setAnonymous();
this.router.navigate(['/login']);
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

@@ -0,0 +1,27 @@
<div class="post-card hoverable" routerLink="/posts/{{post.key}}">
<mdb-card>
<!-- Picture -->
<mdb-card-img [src]="post.image" alt="Article"></mdb-card-img>
<!-- Body -->
<mdb-card-body>
<!-- Title -->
<mdb-card-title>
<h4>{{post.title}}</h4>
</mdb-card-title>
<!-- Description -->
<mdb-card-text>
{{post.description}}
</mdb-card-text>
</mdb-card-body>
<!-- Footer -->
<div class="card-footer text-muted mt-4">
<img [src]="getAvatarUrl()"
class="author-img"
[alt]="post.author.name"
[mdbTooltip]="post.author.name"
placement="bottom"/>
Article écrit par {{post.author.name}}
<span class="creation-date-area">({{post.creationDate | date:'yyyy-MM-dd HH:mm:ss'}})</span>
</div>
</mdb-card>
</div>

View File

@@ -3,35 +3,11 @@ import { Post } from '../entities';
@Component({
selector: 'app-post-card',
template: `
<div class="card hoverable">
<div class="view hm-white-slight waves-light" mdbRippleRadius>
<img id="post-image" class="img-fluid" [src]="post.image" alt="Article" />
<a routerLink="/posts/{{post.key}}">
<div class="mask"></div>
</a>
</div>
<div class="card-body">
<h4 class="card-title">{{post.title}}</h4>
<p class="card-text">{{post.description}}</p>
</div>
<div class="card-data">
<img [src]="getAvatarUrl()"
class="author-img"
[alt]="post.author.name"
[mdbTooltip]="post.author.name"
placement="bottom"/>
Article écrit par {{post.author.name}}
<span class="creation-date-area">({{post.creationDate | date:'yyyy-MM-dd HH:mm:ss'}})</span>
</div>
</div>`,
templateUrl: './post-card.component.html',
styles: [`
div.card {
.post-card {
margin-bottom: 50px;
}
.card .card-data {
padding: 15px;
background-color: #f5f5f5;
cursor: pointer;
}
.creation-date-area {
color: #bdbdbd;

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');
@@ -159,11 +159,18 @@ export class CreateUpdatePostComponent implements OnInit {
if (this.model.key) {
this.createUpdatePostService.updatePost(this.model).subscribe(post => {
NotificationsComponent.error('Modification enregistrée');
NotificationsComponent.success('Modification enregistrée');
}, error => {
console.log(error);
NotificationsComponent.error('Une erreur est survenue lors de l\'enregistrement des modifications.');
});
} else {
this.createUpdatePostService.addPost(this.model).subscribe(post => {
NotificationsComponent.success('Article enregistré');
this.router.navigate([`/posts/update/${post.key}`]);
}, error => {
console.log(error);
NotificationsComponent.error('Une erreur est survenue lors de la création du post.');
});
}
} else {

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,5 +1,5 @@
export const environment = {
production: false,
appVersion: '1.2.0',
appVersion: '1.2.2',
title: 'Intégration'
};

View File

@@ -1,5 +1,5 @@
export const environment = {
production: true,
appVersion: '1.1.0',
appVersion: '1.2.2',
title: ''
};

View File

@@ -4,7 +4,7 @@
export const environment = {
production: false,
appVersion: '1.2.0',
appVersion: '1.2.2',
title: 'Développement'
};

View File

@@ -3,9 +3,10 @@
<head>
<title>Codiki</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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>

View File

@@ -74,3 +74,7 @@ body {
border: 1px solid rgba(0, 0, 0, 0.125) !important;
}
/* ***** End of Card style ***** */
img {
max-width: 100% !important;
}

View File

@@ -0,0 +1,330 @@
package org.codiki.core.services;
import org.apache.commons.lang.StringEscapeUtils;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class ParserServiceTest {
private ParserService parserService;
@Before
public void setUp() {
parserService = new ParserService();
}
/* *********************************************************** */
/* H E A D E R S P A R S I N G */
/* *********************************************************** */
@Test
public void testParseHeaders_h1() {
final String strInit = "[h1]Header[/h1]";
final String strExpected = "<h1>Header</h1>";
assertThat(parserService.parseHeaders(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseHeaders_h2() {
final String strInit = "[h2]Header[/h2]";
final String strExpected = "<h2>Header</h2>";
assertThat(parserService.parseHeaders(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseHeaders_h3() {
final String strInit = "[h3]Header[/h3]";
final String strExpected = "<h3>Header</h3>";
assertThat(parserService.parseHeaders(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseHeader_onlyCoupleOfTags() {
final String strInit = "[h1][h1]Test[/h1]";
final String strExpected = "[h1]<h1>Test</h1>";
assertThat(parserService.parseHeaders(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseHeader_multipleCouples() {
final String strInit = "[h1]Test1[/h1] [h1]Test2[/h1]";
final String strExpected = "<h1>Test1</h1> <h1>Test2</h1>";
assertThat(parserService.parseHeaders(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseHeaders() {
final String strInit = "[h1]Header1[/h1]\n" +
" Some text in header1\n" +
" [h2]Header2[/h2]\n" +
" Some text in header2\n" +
" [h3]Header3[/h3]\n" +
" Some text in header3\n" +
" [h3][h3]Header3[/h3]\n" +
" Some text in header3\n" +
" [h2]Header2[/h2]\n" +
" Some text in header2\n" +
" [h3]Header3[/h3][/h3]\n" +
" Some text in header3\n" +
" [h1]Header1[/h1]\n" +
" Some text in header1";
final String strExpected = "<h1>Header1</h1>\n" +
" Some text in header1\n" +
" <h2>Header2</h2>\n" +
" Some text in header2\n" +
" <h3>Header3</h3>\n" +
" Some text in header3\n" +
" [h3]<h3>Header3</h3>\n" +
" Some text in header3\n" +
" <h2>Header2</h2>\n" +
" Some text in header2\n" +
" <h3>Header3[/h3]</h3>\n" +
" Some text in header3\n" +
" <h1>Header1</h1>\n" +
" Some text in header1";
assertThat(parserService.parseHeaders(strInit)).isEqualTo(strExpected);
}
/* *********************************************************** */
/* B A C K S P A C E S P A R S I N G */
/* *********************************************************** */
@Test
public void testParseBackSpaces() {
final String strInit = "Hello\nworld!";
final String strExpected = "Hello<br/>world!";
assertThat(parserService.parseBackSpaces(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseBackSpaces_endTagCode() {
final String strInit = "[/code]\n\n";
final String strExpected = "[/code]\n";
assertThat(parserService.parseBackSpaces(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseBackSpaces_endTagCode_2() {
final String strInit = "[/code]\n";
final String strExpected = "[/code]<br/>";
assertThat(parserService.parseBackSpaces(strInit)).isEqualTo(strExpected);
}
/* *********************************************************** */
/* I M A G E S P A R S I N G */
/* *********************************************************** */
@Test
public void testParseImages_simpleImg() {
final String strInit = StringEscapeUtils.escapeHtml("[img src=\"pathToImage\" /]");
final String strExpected = "<img src=\"pathToImage\" alt=\"\" />";
assertThat(parserService.parseImages(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseImages_subTagAlt() {
final String strInit = StringEscapeUtils.escapeHtml("[img src=\"pathToImage\" alt=\"alternative\" /]");
final String strExpected = "<img src=\"pathToImage\" alt=\"alternative\" />";
assertThat(parserService.parseImages(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseImages_multipleImages() {
final String strInit = StringEscapeUtils.escapeHtml("[img src=\"pathToImage1\" /]\n" +
" [img src=\"pathToImage2\" /]");
final String strExpected = "<img src=\"pathToImage1\" alt=\"\" />\n" +
" <img src=\"pathToImage2\" alt=\"\" />";
assertThat(parserService.parseImages(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseImages() {
final String strInit = StringEscapeUtils.escapeHtml("[img src=\"pathToImage1\" /]\n" +
" [img src=\"pathToImage2\" alt=\"alternativeOfImage2\" /]\n" +
" [img src=\"pathToImage3\" lgd=\"legendOfImage3\" /]\n" +
" [img src=\"pathToImage4\" alt=\"alternativeOfImage4\" lgd=\"legendOfImage4\" /]\n" +
" [img src=\"pathToImage5\" \" /]\n" +
" [img src=\"pathToImage6\" src=\"pathToImage6_2\" /]\n" +
" [img src=\"pathToImage7\" alt=\"alternativeOfImage7\" alt=\"alternativeOfImage7_2\" /]\n" +
" [img src=\"pathToImage8\" alt=\"legendOfImage8\" alt=\"legendOfImage8_2\" /]\n" +
" [img alt=\"alternativeOfImage9\" src=\"pathToImage9\" lgd=\"legendOfImage9\" /]\n" +
" [img lgd=\"legendOfImage10\" src=\"pathToImage10\" alt=\"alternativeOfImage10\" /]\n" +
" [img lgd=\"legendOfImage11\" alt=\"alternativeOfImage11\" src=\"pathToImage11\" /]");
final String strExpected = "<img src=\"pathToImage1\" alt=\"\" />\n" +
" <img src=\"pathToImage2\" alt=\"alternativeOfImage2\" />\n" +
" [img src=&quot;pathToImage3&quot; lgd=&quot;legendOfImage3&quot; /]\n" +
" [img src=&quot;pathToImage4&quot; alt=&quot;alternativeOfImage4&quot; lgd=&quot;legendOfImage4&quot; /]\n" +
" [img src=&quot;pathToImage5&quot; &quot; /]\n" +
" [img src=&quot;pathToImage6&quot; src=&quot;pathToImage6_2&quot; /]\n" +
" [img src=&quot;pathToImage7&quot; alt=&quot;alternativeOfImage7&quot; alt=&quot;alternativeOfImage7_2&quot; /]\n" +
" [img src=&quot;pathToImage8&quot; alt=&quot;legendOfImage8&quot; alt=&quot;legendOfImage8_2&quot; /]\n" +
" [img alt=&quot;alternativeOfImage9&quot; src=&quot;pathToImage9&quot; lgd=&quot;legendOfImage9&quot; /]\n" +
" [img lgd=&quot;legendOfImage10&quot; src=&quot;pathToImage10&quot; alt=&quot;alternativeOfImage10&quot; /]\n" +
" [img lgd=&quot;legendOfImage11&quot; alt=&quot;alternativeOfImage11&quot; src=&quot;pathToImage11&quot; /]";
assertThat(parserService.parseImages(strInit)).isEqualTo(strExpected);
}
/* *********************************************************** */
/* L I N K S P A R S I N G */
/* *********************************************************** */
@Test
public void testParseLinks_simpleLink() {
final String strInit = StringEscapeUtils.escapeHtml("[link href=\"pathToLink\" txt=\"textOfLink\" /]");
final String strExpected = "<a href=\"pathToLink\">textOfLink</a>";
assertThat(parserService.parseLinks(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseLinks_multipleLinks() {
final String strInit = StringEscapeUtils.escapeHtml("[link href=\"pathToLink1\" txt=\"textOfLink1\" /]\n" +
" [link href=\"pathToLink2\" txt=\"textOfLink2\" /]");
final String strExpected = "<a href=\"pathToLink1\">textOfLink1</a>\n" +
" <a href=\"pathToLink2\">textOfLink2</a>";
assertThat(parserService.parseLinks(strInit)).isEqualTo(strExpected);
}
/* *********************************************************** */
/* C O D E P A R S I N G */
/* *********************************************************** */
@Test
public void testParseCode_simpleCode() {
final String strInit = parserService.parseBackSpaces(StringEscapeUtils.escapeHtml("[code lg=\"python\"]\n" +
"def foo():\n" +
" return \"Hello world!\"\n" +
"[/code]\n" +
"\n"));
final String strExpected = "<pre class=\"line-numbers\"><code class=\"language-python\">def foo():\n return &quot;Hello world!&quot;\n</code></pre>";
assertThat(parserService.parseCode(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseCode_multipleCodes() {
final String strInit = parserService.parseBackSpaces(StringEscapeUtils.escapeHtml("[code lg=\"python\"]\n" +
"def foo():\n" +
" return \"Hello world!\"\n" +
"[/code]\n" +
"\n" +
"[code lg=\"java\"]\n" +
"public static void main(final String... pArgs) {\n" +
" System.out.println(\"Hello world!\");\n" +
"}\n" +
"[/code]\n" +
"\n"));
final String strExpected = "<pre class=\"line-numbers\"><code class=\"language-python\">def foo():\n return &quot;Hello world!&quot;\n</code></pre><pre class=\"line-numbers\"><code class=\"language-java\">public static void main(final String... pArgs) {\n System.out.println(&quot;Hello world!&quot;);\n}\n</code></pre>";
assertThat(parserService.parseCode(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseLinks_backSlashAndCodeParsing() {
final String strInit = parserService.parseBackSpaces(StringEscapeUtils.escapeHtml("[code lg=\"python\"]\n" +
"def foo():\n" +
" return \"Hello world!\"\n" +
"[/code]\n" +
"\n"));
final String strExpected = "<pre class=\"line-numbers\"><code class=\"language-python\">def foo():\n return &quot;Hello world!&quot;\n</code></pre>";
assertThat(parserService.parseCode(strInit)).isEqualTo(strExpected);
}
@Test
public void testParseLinks_backSlashAndCodeParsing_multiple() {
final String strInit = parserService.parseBackSpaces(StringEscapeUtils.escapeHtml("[code lg=\"python\"]\n" +
"def foo():\n" +
" return \"Hello world!\"\n" +
"[/code]\n" +
"\n" +
"[code lg=\"java\"]\n" +
"public static void main(final String... pArgs) {\n" +
" System.out.println(\"Hello world!\");\n" +
"}\n" +
"[/code]\n" +
"\n"));
final String strExpected = "<pre class=\"line-numbers\"><code class=\"language-python\">def foo():\n return &quot;Hello world!&quot;\n</code></pre><pre class=\"line-numbers\"><code class=\"language-java\">public static void main(final String... pArgs) {\n System.out.println(&quot;Hello world!&quot;);\n}\n</code></pre>";
assertThat(parserService.parseCode(strInit)).isEqualTo(strExpected);
}
@Test
public void testParse() {
final String strInit = "\n" +
"[h1]Title 1[/h1]\n" +
"[h2]Title 2[/h2]\n" +
"[h3]Title 3[/h3]\n" +
"Some text on multiple\n" +
"lines\n" +
"like\n" +
"here!\n" +
"\n" +
"[img src=\"pathToImage\" alt=\"alternativeOfImage\" /]\n" +
"\n" +
"[code lg=\"java\"]\n" +
"public static void main(final String... pArgs) {\n" +
" System.out.println(\"Hello world!\");\n" +
"}\n" +
"[/code]\n" +
"\n" +
"\n" +
"[link href=\"pathToLink1\" txt=\"textOfLink1\" /]\n" +
"\n" +
"[img src=\"pathToImage2\" alt=\"alternativeOfImage2\" lgd=\"legendOfImage2\" /]\n" +
"[link href=\"pathToLink2\" txt=\"textOfLink2\" /]\n" +
"\n" +
"[code lg=\"python\"]\n" +
"def foo():\n" +
" return \"Hello world!\"\n" +
"[/code]\n" +
"\n" +
"";
final String strExpected = "<br/>" +
"<h1>Title 1</h1><br/>" +
"<h2>Title 2</h2><br/>" +
"<h3>Title 3</h3><br/>" +
"Some text on multiple<br/>" +
"lines<br/>" +
"like<br/>" +
"here!<br/>" +
"<br/>" +
"<img src=\"pathToImage\" alt=\"alternativeOfImage\" /><br/>" +
"<br/>" +
"<pre class=\"line-numbers\"><code class=\"language-java\">" +
"public static void main(final String... pArgs) {\n" +
" System.out.println(&quot;Hello world!&quot;);\n" +
"}\n" +
"</code></pre><br/>" +
"<a href=\"pathToLink1\">textOfLink1</a><br/>" +
"<br/>" +
"[img src=&quot;pathToImage2&quot; alt=&quot;alternativeOfImage2&quot; lgd=&quot;legendOfImage2&quot; /]<br/>" +
"<a href=\"pathToLink2\">textOfLink2</a><br/>" +
"<br/>" +
"<pre class=\"line-numbers\"><code class=\"language-python\">" +
"def foo():\n" +
" return &quot;Hello world!&quot;\n" +
"</code></pre>";
assertThat(parserService.parse(strInit)).isEqualTo(strExpected);
}
}