Merge branch 'integ'

This commit is contained in:
2019-02-19 20:50:36 +01:00
125 changed files with 12759 additions and 1320 deletions

4
.gitignore vendored
View File

@@ -25,5 +25,5 @@ nbdist/
### Angular ### ### Angular ###
src/main/resources/static/ src/main/resources/static/
src/main/ts/node_modules/ **/node_modules/
node/ node/

Binary file not shown.

13
pom.xml
View File

@@ -33,10 +33,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- <dependency> --> <dependency>
<!-- <groupId>org.springframework.boot</groupId> --> <groupId>org.springframework.boot</groupId>
<!-- <artifactId>spring-boot-starter-security</artifactId> --> <artifactId>spring-boot-starter-security</artifactId>
<!-- </dependency> --> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -48,6 +48,11 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt --> <!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt -->
<dependency> <dependency>
<groupId>org.mindrot</groupId> <groupId>org.mindrot</groupId>

View File

@@ -3,9 +3,11 @@ package org.codiki;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication @SpringBootApplication
@EnableAutoConfiguration @EnableAutoConfiguration
@ComponentScan(basePackages = "org.codiki")
public class CodikiApplication { public class CodikiApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -1,6 +1,7 @@
package org.codiki.account; package org.codiki.account;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.Optional; import java.util.Optional;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -8,9 +9,14 @@ import javax.servlet.http.HttpServletResponse;
import org.codiki.core.entities.dto.PasswordWrapperDTO; import org.codiki.core.entities.dto.PasswordWrapperDTO;
import org.codiki.core.entities.dto.UserDTO; import org.codiki.core.entities.dto.UserDTO;
import org.codiki.core.entities.dto.View;
import org.codiki.core.entities.persistence.User; import org.codiki.core.entities.persistence.User;
import org.codiki.core.security.TokenService; import org.codiki.core.services.UserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
@@ -18,43 +24,28 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonView;
@RestController @RestController
@RequestMapping("/api/account") @RequestMapping("/api/account")
public class AccountController { public class AccountController {
private static final String HEADER_TOKEN = "token";
@Autowired @Autowired
private AccountService accountService; private AccountService accountService;
@Autowired @Autowired
private TokenService tokenService; private UserService userService;
/** @JsonView(View.UserDTO.class)
* Log in the user in request body.
*
* @param pUser
* The user to connect.
* @param response
* The reponse injected by Spring.
* @return The connected user object.
* @throws IOException
* If credentials are bad.
*/
@PostMapping("/login") @PostMapping("/login")
public UserDTO login(@RequestBody UserDTO pUser, HttpServletResponse response) throws IOException { public User login(@RequestBody final User pUser) throws BadCredentialsException {
return accountService.checkCredentials(response, pUser); return accountService.authenticate(pUser);
} }
/**
* Log out the user.
*
* @param pRequest
* The request injected by Spring.
*/
@GetMapping("/logout") @GetMapping("/logout")
public void logout(HttpServletRequest pRequest) { public void logout(final HttpServletRequest request, final HttpServletResponse response) {
tokenService.removeUser(pRequest.getHeader(HEADER_TOKEN)); final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if(auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
} }
/** /**
@@ -74,8 +65,9 @@ public class AccountController {
@PutMapping("/changePassword") @PutMapping("/changePassword")
public void changePassword(@RequestBody final PasswordWrapperDTO pPasswordWrapper, public void changePassword(@RequestBody final PasswordWrapperDTO pPasswordWrapper,
final HttpServletRequest pRequest, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse,
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Principal pPrincipal) throws IOException {
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
accountService.changePassword(connectedUser.get(), pPasswordWrapper, pResponse); accountService.changePassword(connectedUser.get(), pPasswordWrapper, pResponse);
} else { } else {
@@ -84,13 +76,13 @@ public class AccountController {
} }
@PostMapping("/signin") @PostMapping("/signin")
public UserDTO signin(@RequestBody final UserDTO pUser, final HttpServletResponse pResponse) throws IOException { public void signin(@RequestBody final User pUser, final HttpServletResponse pResponse) throws IOException {
return accountService.signin(pUser, pResponse); accountService.signin(pUser, pResponse);
} }
@PutMapping("/") @PutMapping("/")
public void update(@RequestBody final UserDTO pUser, final HttpServletRequest pRequest, public void update(@RequestBody final UserDTO pUser, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
accountService.updateUser(pUser, pRequest, pResponse); accountService.updateUser(pUser, pRequest, pResponse, pPrincipal);
} }
} }

View File

@@ -1,13 +1,13 @@
package org.codiki.account; package org.codiki.account;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.naming.AuthenticationException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -17,53 +17,41 @@ import org.codiki.core.entities.dto.UserDTO;
import org.codiki.core.entities.persistence.User; import org.codiki.core.entities.persistence.User;
import org.codiki.core.repositories.ImageRepository; import org.codiki.core.repositories.ImageRepository;
import org.codiki.core.repositories.UserRepository; import org.codiki.core.repositories.UserRepository;
import org.codiki.core.security.TokenService; import org.codiki.core.security.CustomAuthenticationProvider;
import org.codiki.core.services.FileUploadService; import org.codiki.core.services.FileUploadService;
import org.codiki.core.services.UserService;
import org.codiki.core.utils.StringUtils; import org.codiki.core.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; 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.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Service @Service
public class AccountService { public class AccountService {
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Autowired
private UserService userService;
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@Autowired
private TokenService tokenService;
@Autowired @Autowired
private FileUploadService fileUploadService; private FileUploadService fileUploadService;
@Autowired @Autowired
private ImageRepository imageRepository; private ImageRepository imageRepository;
/** public User authenticate(final User pUser) throws BadCredentialsException {
* Check the user credentials and generate him a token if they are correct. final User user = userService.checkCredentials(pUser.getEmail(), pUser.getPassword());
*
* @param pUser
* The user sent from client.
* @return The user populated with the generated token.
* @throws IOException
* If the credentials are bad.
* @throws AuthenticationException
* If the credentials are wrong.
*/
public UserDTO checkCredentials(HttpServletResponse pResponse, UserDTO pUser) throws IOException {
UserDTO result = null;
Optional<User> user = userRepository.findByEmail(pUser.getEmail()); authenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
if(user.isPresent() && StringUtils.compareHash(pUser.getPassword(), user.get().getPassword())) { return user;
tokenService.addUser(user.get());
result = new UserDTO(user.get(), true);
} else {
pResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
}
return result;
} }
public void changePassword(final User pUser, final PasswordWrapperDTO pPasswordWrapper, public void changePassword(final User pUser, final PasswordWrapperDTO pPasswordWrapper,
@@ -85,11 +73,11 @@ public class AccountService {
} }
} }
public String uploadFile(final MultipartFile pFile, public String uploadFile(final MultipartFile pFile, final HttpServletRequest pRequest,
final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final String avatarFileName = fileUploadService.uploadProfileImage(pFile); final String avatarFileName = fileUploadService.uploadProfileImage(pFile);
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId()); final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId());
if(userFromDb.isPresent()) { if(userFromDb.isPresent()) {
@@ -109,10 +97,11 @@ public class AccountService {
return fileUploadService.loadAvatar(pAvatarFileName); return fileUploadService.loadAvatar(pAvatarFileName);
} }
public List<ImageDTO> getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { public List<ImageDTO> getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
final Principal pPrincipal) throws IOException {
List<ImageDTO> result = new LinkedList<>(); List<ImageDTO> result = new LinkedList<>();
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
result = imageRepository.getByUserId(connectedUser.get().getId()) result = imageRepository.getByUserId(connectedUser.get().getId())
.stream().map(ImageDTO::new).collect(Collectors.toList()); .stream().map(ImageDTO::new).collect(Collectors.toList());
@@ -123,27 +112,25 @@ public class AccountService {
return result; return result;
} }
public UserDTO signin(final UserDTO pUser, final HttpServletResponse pResponse) throws IOException { public void signin(final User pUser, final HttpServletResponse pResponse) throws IOException {
User user = new User();
if(pUser.getName() == null || pUser.getEmail() == null || pUser.getPassword() == null || "".equals(pUser.getPassword().trim())) { if(pUser.getName() == null || pUser.getEmail() == null || pUser.getPassword() == null || "".equals(pUser.getPassword().trim())) {
pResponse.sendError(HttpServletResponse.SC_BAD_REQUEST); pResponse.sendError(HttpServletResponse.SC_BAD_REQUEST);
} else if(userRepository.findByEmail(pUser.getEmail()).isPresent()) { } else if(userRepository.findByEmail(pUser.getEmail()).isPresent()) {
pResponse.sendError(HttpServletResponse.SC_CONFLICT); pResponse.sendError(HttpServletResponse.SC_CONFLICT);
} else { } else {
User user = new User();
user.setName(pUser.getName()); user.setName(pUser.getName());
user.setEmail(pUser.getEmail()); user.setEmail(pUser.getEmail());
user.setPassword(StringUtils.hashPassword(pUser.getPassword())); user.setPassword(StringUtils.hashPassword(pUser.getPassword()));
user.setInscriptionDate(new Date()); user.setInscriptionDate(new Date());
userRepository.save(user); userRepository.save(user);
} }
return new UserDTO(user);
} }
public void updateUser(final UserDTO pUser, final HttpServletRequest pRequest, public void updateUser(final UserDTO pUser, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final Optional<User> connectedUserOpt = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUserOpt = userService.getUserByPrincipal(pPrincipal);
if(connectedUserOpt.isPresent()) { if(connectedUserOpt.isPresent()) {
final User connectedUser = connectedUserOpt.get(); final User connectedUser = connectedUserOpt.get();

View File

@@ -1,129 +0,0 @@
package org.codiki.core;
import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codiki.core.security.Route;
import org.codiki.core.utils.StringUtils;
import org.springframework.http.HttpMethod;
/**
* Base class for all filters of the application.<br/>
* <br/>
* The children classes have to implements the method
* {@link AbstractFilter#getClass()} to set the URLs filtered (with all or some
* http methods), and the method
* {@link AbstractFilter#filter(HttpServletRequest, ServletResponse, FilterChain)}
* to define the filter processing.
*
* @author Takiguchi
*
*/
public abstract class AbstractFilter implements Filter {
/** Regex url path prefix for method {@link this#isRequestFiltered(String)}. */
private static final String PREFIX_URL_PATH = "https?:\\/\\/.*(:\\d{0,5})?";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Do nothing
}
/**
* Returns the list of routes which will be processed by the filter.
*
* @return The routes.
*/
protected abstract List<Route> getRoutes();
/**
* Filter actions for its processing.
*
* @param request
* The http request.
* @param response
* The response.
* @param chain
* The chain.
*/
protected abstract void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if(isRequestFiltered(httpRequest.getRequestURL().toString(), httpRequest.getMethod())) {
filter(httpRequest, (HttpServletResponse) response, chain);
} else {
chain.doFilter(request, response);
}
}
/**
* Check if the url is allowed with the given method in parameters.
*
* @param pRequestUrl
* The url request.
* @param pRequestMethod
* The http method of the request.
* @return {@code true} if the url is allowed with the method, {@code false}
* otherwise.
*/
boolean isRequestFiltered(final String pRequestUrl, final String pRequestMethod) {
boolean result = false;
for(final Route route : getRoutes()) {
/*
* Check urls matching, and if the method of the route isn't set, all methods
* are allowed. Otherwise, we check the methods too.
*/
if(Pattern.matches(StringUtils.concat(PREFIX_URL_PATH, route.getUrl()), pRequestUrl)) {
if(!route.getMethod().isPresent() || isMethodFiltered(route, pRequestMethod)) {
result = true;
break;
}
}
}
return result;
}
/**
* Checks if the route do filter the method in parameters.
*
* @param pRoute
* The registered route.
* @param pRequestMethod
* The http method to check with the registered route.
*/
boolean isMethodFiltered(final Route pRoute, final String pRequestMethod) {
boolean result = false;
if(pRoute.getMethod().isPresent()) {
for(final HttpMethod routeMethod : pRoute.getMethod().get()) {
if(routeMethod.name().equals(pRequestMethod)) {
result = true;
break;
}
}
}
return result;
}
@Override
public void destroy() {
// Do nothing
}
}

View File

@@ -1,20 +0,0 @@
package org.codiki.core.constant;
public enum FileEnum {
/** Folder in where pictures will be uploaded. */
FOLDER_UPLOAD("/opt/codiki/pictures/tmp/"),
/** Folder in where profile pictures will be stored. */
FOLDER_PROFILE_IMAGES("/opt/codiki/pictures/profiles/"),
/** Folder in where images will be stored. */
FOLDER_IMAGE("/opt/codiki/pictures/posts/");
private String value;
private FileEnum(final String pValue) {
value = pValue;
}
public String val() {
return value;
}
}

View File

@@ -21,8 +21,6 @@ public class UserDTO {
private Role role; private Role role;
private String token;
public UserDTO() { public UserDTO() {
super(); super();
} }
@@ -38,9 +36,6 @@ public class UserDTO {
public UserDTO(final User pUser, final boolean pWithToken) { public UserDTO(final User pUser, final boolean pWithToken) {
this(pUser); this(pUser);
if(pWithToken) {
token = pUser.getToken().getValue();
}
} }
public String getKey() { public String getKey() {
@@ -98,12 +93,4 @@ public class UserDTO {
public void setRole(Role role) { public void setRole(Role role) {
this.role = role; this.role = role;
} }
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
} }

View File

@@ -0,0 +1,6 @@
package org.codiki.core.entities.dto;
public class View {
public interface UserDTO {}
public interface PostDTO {}
}

View File

@@ -5,6 +5,7 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType; import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
@@ -16,6 +17,9 @@ import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import org.codiki.core.entities.dto.CategoryDTO; import org.codiki.core.entities.dto.CategoryDTO;
import org.codiki.core.entities.dto.View;
import com.fasterxml.jackson.annotation.JsonView;
@Entity @Entity
@Table(name="category") @Table(name="category")
@@ -28,14 +32,16 @@ public class Category implements Serializable {
/* ******************* */ /* ******************* */
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonView(View.PostDTO.class)
private Long id; private Long id;
@JsonView(View.PostDTO.class)
private String name; private String name;
/* ******************* */ /* ******************* */
/* Relations */ /* Relations */
/* ******************* */ /* ******************* */
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id") @JoinColumn(name = "creator_id")
protected User creator; protected User creator;

View File

@@ -6,6 +6,7 @@ import java.util.List;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType; import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
@@ -39,7 +40,7 @@ public class Comment implements Serializable {
/* ******************* */ /* ******************* */
/* Relations */ /* Relations */
/* ******************* */ /* ******************* */
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author") @JoinColumn(name = "author")
private User author; private User author;

View File

@@ -5,6 +5,7 @@ import java.util.Date;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType; import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
@@ -35,7 +36,7 @@ public class CommentHistory implements Serializable {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date updateDate; private Date updateDate;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "comment_id") @JoinColumn(name = "comment_id")
private Comment comment; private Comment comment;

View File

@@ -4,6 +4,7 @@ import java.io.Serializable;
import java.util.Date; import java.util.Date;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType; import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
@@ -30,7 +31,7 @@ public class Image implements Serializable {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date date; private Date date;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") @JoinColumn(name = "user_id")
private User user; private User user;

View File

@@ -19,10 +19,13 @@ import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import org.codiki.core.entities.dto.PostDTO; import org.codiki.core.entities.dto.PostDTO;
import org.codiki.core.entities.dto.View;
import org.codiki.core.utils.DateUtils; import org.codiki.core.utils.DateUtils;
import org.hibernate.annotations.Generated; import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime; import org.hibernate.annotations.GenerationTime;
import com.fasterxml.jackson.annotation.JsonView;
@Entity @Entity
@Table(name="post") @Table(name="post")
public class Post implements Serializable { public class Post implements Serializable {
@@ -38,19 +41,25 @@ public class Post implements Serializable {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@JsonView(View.PostDTO.class)
// This annotation serves to fetch the attribute after an insert into db // This annotation serves to fetch the attribute after an insert into db
@Generated(GenerationTime.ALWAYS) @Generated(GenerationTime.ALWAYS)
private String key; private String key;
@JsonView(View.PostDTO.class)
private String title; private String title;
@JsonView(View.PostDTO.class)
private String text; private String text;
@JsonView(View.PostDTO.class)
@Column(length = 250) @Column(length = 250)
private String description; private String description;
@JsonView(View.PostDTO.class)
private String image; private String image;
@JsonView(View.PostDTO.class)
@Column(name = "creation_date") @Column(name = "creation_date")
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date creationDate; private Date creationDate;
@@ -58,11 +67,13 @@ public class Post implements Serializable {
/* ******************* */ /* ******************* */
/* Relations */ /* Relations */
/* ******************* */ /* ******************* */
@ManyToOne(fetch = FetchType.EAGER) @JsonView(View.PostDTO.class)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id") @JoinColumn(name = "creator_id")
private User author; private User author;
@ManyToOne @JsonView(View.PostDTO.class)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id") @JoinColumn(name = "category_id")
private Category category; private Category category;

View File

@@ -5,6 +5,7 @@ import java.util.Date;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType; import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
@@ -31,7 +32,7 @@ public class PostHistory implements Serializable {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date updateDate; private Date updateDate;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id") @JoinColumn(name = "post_id")
private Post post; private Post post;

View File

@@ -8,15 +8,21 @@ import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import org.codiki.core.entities.dto.View;
import com.fasterxml.jackson.annotation.JsonView;
@Entity @Entity
@Table(name="role") @Table(name="role")
public class Role implements Serializable { public class Role implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@JsonView({View.UserDTO.class})
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@JsonView({View.UserDTO.class})
private String name; private String name;
public Long getId() { public Long getId() {

View File

@@ -3,6 +3,7 @@ package org.codiki.core.entities.persistence;
import java.util.List; import java.util.List;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.Table; import javax.persistence.Table;
@@ -15,7 +16,7 @@ public class SubCategory extends Category {
/* ******************* */ /* ******************* */
/* Relations */ /* Relations */
/* ******************* */ /* ******************* */
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "main_category") @JoinColumn(name = "main_category")
private Category mainCategory; private Category mainCategory;

View File

@@ -20,10 +20,12 @@ import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import org.codiki.core.entities.security.Token; import org.codiki.core.entities.dto.View;
import org.hibernate.annotations.Generated; import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime; import org.hibernate.annotations.GenerationTime;
import com.fasterxml.jackson.annotation.JsonView;
@Entity @Entity
@Table(name="`user`") @Table(name="`user`")
public class User implements Serializable { public class User implements Serializable {
@@ -36,19 +38,24 @@ public class User implements Serializable {
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="user_id_seq") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="user_id_seq")
@SequenceGenerator(name="user_id_seq", sequenceName="user_id_seq", allocationSize=1) @SequenceGenerator(name="user_id_seq", sequenceName="user_id_seq", allocationSize=1)
private Long id; private Long id;
@JsonView({View.UserDTO.class, View.PostDTO.class})
// This annotation serves to fetch the attribute after an insert into db // This annotation serves to fetch the attribute after an insert into db
@Generated(GenerationTime.ALWAYS) @Generated(GenerationTime.ALWAYS)
private String key; private String key;
@JsonView({View.UserDTO.class, View.PostDTO.class})
private String name; private String name;
@JsonView({View.UserDTO.class, View.PostDTO.class})
private String email; private String email;
private String password; private String password;
@JsonView({View.UserDTO.class, View.PostDTO.class})
private String image; private String image;
@JsonView({View.UserDTO.class, View.PostDTO.class})
@Column(name = "inscription_date") @Column(name = "inscription_date")
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date inscriptionDate; private Date inscriptionDate;
@@ -56,7 +63,8 @@ public class User implements Serializable {
/* ******************* */ /* ******************* */
/* Relations */ /* Relations */
/* ******************* */ /* ******************* */
@ManyToOne(fetch = FetchType.EAGER) @JsonView({View.UserDTO.class})
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id") @JoinColumn(name = "role_id")
private Role role; private Role role;
@@ -69,15 +77,11 @@ public class User implements Serializable {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Image> listImages; private List<Image> listImages;
/** Authentication token. */
private transient Token token;
/* ******************* */ /* ******************* */
/* Constructors */ /* Constructors */
/* ******************* */ /* ******************* */
public User() { public User() {
super(); super();
token = new Token();
} }
/* ******************* */ /* ******************* */
@@ -184,9 +188,4 @@ public class User implements Serializable {
public void setListImages(List<Image> listImages) { public void setListImages(List<Image> listImages) {
this.listImages = listImages; this.listImages = listImages;
} }
public Token getToken() {
return token;
}
} }

View File

@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.codiki.core.entities.persistence.Post; import org.codiki.core.entities.persistence.Post;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@@ -15,9 +16,9 @@ public interface PostRepository extends CrudRepository<Post, Long>, PostSearchRe
@Query("SELECT p FROM Post p WHERE p.key = :postKey") @Query("SELECT p FROM Post p WHERE p.key = :postKey")
Optional<Post> getByKey(@Param("postKey") final String pPostKey); Optional<Post> getByKey(@Param("postKey") final String pPostKey);
@Query(value = "SELECT * FROM post p INNER JOIN \"user\" u ON u.id = p.creator_id ORDER BY p.creation_date DESC LIMIT :limit", // TODO : Remove "JOIN FETCH u.role", actually necessary for JSON serialization
nativeQuery = true) @Query("SELECT p FROM Post p JOIN FETCH p.author u JOIN FETCH u.role ORDER BY p.creationDate DESC")
List<Post> getLast(@Param("limit") final Integer pLimit); List<Post> getLast(Pageable pPageable);
@Query(value = "SELECT * FROM post p INNER JOIN \"user\" u ON u.id = creator_id WHERE category_id = :categoryId ORDER BY creation_date DESC", @Query(value = "SELECT * FROM post p INNER JOIN \"user\" u ON u.id = creator_id WHERE category_id = :categoryId ORDER BY creation_date DESC",
nativeQuery = true) nativeQuery = true)

View File

@@ -1,50 +0,0 @@
package org.codiki.core.security;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codiki.core.AbstractFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AuthenticationFilter extends AbstractFilter {
private static final String HTTP_OPTIONS = "OPTIONS";
private static final String HEADER_TOKEN = "token";
@Autowired
private TokenService tokenService;
@Override
protected List<Route> getRoutes() {
return Arrays.asList(
new Route("\\/api\\/posts\\/myPosts"),
new Route("\\/api\\/posts\\/preview"),
new Route("\\/api\\/posts\\/", HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE),
new Route("\\/api\\/account\\/changePassword"),
new Route("\\/api\\/account\\/", HttpMethod.PUT)
);
}
@Override
protected void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if(HTTP_OPTIONS.equals(request.getMethod()) || tokenService.isUserConnected(request.getHeader(HEADER_TOKEN))) {
chain.doFilter(request, response);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}

View File

@@ -0,0 +1,33 @@
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;
@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>());
// Set the auth bean in spring security context
SecurityContextHolder.getContext().setAuthentication(auth);
return auth;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

View File

@@ -0,0 +1,28 @@
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;
/**
* Authentication entry point configured in
* {@link SecurityConfiguration#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)}
* to avoid yo get a login form at authentication failure from Angular app.
*
* @author takiguchi
*
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}

View File

@@ -1,71 +0,0 @@
package org.codiki.core.security;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpMethod;
/**
* Route for filter matching.
*
* @author Takiguchi
*
*/
public class Route {
/** The regex to match urls. */
private String url;
/** The http method to match. Use a {@link Optional#empty()} to match all methods. */
private Optional<List<HttpMethod>> method;
/**
* Instanciate a vierge route.
*/
public Route() {
super();
url = "";
method = Optional.empty();
}
/**
* Instanciate a route for all http methods.
*
* @param pUrl
* The regex to match urls.
*/
public Route(final String pUrl) {
this();
this.url = pUrl;
}
/**
* Instanciate a route for methods given in parameters
*
* @param pUrl
* The regex to match urls.
* @param pMethod
* The http method to match. Use a {@link Optional#empty()} to match
* all methods.
*/
public Route(final String pUrl, final HttpMethod... pMethods) {
this(pUrl);
this.method = Optional.of(Arrays.asList(pMethods));
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Optional<List<HttpMethod>> getMethod() {
return method;
}
public void setMethod(HttpMethod pMethods) {
this.method = Optional.of(Arrays.asList(pMethods));
}
}

View File

@@ -0,0 +1,75 @@
package org.codiki.core.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String XSRF_REPOSITORY_HEADER_NAME = "X-XSRF-TOKEN";
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Autowired
private RestAuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(
"/api/account/login",
"/api/account/logout",
"/api/account/signin"
).permitAll()
.antMatchers(
"/api/images/uploadAvatar",
"/api/images/myImages",
"/api/posts/myPosts"
).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
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.addFilterAfter(new XSRFTokenFilter(), CsrfFilter.class)
.csrf()
.csrfTokenRepository(xsrfTokenRepository());
http.httpBasic();
http.csrf().disable();
}
private CsrfTokenRepository xsrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName(XSRF_REPOSITORY_HEADER_NAME);
return repository;
}
}

View File

@@ -1,132 +0,0 @@
package org.codiki.core.security;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import javax.servlet.http.HttpServletRequest;
import org.codiki.core.entities.persistence.User;
import org.springframework.stereotype.Service;
@Service
public class TokenService {
/** Map of connected users. */
private static final Map<String, User> connectedUsers;
private static final String HEADER_TOKEN = "token";
private static final long INTERVAL_USER_CLEANING = 5;
private static final long INTERVAL_USER_CLEANING_VAL = INTERVAL_USER_CLEANING * 60 * 1000;
private Date lastUsersCleaning;
/**
* Class constructor
*/
static {
connectedUsers = new TreeMap<>();
}
/**
* Check if the token matches to a user session, and if it is still valid.
*
* @param pToken
* The token to check.
* @return {@code true} if the token is still valid, {@code false}
* otherwise.
*/
public boolean isUserConnected(final String pToken) {
boolean result = false;
if (pToken != null && connectedUsers.containsKey(pToken)) {
if (connectedUsers.get(pToken).getToken().isValid()) {
result = true;
} else {
connectedUsers.remove(pToken);
}
}
// clear all the expired sessions
final Date now = new Date();
if(lastUsersCleaning == null || now.getTime() - lastUsersCleaning.getTime() >= INTERVAL_USER_CLEANING_VAL) {
new Thread(this::clearExpiredUsers).start();
lastUsersCleaning = now;
}
return result;
}
/**
* Remove from the connected users map all the elements which their token is
* expired.
*/
private void clearExpiredUsers() {
synchronized (this) {
List<User> usersToRemove = new LinkedList<>();
connectedUsers.entrySet().stream().forEach(user -> {
if(!user.getValue().getToken().isValid()) {
usersToRemove.add(user.getValue());
}
});
usersToRemove.stream().map(User::getKey).forEach(connectedUsers::remove);
}
}
/**
* Add the user to the connected users map.
*
* @param pUser
* The user to add.
*/
public void addUser(final User pUser) {
if(connectedUsers.get(pUser.getToken().getValue()) == null) {
connectedUsers.put(pUser.getToken().getValue(), pUser);
}
}
/**
* Refresh the user token last access date in the token service.
*
* @param pToken
* The user token.
*/
public void refreshUserToken(final String pToken) {
final User user = connectedUsers.get(pToken);
if(user != null) {
user.getToken().setLastAccess();
}
}
/**
* Remove the user to the connected users map.
*
* @param pUser
* The user to remove.
*/
public void removeUser(final User pUser) {
removeUser(pUser.getToken().getValue());
}
/**
* Remove the user associated to the token given in parameters, from the
* connected users map.
*
* @param pToken
* The user to delete token.
*/
public void removeUser(final String pToken) {
if(pToken != null && connectedUsers.containsKey(pToken)) {
connectedUsers.remove(pToken);
}
}
public Optional<User> getAuthenticatedUserByToken(final HttpServletRequest pRequest) {
return Optional.ofNullable(connectedUsers.get(pRequest.getHeader(HEADER_TOKEN)));
}
}

View File

@@ -0,0 +1,34 @@
package org.codiki.core.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
public class XSRFTokenFilter extends OncePerRequestFilter {
private static final String XSRF_TOKEN_PATH = "/";
private static final String XSRF_TOKEN = "XSRF-TOKEN";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if(csrf != null) {
Cookie cookie = WebUtils.getCookie(request, XSRF_TOKEN);
String token = csrf.getToken();
if(cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie(XSRF_TOKEN, token);
cookie.setPath(XSRF_TOKEN_PATH);
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -6,7 +6,9 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.RandomStringUtils;
import org.codiki.core.constant.FileEnum; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource; import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -14,9 +16,18 @@ import org.springframework.web.multipart.MultipartFile;
@Service @Service
public class FileUploadService { public class FileUploadService {
/** Logger. */
private static final Logger LOG = LoggerFactory.getLogger(FileUploadService.class);
/** Length of uploaded file names. */ /** Length of uploaded file names. */
private static final int DESTINATION_IMAGE_NAME_LENGTH = 30; 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;
/** /**
* Builds the destination file name, which is a random string with 30 char * Builds the destination file name, which is a random string with 30 char
* length. * length.
@@ -28,36 +39,39 @@ public class FileUploadService {
} }
public String uploadProfileImage(final MultipartFile pFile) { public String uploadProfileImage(final MultipartFile pFile) {
return uploadFile(pFile, FileEnum.FOLDER_PROFILE_IMAGES); return uploadFile(pFile, folderProfileImages);
} }
public String uploadImage(final MultipartFile pFile) { public String uploadImage(final MultipartFile pFile) {
return uploadFile(pFile, FileEnum.FOLDER_IMAGE); return uploadFile(pFile, folderImages);
} }
private String uploadFile(final MultipartFile pFile, final FileEnum pPath) { private String uploadFile(final MultipartFile pFile, final String pPath) {
String result; String result;
try { try {
result = buildDestinationFileName(); result = buildDestinationFileName();
Files.copy(pFile.getInputStream(), Paths.get(pPath.val()).resolve(result)); final Path destinationFile = Paths.get(pPath).resolve(result);
LOG.debug("Upload file saved as {}", destinationFile.toString());
Files.copy(pFile.getInputStream(), destinationFile);
return result; return result;
} catch (final Exception pEx) { } catch (final Exception pEx) {
LOG.error("Error during file upload.", pEx);
// TODO : Refactor exception // TODO : Refactor exception
throw new RuntimeException(); throw new RuntimeException();
} }
} }
public Resource loadAvatar(final String pAvatarFileName) { public Resource loadAvatar(final String pAvatarFileName) {
return loadImage(pAvatarFileName, FileEnum.FOLDER_PROFILE_IMAGES); return loadImage(pAvatarFileName, folderProfileImages);
} }
public Resource loadImage(final String pImageLink) { public Resource loadImage(final String pImageLink) {
return loadImage(pImageLink, FileEnum.FOLDER_IMAGE); return loadImage(pImageLink, folderImages);
} }
private Resource loadImage(final String pImageLink, final FileEnum pFilePath) { private Resource loadImage(final String pImageLink, final String pFilePath) {
try { try {
Path imageFile = Paths.get(pFilePath.val()).resolve(pImageLink); Path imageFile = Paths.get(pFilePath).resolve(pImageLink);
Resource imageResource = new UrlResource(imageFile.toUri()); Resource imageResource = new UrlResource(imageFile.toUri());
if(imageResource.exists() || imageResource.isReadable()) { if(imageResource.exists() || imageResource.isReadable()) {
return imageResource; return imageResource;

View File

@@ -0,0 +1,37 @@
package org.codiki.core.services;
import java.security.Principal;
import java.util.Optional;
import org.codiki.core.entities.persistence.User;
import org.codiki.core.repositories.UserRepository;
import org.codiki.core.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private static final String MSG_BAD_CREDENTIALS = "Adresse email ou mot de passe incorrect.";
@Autowired
private 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())) {
throw new BadCredentialsException(MSG_BAD_CREDENTIALS);
}
return optUser.get();
}
public Optional<User> getUserByPrincipal(final Principal pPrincipal) {
SecurityContextHolder.getContext().getAuthentication();
return userRepository.findByEmail(pPrincipal.getName());
}
}

View File

@@ -1,6 +1,7 @@
package org.codiki.images; package org.codiki.images;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -8,6 +9,8 @@ import javax.servlet.http.HttpServletResponse;
import org.codiki.core.entities.dto.ImageDTO; import org.codiki.core.entities.dto.ImageDTO;
import org.codiki.core.utils.StringUtils; import org.codiki.core.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -24,18 +27,21 @@ import org.springframework.web.multipart.MultipartFile;
@RestController @RestController
@RequestMapping("/api/images") @RequestMapping("/api/images")
public class ImageController { public class ImageController {
private static final Logger LOG = LoggerFactory.getLogger(ImageController.class);
@Autowired @Autowired
private ImageService imageService; private ImageService imageService;
@PostMapping("/uploadAvatar") @PostMapping("/uploadAvatar")
public ResponseEntity<String> uploadAvatar(@RequestParam("file") MultipartFile pFile, public ResponseEntity<String> uploadAvatar(@RequestParam("file") MultipartFile pFile,
final HttpServletRequest pRequest, final HttpServletResponse pResponse) { final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Principal pPrincipal) {
LOG.debug("Upload avatar.");
ResponseEntity<String> result; ResponseEntity<String> result;
try { try {
result = ResponseEntity.status(HttpStatus.OK) result = ResponseEntity.status(HttpStatus.OK)
.body(imageService.uploadAvatar(pFile, pRequest, pResponse)); .body(imageService.uploadAvatar(pFile, pRequest, pResponse, pPrincipal));
} catch(final Exception pEx) { } catch(final Exception pEx) {
LOG.error("Error during avatar upload.", pEx);
result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED) result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
.body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + ".")); .body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + "."));
} }
@@ -44,11 +50,11 @@ public class ImageController {
@PostMapping @PostMapping
public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile pFile, public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile pFile,
final HttpServletRequest pRequest, final HttpServletResponse pResponse) { final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Principal pPrincipal) {
ResponseEntity<String> result; ResponseEntity<String> result;
try { try {
result = ResponseEntity.status(HttpStatus.OK) result = ResponseEntity.status(HttpStatus.OK)
.body(imageService.uploadImage(pFile, pRequest, pResponse)); .body(imageService.uploadImage(pFile, pRequest, pResponse, pPrincipal));
} catch(final Exception pEx) { } catch(final Exception pEx) {
result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED) result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
.body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + ".")); .body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + "."));
@@ -73,8 +79,9 @@ public class ImageController {
} }
@GetMapping("/myImages") @GetMapping("/myImages")
public List<ImageDTO> myImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { public List<ImageDTO> myImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
return imageService.getUserImages(pRequest, pResponse); final Principal pPrincipal) throws IOException {
return imageService.getUserImages(pRequest, pResponse, pPrincipal);
} }
@GetMapping("/{imageLink}/details") @GetMapping("/{imageLink}/details")

View File

@@ -1,6 +1,7 @@
package org.codiki.images; package org.codiki.images;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -15,8 +16,8 @@ import org.codiki.core.entities.persistence.Image;
import org.codiki.core.entities.persistence.User; import org.codiki.core.entities.persistence.User;
import org.codiki.core.repositories.ImageRepository; import org.codiki.core.repositories.ImageRepository;
import org.codiki.core.repositories.UserRepository; import org.codiki.core.repositories.UserRepository;
import org.codiki.core.security.TokenService;
import org.codiki.core.services.FileUploadService; import org.codiki.core.services.FileUploadService;
import org.codiki.core.services.UserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -24,24 +25,23 @@ import org.springframework.web.multipart.MultipartFile;
@Service @Service
public class ImageService { public class ImageService {
@Autowired
private UserService userService;
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@Autowired
private TokenService tokenService;
@Autowired @Autowired
private FileUploadService fileUploadService; private FileUploadService fileUploadService;
@Autowired @Autowired
private ImageRepository imageRepository; private ImageRepository imageRepository;
public String uploadAvatar(final MultipartFile pFile, public String uploadAvatar(final MultipartFile pFile, final HttpServletRequest pRequest,
final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final String avatarFileName = fileUploadService.uploadProfileImage(pFile); final String avatarFileName = fileUploadService.uploadProfileImage(pFile);
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId()); final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId());
if(userFromDb.isPresent()) { if(userFromDb.isPresent()) {
@@ -57,11 +57,11 @@ public class ImageService {
return avatarFileName; return avatarFileName;
} }
public String uploadImage(final MultipartFile pFile, public String uploadImage(final MultipartFile pFile, final HttpServletRequest pRequest,
final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final String imageFileName = fileUploadService.uploadImage(pFile); final String imageFileName = fileUploadService.uploadImage(pFile);
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId()); final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId());
if(userFromDb.isPresent()) { if(userFromDb.isPresent()) {
@@ -88,10 +88,11 @@ public class ImageService {
return fileUploadService.loadImage(pImageLink); return fileUploadService.loadImage(pImageLink);
} }
public List<ImageDTO> getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { public List<ImageDTO> getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
final Principal pPrincipal) throws IOException {
List<ImageDTO> result = new LinkedList<>(); List<ImageDTO> result = new LinkedList<>();
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
result = imageRepository.getByUserId(connectedUser.get().getId()) result = imageRepository.getByUserId(connectedUser.get().getId())
.stream().map(ImageDTO::new).collect(Collectors.toList()); .stream().map(ImageDTO::new).collect(Collectors.toList());

View File

@@ -1,6 +1,7 @@
package org.codiki.posts; package org.codiki.posts;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -11,12 +12,14 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.codiki.core.entities.dto.PostDTO; import org.codiki.core.entities.dto.PostDTO;
import org.codiki.core.entities.dto.View;
import org.codiki.core.entities.persistence.Post; import org.codiki.core.entities.persistence.Post;
import org.codiki.core.entities.persistence.User; import org.codiki.core.entities.persistence.User;
import org.codiki.core.repositories.PostRepository; import org.codiki.core.repositories.PostRepository;
import org.codiki.core.security.TokenService;
import org.codiki.core.services.ParserService; import org.codiki.core.services.ParserService;
import org.codiki.core.services.UserService;
import org.springframework.beans.factory.annotation.Autowired; 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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -26,6 +29,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonView;
@RestController @RestController
@RequestMapping("/api/posts") @RequestMapping("/api/posts")
public class PostController { public class PostController {
@@ -39,111 +44,115 @@ public class PostController {
private PostRepository postRepository; private PostRepository postRepository;
@Autowired @Autowired
private TokenService tokenService; private PostService postService;
@Autowired @Autowired
private PostService postService; private UserService userService;
@GetMapping @GetMapping
public List<PostDTO> getAll() { public List<PostDTO> getAll() {
return StreamSupport.stream(postRepository.findAll().spliterator(), false) return StreamSupport.stream(postRepository.findAll().spliterator(), false)
.map(PostDTO::new).collect(Collectors.toList()); .map(PostDTO::new).collect(Collectors.toList());
} }
@JsonView(View.PostDTO.class)
@GetMapping("/{postKey}") @GetMapping("/{postKey}")
public PostDTO getByKey(@PathVariable("postKey") final String pPostKey, public Post getByKey(@PathVariable("postKey") final String pPostKey,
final HttpServletResponse response) { final HttpServletResponse response) {
final PostDTO result = getByKeyAndSource(pPostKey, response); final Post result = getByKeyAndSource(pPostKey, response);
if(result != null) { if(result != null) {
result.setText(parserService.parse(result.getText())); result.setText(parserService.parse(result.getText()));
} }
return result; return result;
} }
@JsonView(View.PostDTO.class)
@GetMapping("/{postKey}/source") @GetMapping("/{postKey}/source")
public PostDTO getByKeyAndSource(@PathVariable("postKey")final String pPostKey, public Post getByKeyAndSource(@PathVariable("postKey")final String pPostKey,
final HttpServletResponse response) { final HttpServletResponse response) {
PostDTO result = null; Post result = null;
final Optional<Post> post = postRepository.getByKey(pPostKey); final Optional<Post> post = postRepository.getByKey(pPostKey);
if(post.isPresent()) { if(post.isPresent()) {
result = new PostDTO(post.get()); result = post.get();
} else { } else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.setStatus(HttpServletResponse.SC_NOT_FOUND);
} }
return result; return result;
} }
@JsonView(View.PostDTO.class)
@GetMapping("/last") @GetMapping("/last")
public List<PostDTO> getLast() { public List<Post> getLast() {
return postRepository.getLast(LIMIT_POSTS_HOME).stream() return postRepository.getLast(PageRequest.of(0, LIMIT_POSTS_HOME));
.map(PostDTO::new).collect(Collectors.toList());
} }
@JsonView(View.PostDTO.class)
@GetMapping("/search/{searchCriteria}") @GetMapping("/search/{searchCriteria}")
public List<PostDTO> search(@PathVariable("searchCriteria") final String pSearchCriteria) { public List<Post> search(@PathVariable("searchCriteria") final String pSearchCriteria) {
return postService.search(pSearchCriteria); return postService.search(pSearchCriteria);
} }
@JsonView(View.PostDTO.class)
@GetMapping("/byCategory/{categoryId}") @GetMapping("/byCategory/{categoryId}")
public List<PostDTO> getByCategory(@PathVariable("categoryId") final Long pCategoryId) { public List<Post> getByCategory(@PathVariable("categoryId") final Long pCategoryId) {
return postRepository.getByCategoryId(pCategoryId).stream() return postRepository.getByCategoryId(pCategoryId);
.map(PostDTO::new).collect(Collectors.toList());
} }
@JsonView(View.PostDTO.class)
@GetMapping("/myPosts") @GetMapping("/myPosts")
public List<PostDTO> getMyPosts(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { public List<Post> getMyPosts(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
List<PostDTO> result = new LinkedList<>(); final Principal pPrincipal) throws IOException {
List<Post> result = new LinkedList<>();
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
result = postRepository.getByCreator(connectedUser.get().getId()) // result = postRepository.getByCreator(connectedUser.get().getId())
.stream().map(PostDTO::new).collect(Collectors.toList()); // .stream().map(PostDTO::new).collect(Collectors.toList());
result = postRepository.getByCreator(connectedUser.get().getId());
} else { } else {
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} }
return result; return result;
} }
@JsonView(View.PostDTO.class)
@PostMapping("/preview") @PostMapping("/preview")
public PostDTO preview(@RequestBody final PostDTO pPost) { public Post preview(@RequestBody final Post pPost) {
final PostDTO result = new PostDTO(); pPost.setImage(pPost.getImage() == null || "".equals(pPost.getImage())
result.setTitle(pPost.getTitle());
result.setImage(pPost.getImage() == null || "".equals(pPost.getImage())
? "https://news-cdn.softpedia.com/images/news2/this-is-the-default-wallpaper-of-the-gnome-3-20-desktop-environment-500743-2.jpg" ? "https://news-cdn.softpedia.com/images/news2/this-is-the-default-wallpaper-of-the-gnome-3-20-desktop-environment-500743-2.jpg"
: pPost.getImage()); : pPost.getImage());
result.setDescription(pPost.getDescription()); pPost.setText(parserService.parse(pPost.getText()));
result.setText(parserService.parse(pPost.getText()));
return result; return pPost;
} }
@JsonView(View.PostDTO.class)
@PostMapping("/") @PostMapping("/")
public PostDTO insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest, public Post insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) { final HttpServletResponse pResponse, final Principal pPrincipal) {
PostDTO result = null; Post result = null;
Optional<Post> postCreated = postService.insert(pPost, pRequest, pResponse); Optional<Post> postCreated = postService.insert(pPost, pRequest, pResponse, pPrincipal);
if(postCreated.isPresent()) { if(postCreated.isPresent()) {
result = new PostDTO(postCreated.get()); result = postCreated.get();
} }
return result; return result;
} }
@PutMapping("/") @PutMapping("/")
public void update(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest, public void update(@RequestBody final Post pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
postService.update(pPost, pRequest, pResponse); postService.update(pPost, pRequest, pResponse, pPrincipal);
} }
@DeleteMapping("/{postKey}") @DeleteMapping("/{postKey}")
public void delete(@PathVariable("postKey") final String pPostKey, public void delete(@PathVariable("postKey") final String pPostKey, final HttpServletRequest pRequest,
final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
postService.delete(pPostKey, pRequest, pResponse); postService.delete(pPostKey, pRequest, pResponse, pPrincipal);
} }
} }

View File

@@ -1,6 +1,7 @@
package org.codiki.posts; package org.codiki.posts;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
@@ -19,7 +20,7 @@ import org.codiki.core.entities.persistence.Post;
import org.codiki.core.entities.persistence.User; import org.codiki.core.entities.persistence.User;
import org.codiki.core.repositories.CategoryRepository; import org.codiki.core.repositories.CategoryRepository;
import org.codiki.core.repositories.PostRepository; import org.codiki.core.repositories.PostRepository;
import org.codiki.core.security.TokenService; import org.codiki.core.services.UserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -44,13 +45,13 @@ public class PostService {
private CategoryRepository categoryRepository; private CategoryRepository categoryRepository;
@Autowired @Autowired
private TokenService tokenService; private UserService userService;
public Optional<Post> insert(final PostDTO pPost, final HttpServletRequest pRequest, public Optional<Post> insert(final PostDTO pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) { final HttpServletResponse pResponse, final Principal pPrincipal) {
Optional<Post> result = Optional.empty(); Optional<Post> result = Optional.empty();
final Optional<User> user = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> user = userService.getUserByPrincipal(pPrincipal);
if(user.isPresent()) { if(user.isPresent()) {
final Post postToSave = new Post(pPost); final Post postToSave = new Post(pPost);
@@ -64,9 +65,9 @@ public class PostService {
return result; return result;
} }
public void update(final PostDTO pPost, final HttpServletRequest pRequest, public void update(final Post pPost, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent() && connectedUser.get().getKey().equals(pPost.getAuthor().getKey())) { if(connectedUser.isPresent() && connectedUser.get().getKey().equals(pPost.getAuthor().getKey())) {
final Optional<Post> postOpt = postRepository.getByKey(pPost.getKey()); final Optional<Post> postOpt = postRepository.getByKey(pPost.getKey());
@@ -96,10 +97,10 @@ public class PostService {
} }
public void delete(final String pPostKey, final HttpServletRequest pRequest, public void delete(final String pPostKey, final HttpServletRequest pRequest,
final HttpServletResponse pResponse) throws IOException { final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
final Optional<Post> postToDelete = postRepository.getByKey(pPostKey); final Optional<Post> postToDelete = postRepository.getByKey(pPostKey);
if(postToDelete.isPresent()) { if(postToDelete.isPresent()) {
final Optional<User> connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
if(connectedUser.isPresent()) { if(connectedUser.isPresent()) {
if(connectedUser.get().getKey().equals(postToDelete.get().getAuthor().getKey())) { if(connectedUser.get().getKey().equals(postToDelete.get().getAuthor().getKey())) {
postRepository.delete(postToDelete.get()); postRepository.delete(postToDelete.get());
@@ -114,7 +115,7 @@ public class PostService {
} }
} }
public List<PostDTO> search(String pSearchCriteria) { public List<Post> search(String pSearchCriteria) {
final String[] criteriasArray = pSearchCriteria.split(" "); final String[] criteriasArray = pSearchCriteria.split(" ");
final List<SearchEntity> listSearchEntities = new LinkedList<>(); final List<SearchEntity> listSearchEntities = new LinkedList<>();
@@ -124,7 +125,7 @@ public class PostService {
}); });
Collections.sort(listSearchEntities, (e1, e2) -> e2.getScore() - e1.getScore()); Collections.sort(listSearchEntities, (e1, e2) -> e2.getScore() - e1.getScore());
return listSearchEntities.stream().map(SearchEntity::getPost).map(PostDTO::new).collect(Collectors.toList()); return listSearchEntities.stream().map(SearchEntity::getPost).collect(Collectors.toList());
} }
void calculateScore(final SearchEntity searchPost, final String[] pCriteriasArray) { void calculateScore(final SearchEntity searchPost, final String[] pCriteriasArray) {

View File

@@ -10,8 +10,14 @@ spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
# Because detection is disabled you have to set correct dialect by hand. # Because detection is disabled you have to set correct dialect by hand.
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
logging.level.org.hibernate=DEBUG #logging.level.org.hibernate=DEBUG
spring.servlet.multipart.max-file-size=104857600 spring.servlet.multipart.max-file-size=104857600
codiki.files.upload=/opt/codiki/pictures/tmp
codiki.files.profile-images=/opt/codiki/pictures/profiles
codiki.files.images=/opt/codiki/pictures/posts
logging.level.org.codiki=DEBUG
logging.path=/opt/codiki/logs
logging.file=codiki
cors.enabled=false cors.enabled=false

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property resource="application.properties" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>DENY</onMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
</filter>
<encoder>
<pattern>%date %-5level [%-10.10thread] %-40.40logger{40} %msg%n</pattern>
</encoder>
</appender>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder>
<pattern>%date %-5level [%-10.10thread] %-40.40logger{40} %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logging.path}/${logging.file}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logging.path}/${logging.file}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>5MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date %-5level [%-10.10thread] %-40.40logger{40} %msg%n</pattern>
</encoder>
</appender>
<logger name="org.codiki" level="INFO"/>
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="STDERR"/>
<appender-ref ref="FILE"/>
<!-- <springProfile name="production">
<appender-ref ref="FILE"/>
</springProfile> -->
</root>
</configuration>

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

@@ -0,0 +1,5 @@
INSERT INTO version (number) VALUES ('1.1.0');
INSERT INTO version_revision (version_id, text, bugfix) VALUES
(3, 'Migration vers Angular 7.', FALSE),
(3, 'Correction d''anomalies graphiques mineures.', TRUE);

View File

@@ -1,67 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "codiki-bootstrap-material"
},
"apps": [
{
"root": "src",
"outDir": "../resources/static/",
"assets": [
"assets",
"favicon.png"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"../node_modules/font-awesome/scss/font-awesome.scss",
"../node_modules/angular-bootstrap-md/scss/bootstrap/bootstrap.scss",
"../node_modules/angular-bootstrap-md/scss/mdb-free.scss",
"./styles.scss"
],
"scripts": [
"../node_modules/chart.js/dist/Chart.js",
"../node_modules/hammerjs/hammer.min.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"integ": "environments/environment.integ.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"component": {}
}
}

View File

@@ -3,7 +3,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = tab
indent_size = 2 indent_size = 2
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true

39
src/main/ts/.gitignore vendored Executable file
View File

@@ -0,0 +1,39 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

22
src/main/ts/.vscode/launch.json vendored Executable file → Normal file
View File

@@ -5,12 +5,28 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "ng serve", "type": "firefox",
"request": "launch",
"reAttach": true,
"name": "Launch Firefox",
"url": "http://localhost:4200/",
"webRoot": "${workspaceFolder}/"
},
{
"type": "chrome", "type": "chrome",
"request": "launch", "request": "launch",
"url": "http://localhost:4200/#", "name": "Launch Chrome",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}", "webRoot": "${workspaceFolder}",
"runtimeExecutable": "/usr/bin/chromium" "sourceMaps": true,
"runtimeExecutable": "/usr/bin/chromium-browser",
}, },
{
"name": "Firefox debugger attach",
"type": "firefox",
"request": "attach",
"url": "http://localhost:4200/",
"webRoot": "${workspaceFolder}"
}
] ]
} }

View File

@@ -1,27 +0,0 @@
# CodikiBootstrapMaterial
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.1.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

157
src/main/ts/angular.json Executable file
View File

@@ -0,0 +1,157 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ts-v7": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "../resources/static/",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss",
"node_modules/@fortawesome/fontawesome-free/scss/solid.scss",
"node_modules/@fortawesome/fontawesome-free/scss/regular.scss",
"node_modules/@fortawesome/fontawesome-free/scss/brands.scss",
"node_modules/angular-bootstrap-md/scss/bootstrap/bootstrap.scss",
"node_modules/angular-bootstrap-md/scss/mdb-free.scss",
"src/styles.scss"
],
"scripts": [
"node_modules/chart.js/dist/Chart.js",
"node_modules/hammerjs/hammer.min.js"
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
},
"integ": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.integ.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "ts-v7:build"
},
"configurations": {
"production": {
"browserTarget": "ts-v7:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ts-v7:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.scss"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"ts-v7-e2e": {
"root": "e2e/",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "ts-v7:serve"
},
"configurations": {
"production": {
"devServerTarget": "ts-v7:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "ts-v7"
}

View File

@@ -6,7 +6,7 @@ const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = { exports.config = {
allScriptsTimeout: 11000, allScriptsTimeout: 11000,
specs: [ specs: [
'./e2e/**/*.e2e-spec.ts' './src/**/*.e2e-spec.ts'
], ],
capabilities: { capabilities: {
'browserName': 'chrome' 'browserName': 'chrome'
@@ -21,8 +21,8 @@ exports.config = {
}, },
onPrepare() { onPrepare() {
require('ts-node').register({ require('ts-node').register({
project: 'e2e/tsconfig.e2e.json' project: require('path').join(__dirname, './tsconfig.e2e.json')
}); });
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
} }
}; };

View File

@@ -0,0 +1,14 @@
import { AppPage } from './app.po';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to ts-v7!');
});
});

11
src/main/ts/e2e/src/app.po.ts Executable file
View File

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

View File

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

11407
src/main/ts/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,54 @@
{ {
"name": "codiki-bootstrap-material", "name": "ts-v7",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build --prod", "build": "ng build",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^5.2.0", "@angular/animations": "^7.2.2",
"@angular/common": "^5.2.0", "@angular/common": "^7.2.2",
"@angular/compiler": "^5.2.0", "@angular/compiler": "^7.2.2",
"@angular/core": "^5.2.0", "@angular/core": "^7.2.2",
"@angular/forms": "^5.2.0", "@angular/forms": "^7.2.2",
"@angular/http": "^5.2.0", "@angular/http": "^7.2.2",
"@angular/platform-browser": "^5.2.0", "@angular/platform-browser": "^7.2.2",
"@angular/platform-browser-dynamic": "^5.2.0", "@angular/platform-browser-dynamic": "^7.2.2",
"@angular/router": "^5.2.0", "@angular/router": "^7.2.2",
"angular-bootstrap-md": "^5.2.3", "@fortawesome/fontawesome-free": "^5.6.3",
"chart.js": "^2.5.0", "@types/chart.js": "^2.7.42",
"core-js": "^2.4.1", "angular-bootstrap-md": "^7.3.0",
"font-awesome": "^4.7.0", "chart.js": "^2.7.3",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"ng2-handy-syntax-highlighter": "^1.0.12", "rxjs": "~6.3.3",
"rxjs": "^5.5.6", "tslib": "^1.9.0",
"zone.js": "^0.8.19" "zone.js": "~0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@angular/cli": "~1.7.1", "@angular-devkit/build-angular": "~0.12.0",
"@angular/compiler-cli": "^5.2.0", "@angular/cli": "~7.2.3",
"@angular/language-service": "^5.2.0", "@angular/compiler-cli": "^7.2.2",
"@types/jasmine": "~2.8.3", "@angular/language-service": "^7.2.2",
"@types/jasminewd2": "~2.0.2", "@types/jasmine": "^2.8.16",
"@types/node": "~6.0.60", "@types/jasminewd2": "~2.0.3",
"codelyzer": "^4.0.1", "@types/node": "~8.9.4",
"jasmine-core": "~2.8.0", "codelyzer": "~4.3.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.0", "karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0", "karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^1.2.1", "karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.0", "karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2", "karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2", "protractor": "~5.4.0",
"ts-node": "~4.1.0", "ts-node": "~7.0.0",
"tslint": "~5.9.1", "tslint": "~5.11.0",
"typescript": "~2.5.3" "typescript": "~3.2.4"
} }
} }

6
src/main/ts/proxy.conf.json Executable file
View File

@@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:8080",
"secure": false
}
}

View File

View File

View File

@@ -3,7 +3,7 @@
<h4 class="card-title">Mot de passe</h4> <h4 class="card-title">Mot de passe</h4>
<form (ngSubmit)="onSubmit()" #changePasswordForm="ngForm"> <form (ngSubmit)="onSubmit()" #changePasswordForm="ngForm">
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="oldPassword" id="oldPassword"
name="oldPassword" name="oldPassword"
type="password" type="password"
@@ -11,12 +11,12 @@
[(ngModel)]="model.oldPassword" [(ngModel)]="model.oldPassword"
#oldPassword="ngModel" #oldPassword="ngModel"
data-error="Veuillez saisir votre mot de passe actuel" data-error="Veuillez saisir votre mot de passe actuel"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="oldPassword">Mot de passe actuel</label> <label for="oldPassword">Mot de passe actuel</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="newPassword" id="newPassword"
name="newPassword" name="newPassword"
type="password" type="password"
@@ -24,12 +24,12 @@
[(ngModel)]="model.newPassword" [(ngModel)]="model.newPassword"
#newPassword="ngModel" #newPassword="ngModel"
data-error="Veuillez saisir votre nouveau mot de passe" data-error="Veuillez saisir votre nouveau mot de passe"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="newPassword">Nouveau mot de passe</label> <label for="newPassword">Nouveau mot de passe</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="confirmPassword" id="confirmPassword"
name="confirmPassword" name="confirmPassword"
type="password" type="password"
@@ -37,11 +37,11 @@
[(ngModel)]="model.confirmPassword" [(ngModel)]="model.confirmPassword"
#confirmPassword="ngModel" #confirmPassword="ngModel"
data-error="Veuillez confirmer votre nouveau mot de passe" data-error="Veuillez confirmer votre nouveau mot de passe"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="confirmPassword">Confirmation de votre mot de passe</label> <label for="confirmPassword">Confirmation de votre mot de passe</label>
</div> </div>
<div class="card red lighten-2 text-center z-depth-2" *ngIf="error"> <div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body"> <div class="card-body">
<p class="white-text mb-0">{{error}}</p> <p class="white-text mb-0">{{error}}</p>
</div> </div>
@@ -58,4 +58,3 @@
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { PasswordWrapper } from '../../core/entities'; import { PasswordWrapper } from '../../core/entities';
import { ChangePasswordService } from './change-password.service'; import { ChangePasswordService } from './change-password.service';
import { FormGroup, Validators, FormBuilder } from '@angular/forms'; import { RouteReuseStrategy, Router } from '@angular/router';
@Component({ @Component({
selector: 'app-change-password', selector: 'app-change-password',
@@ -14,24 +14,47 @@ import { FormGroup, Validators, FormBuilder } from '@angular/forms';
.submitFormArea { .submitFormArea {
line-height: 50px; line-height: 50px;
} }
#errorMsg {
max-height: 0;
overflow: hidden;
transition: max-height 0.5s ease-out;
margin: 0;
}
`] `]
}) })
export class ChangePasswordComponent { export class ChangePasswordComponent {
model: PasswordWrapper = new PasswordWrapper('', '', ''); model: PasswordWrapper = new PasswordWrapper('', '', '');
error: string; error: string;
constructor( constructor(
private router: Router,
private changePasswordService: ChangePasswordService private changePasswordService: ChangePasswordService
) {} ) {}
onSubmit(): void { onSubmit(): void {
if (this.model.newPassword !== this.model.confirmPassword) { if (this.model.newPassword !== this.model.confirmPassword) {
this.error = 'Les mots de passe saisis ne correspondent pas.'; this.setMessage('Les mots de passe saisis ne correspondent pas.');
} else { } else {
this.changePasswordService.changePassword(this.model).subscribe(null, error => { this.changePasswordService.changePassword(this.model).subscribe(() => {
this.error = 'Le mot de passe saisi ne correspond pas au votre.'; this.router.navigate(['/accountSettings']);
}, error => {
this.setMessage('Le mot de passe saisi ne correspond pas au votre.');
}); });
} }
} }
setMessage(message: string): void {
this.error = message;
const resultMsgDiv = document.getElementById('errorMsg');
resultMsgDiv.style.maxHeight = '64px';
setTimeout(() => {
resultMsgDiv.style.maxHeight = '0px';
setTimeout(() => {
this.error = undefined;
}, 550);
}, 3000);
}
} }

View File

@@ -1,10 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { PasswordWrapper } from '../../core/entities'; import { PasswordWrapper } from '../../core/entities';
import { environment } from '../../../environments/environment';
const ACCOUNT_URL = environment.apiUrl + '/api/account';
@Injectable() @Injectable()
export class ChangePasswordService { export class ChangePasswordService {
@@ -12,6 +10,6 @@ export class ChangePasswordService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
changePassword(passwordWrapper: PasswordWrapper): Observable<any> { changePassword(passwordWrapper: PasswordWrapper): Observable<any> {
return this.http.put(ACCOUNT_URL + '/changePassword', passwordWrapper); return this.http.put('/api/account/changePassword', passwordWrapper);
} }
} }

View File

@@ -1,60 +1,60 @@
<input id="profilImageInput" type="file" (change)="uploadImage($event)" [hidden]="true"> <input id="profilImageInput" type="file" (change)="uploadImage($event)" [hidden]="true">
<div class="col-md-8 offset-md-2 col-lg-6 offset-lg-3"> <div class="col-md-8 offset-md-2 col-lg-6 offset-lg-3">
<div class="card"> <mdb-card>
<img id="profil-image" class="card-img-top" <img id="profil-image" class="card-img-top"
(click)="triggerProfilImageChange()" (click)="triggerProfilImageChange()"
[src]="getAvatarUrl()" [src]="getAvatarUrl()"
alt="Card image cap" alt="Card image cap"
mdbTooltip="Modifier mon image de profil" mdbTooltip="Modifier mon image de profil"
placement="bottom"> placement="bottom">
<div id="form" class="card-body"> <mdb-card-body id="form">
<form (ngSubmit)="onSubmit()" #profilEditionForm="ngForm"> <form (ngSubmit)="onSubmit()" #profilEditionForm="ngForm">
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="name" id="name"
name="name" name="name"
type="text" type="text"
class="form-control" class="form-control"
[(ngModel)]="model.name" [(ngModel)]="model.name"
#name="ngModel" #name="ngModel"
data-error="Veuillez saisir votre nom d'utilisateur" data-error="Veuillez saisir votre nom d'utilisateur"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="name">Nom d'utilisateur</label> <label for="name">Nom d'utilisateur</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="email" id="email"
name="email" name="email"
type="email" type="email"
class="form-control" class="form-control"
[(ngModel)]="model.email" [(ngModel)]="model.email"
#email="ngModel" #email="ngModel"
data-error="Veuillez saisir votre adresse email" data-error="Veuillez saisir votre adresse email"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="email">Email</label> <label for="email">Email</label>
</div> </div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2"> <div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body"> <div class="card-body">
<p class="white-text mb-0">{{modelError}}</p> <p class="white-text mb-0">{{modelError}}</p>
</div> </div>
</div> </div>
<div id="resultMsg" class="card green lighten-2 text-center z-depth-2" > <div id="resultMsg" class="card green lighten-2 text-center z-depth-2" >
<div class="card-body"> <div class="card-body">
<p class="white-text mb-0">{{result}}</p> <p class="white-text mb-0">{{result}}</p>
</div> </div>
</div> </div>
<div class="col submitFormArea"> <div class="col submitFormArea">
<a routerLink="/accountSettings" class="indigo-text"> <a routerLink="/accountSettings" class="indigo-text">
Annuler Annuler
</a> </a>
<button class="float-right waves-effect waves-light indigo btn" <button class="float-right waves-effect waves-light indigo btn"
type="submit" [disabled]="!profilEditionForm.form.valid"> type="submit" [disabled]="!profilEditionForm.form.valid">
Suivant Suivant
</button> </button>
</div> </div>
</form> </form>
</div> </mdb-card-body>
</div> </mdb-card>
</div> </div>

View File

@@ -3,8 +3,6 @@ import { ProfilEditionService } from './profil-edition.service';
import { HttpEventType, HttpResponse } from '@angular/common/http'; import { HttpEventType, HttpResponse } from '@angular/common/http';
import { User } from '../../core/entities'; import { User } from '../../core/entities';
import { AuthService } from '../../core/services/auth.service'; import { AuthService } from '../../core/services/auth.service';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'app-profil-edition', selector: 'app-profil-edition',
@@ -49,7 +47,7 @@ export class ProfilEditionComponent implements OnInit {
getAvatarUrl(): string { getAvatarUrl(): string {
return this.model.image return this.model.image
? `${environment.apiUrl}/api/images/loadAvatar/${this.model.image}` ? `/api/images/loadAvatar/${this.model.image}`
: './assets/images/default_user.png'; : './assets/images/default_user.png';
} }
@@ -70,7 +68,8 @@ export class ProfilEditionComponent implements OnInit {
} else if (result instanceof HttpResponse) { } else if (result instanceof HttpResponse) {
console.log('File ' + result.body + ' completely uploaded!'); console.log('File ' + result.body + ' completely uploaded!');
this.model.image = result.body as string; this.model.image = result.body as string;
this.authService.setUser(this.model); this.authService.setAuthenticated(this.model);
this.setMessage('Image de profil modifiée.', false);
} }
this.selectedFiles = undefined; this.selectedFiles = undefined;
}); });

View File

@@ -1,12 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {HttpClient, HttpRequest, HttpEvent} from '@angular/common/http'; import {HttpClient, HttpRequest, HttpEvent} from '@angular/common/http';
import {Observable} from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { User } from '../../core/entities'; import { User } from '../../core/entities';
const IMAGES_URL = environment.apiUrl + '/api/images';
const ACCOUNT_URL = environment.apiUrl + '/api/account';
@Injectable() @Injectable()
export class ProfilEditionService { export class ProfilEditionService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
@@ -17,7 +13,7 @@ export class ProfilEditionService {
formData.append('file', file); formData.append('file', file);
return this.http.request(new HttpRequest( return this.http.request(new HttpRequest(
'POST', IMAGES_URL + '/uploadAvatar', formData, { 'POST', '/api/images/uploadAvatar', formData, {
reportProgress: true, reportProgress: true,
responseType: 'text' responseType: 'text'
} }
@@ -25,6 +21,6 @@ export class ProfilEditionService {
} }
updateUser(user: User): Observable<any> { updateUser(user: User): Observable<any> {
return this.http.put<any>(`${ACCOUNT_URL}/`, user); return this.http.put<any>(`/api/account/`, user);
} }
} }

View File

@@ -2,4 +2,4 @@
<main class="container"> <main class="container">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<app-footer></app-footer> <app-footer></app-footer>

View File

@@ -1,3 +0,0 @@
main {
flex:1 0 auto;
}

View File

@@ -13,15 +13,15 @@ describe('AppComponent', () => {
const app = fixture.debugElement.componentInstance; const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy(); expect(app).toBeTruthy();
})); }));
it(`should have as title 'app'`, async(() => { it(`should have as title 'ts-v7'`, async(() => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance; const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app'); expect(app.title).toEqual('ts-v7');
})); }));
it('should render title in a h1 tag', async(() => { it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement; const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); expect(compiled.querySelector('h1').textContent).toContain('Welcome to ts-v7!');
})); }));
}); });

View File

@@ -1,10 +1,10 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent {
title = 'app'; title = 'codiki';
} }

View File

@@ -1,15 +1,21 @@
// Angular core
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router'; import { HttpModule } from '@angular/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
// Dependencies // Dependencies
import { MDBBootstrapModule } from 'angular-bootstrap-md'; import { MDBBootstrapModule } from 'angular-bootstrap-md';
// Router // Router
import { appRoutes } from './app.routes'; import { appRoutes } from './app.routing';
// Guard
import { AuthGuard } from './core/guards/auth.guard';
// Interceptor
import { UnauthorizedInterceptor } from './core/interceptors/unauthorized.interceptor';
// Components // Components
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@@ -19,7 +25,6 @@ import { NotFoundComponent } from './not-found/not-found.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { DisconnectionComponent } from './disconnection/disconnection.component'; import { DisconnectionComponent } from './disconnection/disconnection.component';
import { PostCardComponent } from './core/post-card/post-card.component';
import { PostComponent } from './posts/post.component'; import { PostComponent } from './posts/post.component';
import { ByCategoryComponent } from './posts/byCategory/by-category.component'; import { ByCategoryComponent } from './posts/byCategory/by-category.component';
import { MyPostsComponent } from './posts/myPosts/my-posts.component'; import { MyPostsComponent } from './posts/myPosts/my-posts.component';
@@ -32,10 +37,11 @@ import { SearchComponent } from './search/search.component';
import { SigninComponent } from './signin/signin.component'; import { SigninComponent } from './signin/signin.component';
import { VersionRevisionComponent } from './version-revisions/version-revisions.component'; import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
// html components // Reusable components
import { ProgressBarComponent } from './core/directives/progress-bar/progress-bar.component'; import { PostCardComponent } from './core/post-card/post-card.component';
import { SpinnerComponent } from './core/directives/spinner/spinner.component'; import { SpinnerComponent } from './core/directives/spinner/spinner.component';
import { SearchBarComponent } from './core/directives/search-bar/search-bar.component'; import { SearchBarComponent } from './core/directives/search-bar/search-bar.component';
import { ProgressBarComponent } from './core/directives/progress-bar/progress-bar.component';
// Services // Services
import { AuthService } from './core/services/auth.service'; import { AuthService } from './core/services/auth.service';
@@ -49,73 +55,63 @@ import { ProfilEditionService } from './account-settings/profil-edition/profil-e
import { HeaderService } from './header/header.service'; import { HeaderService } from './header/header.service';
import { CreateUpdatePostService } from './posts/create-update/create-update-post.service'; import { CreateUpdatePostService } from './posts/create-update/create-update-post.service';
import { SearchService } from './search/search.service'; import { SearchService } from './search/search.service';
import { VersionRevisionService } from './version-revisions/version-revisions.service';
// Guards
import { AuthGuard } from './core/guards/auth.guard';
// Interceptors
import { TokenInterceptor } from './core/interceptors/token-interceptor';
import { SigninService } from './signin/signin.service'; import { SigninService } from './signin/signin.service';
import { VersionRevisionService } from './version-revisions/version-revisions.service';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
HeaderComponent, HeaderComponent,
FooterComponent, FooterComponent,
NotFoundComponent,
HomeComponent,
LoginComponent, LoginComponent,
SigninComponent,
DisconnectionComponent, DisconnectionComponent,
HomeComponent,
PostCardComponent, PostCardComponent,
PostComponent, SpinnerComponent,
ByCategoryComponent,
MyPostsComponent, MyPostsComponent,
AccountSettingsComponent, AccountSettingsComponent,
CreateUpdatePostComponent,
ChangePasswordComponent, ChangePasswordComponent,
ProfilEditionComponent, ProfilEditionComponent,
ForbiddenComponent, PostComponent,
NotFoundComponent,
ByCategoryComponent,
CreateUpdatePostComponent,
VersionRevisionComponent,
SearchComponent, SearchComponent,
SigninComponent,
ProgressBarComponent,
SpinnerComponent,
SearchBarComponent, SearchBarComponent,
VersionRevisionComponent ProgressBarComponent,
ForbiddenComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
HttpClientModule,
FormsModule, FormsModule,
HttpModule,
HttpClientModule,
MDBBootstrapModule.forRoot(), MDBBootstrapModule.forRoot(),
RouterModule.forRoot( RouterModule.forRoot(
appRoutes, appRoutes,
// { enableTracing: true } // Enabling tracing // { enableTracing: true }, // Enabling tracing
{ onSameUrlNavigation: 'reload' } { onSameUrlNavigation: 'reload'}
) )
], ],
providers: [ providers: [
AuthGuard,
HeaderService,
AuthService, AuthService,
HomeService,
LoginService, LoginService,
SigninService, SigninService,
HomeService,
PostService,
ByCategoryService,
MyPostsService, MyPostsService,
ChangePasswordService, ChangePasswordService,
ProfilEditionService, ProfilEditionService,
HeaderService, PostService,
ByCategoryService,
CreateUpdatePostService, CreateUpdatePostService,
SearchService,
VersionRevisionService, VersionRevisionService,
AuthGuard, SearchService,
{ { provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true }
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent]
schemas: [ NO_ERRORS_SCHEMA ]
}) })
export class AppModule { } export class AppModule { }

View File

@@ -1,22 +1,20 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component'; import { AuthGuard } from './core/guards/auth.guard';
import { NotFoundComponent } from './not-found/not-found.component';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { SigninComponent } from './signin/signin.component'; import { SigninComponent } from './signin/signin.component';
import { HomeComponent } from './home/home.component';
import { DisconnectionComponent } from './disconnection/disconnection.component'; import { DisconnectionComponent } from './disconnection/disconnection.component';
import { PostComponent } from './posts/post.component';
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
import { MyPostsComponent } from './posts/myPosts/my-posts.component'; import { MyPostsComponent } from './posts/myPosts/my-posts.component';
import { AccountSettingsComponent } from './account-settings/account-settings.component'; import { AccountSettingsComponent } from './account-settings/account-settings.component';
import { ChangePasswordComponent } from './account-settings/change-password/change-password.component'; import { ChangePasswordComponent } from './account-settings/change-password/change-password.component';
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { ProfilEditionComponent } from './account-settings/profil-edition/profil-edition.component'; import { ProfilEditionComponent } from './account-settings/profil-edition/profil-edition.component';
import { SearchComponent } from './search/search.component'; import { PostComponent } from './posts/post.component';
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
import { VersionRevisionComponent } from './version-revisions/version-revisions.component'; import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
import { SearchComponent } from './search/search.component';
import { AuthGuard } from './core/guards/auth.guard';
export const appRoutes: Routes = [ export const appRoutes: Routes = [
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
@@ -27,13 +25,12 @@ export const appRoutes: Routes = [
{ path: 'posts/update/:postKey', component: CreateUpdatePostComponent, canActivate: [AuthGuard] }, { path: 'posts/update/:postKey', component: CreateUpdatePostComponent, canActivate: [AuthGuard] },
{ path: 'posts/:postKey', component: PostComponent }, { path: 'posts/:postKey', component: PostComponent },
{ path: 'posts/byCategory/:categoryId', component: ByCategoryComponent}, { path: 'posts/byCategory/:categoryId', component: ByCategoryComponent},
{ path: 'posts/search/:searchCriteria', component: SearchComponent}, { path: 'posts/search/:searchCriteria', component: SearchComponent },
{ path: 'myPosts', component: MyPostsComponent, canActivate: [AuthGuard]}, { path: 'myPosts', component: MyPostsComponent, canActivate: [AuthGuard] },
{ path: 'accountSettings', component: AccountSettingsComponent, canActivate: [AuthGuard] }, { path: 'accountSettings', component: AccountSettingsComponent, canActivate: [AuthGuard] },
{ path: 'changePassword', component: ChangePasswordComponent, canActivate: [AuthGuard] }, { path: 'changePassword', component: ChangePasswordComponent, canActivate: [AuthGuard] },
{ path: 'profilEdit', component: ProfilEditionComponent, canActivate: [AuthGuard] }, { path: 'profilEdit', component: ProfilEditionComponent, canActivate: [AuthGuard] },
{ path: 'versionrevisions', component: VersionRevisionComponent }, { path: 'versionrevisions', component: VersionRevisionComponent },
{ path: 'forbidden', component: ForbiddenComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', component: NotFoundComponent } { path: '**', redirectTo: '/home' }
]; ];

View File

@@ -27,8 +27,10 @@ import { Router } from '@angular/router';
color: white; color: white;
background-color: #5c6bc0; background-color: #5c6bc0;
border-radius: 2px; border-radius: 2px;
border-style: unset;
padding-left: 10px; padding-left: 10px;
padding-right: 35px; padding-right: 35px;
transition: all 0.2s ease-in;
} }
input#search:focus { input#search:focus {

View File

@@ -72,4 +72,4 @@ export class VersionRevision {
public version: Version, public version: Version,
public bugfix: boolean public bugfix: boolean
) { } ) { }
} }

0
src/main/ts/src/app/core/guards/auth.guard.ts Executable file → Normal file
View File

View File

@@ -1,43 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import { User } from '../entities';
import { AuthService } from '../services/auth.service';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(
private authService: AuthService,
private router: Router
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
let request: HttpRequest<any> = req;
if (token) {
request = req.clone({
setHeaders: {
token: token
}
});
}
return next.handle(request).do((event: HttpEvent<any>) => {
// Do nothing for the interceptor
}, (err: any) => {
if (err instanceof HttpErrorResponse && err.status === 401) {
this.authService.disconnect();
this.router.navigate(['/login']);
}
});
}
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';
@Injectable()
export class UnauthorizedInterceptor implements HttpInterceptor {
constructor(
private router: Router,
private authService: AuthService
) {}
intercept(pRequest: HttpRequest<any>, pNext: HttpHandler): Observable<HttpEvent<any>> {
return pNext.handle(pRequest).pipe(catchError(err => {
if (err.status === 401) {
this.authService.setAnonymous();
this.router.navigate(['/login']);
}
const error = err.error.message || err.statusText;
return throwError(error);
}));
}
}

View File

@@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Post } from '../entities'; import { Post } from '../entities';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'app-post-card', selector: 'app-post-card',
@@ -54,7 +53,7 @@ export class PostCardComponent {
getAvatarUrl(): string { getAvatarUrl(): string {
return this.post.author.image return this.post.author.image
? `${environment.apiUrl}/api/images/loadAvatar/${this.post.author.image}` ? `/api/images/loadAvatar/${this.post.author.image}`
: './assets/images/default_user.png'; : './assets/images/default_user.png';
} }
} }

View File

@@ -1,35 +1,28 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { User } from '../entities'; import { User } from '../entities';
const PARAM_TOKEN = 'token';
const PARAM_USER = 'user'; const PARAM_USER = 'user';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor() {} constructor() {}
public getToken(): string { public setAuthenticated(user: User): void {
return localStorage.getItem(PARAM_TOKEN); this.setUser(user);
} }
public setAnonymous(): void {
public setToken(token: string): void { localStorage.clear();
localStorage.setItem(PARAM_TOKEN, token);
} }
public isAuthenticated(): boolean { public isAuthenticated(): boolean {
return this.getToken() != null; return this.getUser() != null;
}
public disconnect(): void {
localStorage.clear();
} }
public isAdmin(): boolean { public isAdmin(): boolean {
return false; return false;
} }
public setUser(user: User): void { private setUser(user: User): void {
localStorage.setItem(PARAM_USER, JSON.stringify(user)); localStorage.setItem(PARAM_USER, JSON.stringify(user));
} }

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Http } from '@angular/http';
import { AuthService } from '../core/services/auth.service'; import { AuthService } from '../core/services/auth.service';
@@ -11,11 +12,18 @@ export class DisconnectionComponent implements OnInit {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private router: Router private router: Router,
private http: Http
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.authService.disconnect(); this.http.get('/api/account/logout').subscribe(() => {
this.router.navigate(['/home']); console.log('Logout success.');
this.authService.setAnonymous();
}, error => {
console.error('Error during logout from API.', error);
}, () => {
this.router.navigate(['/home']);
});
} }
} }

2
src/main/ts/src/app/footer/footer.component.html Executable file → Normal file
View File

@@ -3,7 +3,7 @@
<div class="footer-copyright"> <div class="footer-copyright">
<div class="container"> <div class="container">
<span class="float-left"> <span class="float-left">
<span class="anticopy">&copy;</span> 2017 Tous droits r&eacute;serv&eacute;s - <span class="anticopy">&copy;</span> 2017 - 2019 Tous droits r&eacute;serv&eacute;s -
<i id="appVersion" routerLink="/versionrevisions" mdbTooltip="Notes de versions" placement="top" mdbRippleRadius> <i id="appVersion" routerLink="/versionrevisions" mdbTooltip="Notes de versions" placement="top" mdbRippleRadius>
{{appVersion}} {{appVersion}}
</i> </i>

8
src/main/ts/src/app/footer/footer.component.scss Executable file → Normal file
View File

@@ -5,6 +5,10 @@ footer {
left: 0; left: 0;
} }
.footer-copyright {
padding: 10px 0;
}
.page-footer { .page-footer {
padding-top: 0px; padding-top: 0px;
} }
@@ -16,4 +20,6 @@ span.anticopy {
#appVersion { #appVersion {
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
} display: inline;
cursor: pointer;
}

0
src/main/ts/src/app/footer/footer.component.ts Executable file → Normal file
View File

0
src/main/ts/src/app/forbidden/forbidden.component.ts Executable file → Normal file
View File

View File

@@ -1,4 +1,4 @@
<mdb-navbar SideClass="navbar fixed-top navbar-expand-lg navbar-dark bg-indigo scrolling-navbar ie-nav" [containerInside]="false"> <mdb-navbar SideClass="navbar fixed-top navbar-expand-lg navbar-dark indigo scrolling-navbar ie-nav" [containerInside]="false">
<logo> <logo>
<a class="logo navbar-brand" routerLink="/"> <a class="logo navbar-brand" routerLink="/">
<img id="logo" src="./assets/images/codiki.png" alt="logo" /> <img id="logo" src="./assets/images/codiki.png" alt="logo" />
@@ -15,7 +15,7 @@
<a routerLink="/login" class="nav-link waves-light" mdbRippleRadius> <a routerLink="/login" class="nav-link waves-light" mdbRippleRadius>
<i class="fa fa-sign-in"></i> Connexion <i class="fa fa-sign-in"></i> Connexion
</a> </a>
</li> </li>
<li class="nav-item dropdown" dropdown *ngIf="isAuthenticated()"> <li class="nav-item dropdown" dropdown *ngIf="isAuthenticated()">
<a dropdownToggle mdbRippleRadius type="button" class="nav-link dropdown-toggle waves-light" mdbRippleRadius> <a dropdownToggle mdbRippleRadius type="button" class="nav-link dropdown-toggle waves-light" mdbRippleRadius>
<i class="fa fa-user-circle"></i> Mon Compte <i class="fa fa-user-circle"></i> Mon Compte
@@ -25,11 +25,11 @@
<i class="fa fa-list-alt"></i> Mes articles <i class="fa fa-list-alt"></i> Mes articles
</a> </a>
<a class="dropdown-item waves-light" mdbRippleRadius routerLink="/accountSettings"> <a class="dropdown-item waves-light" mdbRippleRadius routerLink="/accountSettings">
<i class="fa fa-gear"></i> Paramètres <i class="fa fa-cog"></i> Paramètres
</a> </a>
<a class="dropdown-item waves-light danger-color-dark" mdbRippleRadius routerLink="/disconnection"> <a class="dropdown-item waves-light danger-color-dark" mdbRippleRadius routerLink="/disconnection">
<span class="white-text"> <span class="white-text">
<i class="fa fa-sign-out"></i> Déconnexion <i class="fa fa-sign-out-alt"></i> Déconnexion
</span> </span>
</a> </a>
</div> </div>
@@ -48,7 +48,7 @@
</a> </a>
<div *ngFor="let category of listCategories"> <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.length" (click)="openCategoriesLinks(category)">
{{category.name}} <i class="fa fa-chevron-down pull-right"></i> {{category.name}} <i class="fa {{accordionOpenned[category.id] ? 'fa-chevron-down' : 'fa-chevron-right'}} float-right"></i>
</a> </a>
<div class="categoriesLinks" > <div class="categoriesLinks" >
<a *ngFor="let subCategory of category.listSubCategories" <a *ngFor="let subCategory of category.listSubCategories"

View File

@@ -5,8 +5,8 @@
#logo { #logo {
width: 30px; width: 30px;
height: 30px; height: 30px;
margin-top: -7px; margin-top: -7px;
vertical-align: middle; vertical-align: middle;
margin-right: 10px; margin-right: 10px;
} }
@@ -17,25 +17,25 @@
/* The side navigation menu */ /* The side navigation menu */
.sidenav { .sidenav {
height: 100%; /* 100% Full-height */ height: 100%; /* 100% Full-height */
width: 0; /* 0 width - change this with JavaScript */ width: 0; /* 0 width - change this with JavaScript */
position: fixed; /* Stay in place */ position: fixed; /* Stay in place */
z-index: 1000; /* Stay on top */ z-index: 1050; /* Stay on top */
top: 0; /* Stay at the top */ top: 0; /* Stay at the top */
left: 0; left: 0;
background-color: #3f51b5; background-color: #3f51b5;
overflow-x: hidden; /* Disable horizontal scroll */ overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 20px; /* Place content 60px from the top */ padding-top: 20px; /* Place content 60px from the top */
transition: 0.3s; /* 0.5 second transition effect to slide in the sidenav */ transition: 0.3s; /* 0.5 second transition effect to slide in the sidenav */
} }
/* The navigation menu links */ /* The navigation menu links */
.sidenav a { .sidenav a {
padding: 8px 32px 8px 32px; padding: 8px 32px 8px 32px;
text-decoration: none; text-decoration: none;
color: white; color: white;
display: block; display: block;
transition: 0.3s; transition: 0.3s;
} }
/* When you mouse over the navigation links, change their color */ /* When you mouse over the navigation links, change their color */
@@ -45,43 +45,51 @@
} }
.sidenav h3 { .sidenav h3 {
padding: 8px 8px 8px 32px; padding: 8px 8px 8px 32px;
color: white; color: white;
padding-bottom: 25px; padding-bottom: 25px;
border-bottom: 1px solid #5c6bc0; border-bottom: 1px solid #5c6bc0;
} }
/* Position and style the close button (top right corner) */ /* Position and style the close button (top right corner) */
.sidenav .closebtn { .sidenav .closebtn {
position: absolute; position: absolute;
top: 25px; top: 25px;
right: 25px; right: 25px;
margin-left: 50px; margin-left: 50px;
padding: 8px; padding: 8px;
} }
/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */ /* 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) { @media screen and (max-height: 450px) {
.sidenav {padding-top: 15px;} .sidenav {padding-top: 15px;}
.sidenav a {font-size: 18px;} .sidenav a {font-size: 18px;}
} }
#overlay { #overlay {
position: fixed; /* Sit on top of the page content */ position: fixed; /* Sit on top of the page content */
display: none; /* Hidden by default */ display: none; /* Hidden by default */
width: 100%; /* Full width (cover the whole page) */ width: 100%; /* Full width (cover the whole page) */
height: 100%; /* Full height (cover the whole page) */ height: 100%; /* Full height (cover the whole page) */
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0,0,0,0.5); /* Black background with opacity */ 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 */ z-index: 999; /* Specify a stack order in case you're using a different order for other elements */
transition: 0.5s; transition: 0.5s;
} }
.categoriesLinks { .categoriesLinks {
background-color: #303f9f; background-color: #303f9f;
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: max-height 0.2s ease-out; transition: max-height 0.2s ease-out;
}
.dropdown-item.danger-color-dark:hover {
background-color: #ff4444 !important;
}
.collapsible i.fa {
padding-top: 4px;
} }

View File

@@ -16,6 +16,7 @@ export class HeaderComponent implements OnInit {
isAdmin: boolean; isAdmin: boolean;
listCategories: Array<Category> = []; listCategories: Array<Category> = [];
title: string; title: string;
accordionOpenned: Array<Boolean> = [];
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@@ -73,8 +74,10 @@ export class HeaderComponent implements OnInit {
const divContent = divCategoriesLinks.nextElementSibling as HTMLElement; const divContent = divCategoriesLinks.nextElementSibling as HTMLElement;
if (divContent.style.maxHeight) { if (divContent.style.maxHeight) {
divContent.style.maxHeight = null; divContent.style.maxHeight = null;
this.accordionOpenned[category.id] = false;
} else { } else {
divContent.style.maxHeight = divContent.scrollHeight + 'px'; divContent.style.maxHeight = divContent.scrollHeight + 'px';
this.accordionOpenned[category.id] = true;
} }
} }
} }

View File

@@ -1,16 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { Category } from '../core/entities'; import { Category } from '../core/entities';
import { environment } from '../../environments/environment';
const CATEGORY_URL = environment.apiUrl + '/api/categories/';
@Injectable() @Injectable()
export class HeaderService { export class HeaderService {
constructor(private http: HttpClient) {} constructor(
private http: HttpClient
) {}
getAllCategories(): Observable<Array<Category>> { getAllCategories(): Observable<Array<Category>> {
return this.http.get<Array<Category>>(CATEGORY_URL); return this.http.get<Array<Category>>('/api/categories/');
} }
} }

View File

@@ -1,6 +1,6 @@
<div> <div>
<h1>Derniers articles</h1> <h1>Derniers articles</h1>
<app-spinner *ngIf="!listArticle"></app-spinner> <app-spinner *ngIf="!listArticle"></app-spinner>
<div *ngIf="listArticle" class="col-lg-8 offset-lg-2"> <div *ngIf="listArticle" class="col-lg-8 offset-lg-2">
<app-post-card *ngFor="let post of listArticle" [post]="post"></app-post-card> <app-post-card *ngFor="let post of listArticle" [post]="post"></app-post-card>
</div> </div>

View File

@@ -1,18 +1,14 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { Post } from '../core/entities'; import { Post } from '../core/entities';
import { environment } from '../../environments/environment';
const POSTS_URL = environment.apiUrl + '/api/posts';
@Injectable() @Injectable()
export class HomeService { export class HomeService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getLastPosts(): Observable<Array<Post>> { getLastPosts(): Observable<Array<Post>> {
return this.http.get<Array<Post>>(POSTS_URL + '/last'); return this.http.get<Array<Post>>('/api/posts/last');
} }
} }

View File

@@ -4,7 +4,7 @@
<form id="form" (ngSubmit)="onSubmit()" #loginForm="ngForm"> <form id="form" (ngSubmit)="onSubmit()" #loginForm="ngForm">
<div class="md-form"> <div class="md-form">
<i class="fa fa-envelope prefix grey-text"></i> <i class="fa fa-envelope prefix grey-text"></i>
<input mdbActive <input mdbInputDirective
id="email" id="email"
name="email" name="email"
type="email" type="email"
@@ -12,13 +12,13 @@
[(ngModel)]="model.email" [(ngModel)]="model.email"
#email="ngModel" #email="ngModel"
data-error="Veuillez saisir une adresse email valide" data-error="Veuillez saisir une adresse email valide"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="email">Email</label> <label for="email">Email</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<i class="fa fa-lock prefix grey-text"></i> <i class="fa fa-lock prefix grey-text"></i>
<input mdbActive <input mdbInputDirective
id="password" id="password"
name="password" name="password"
type="password" type="password"
@@ -26,9 +26,9 @@
[(ngModel)]="model.password" [(ngModel)]="model.password"
#password="ngModel" #password="ngModel"
data-error="Veuillez saisir votre mot de passe" data-error="Veuillez saisir votre mot de passe"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="password">Password</label> <label for="password">Mot de passe</label>
</div> </div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2"> <div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body"> <div class="card-body">

View File

@@ -1,8 +1,8 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { User } from '../core/entities';
import { AuthService } from '../core/services/auth.service';
import { LoginService } from './login.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { User } from '../core/entities';
import { LoginService } from './login.service';
import { AuthService } from '../core/services/auth.service';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@@ -29,20 +29,24 @@ export class LoginComponent {
loginError: string; loginError: string;
constructor( constructor(
private router: Router,
private loginService: LoginService, private loginService: LoginService,
private authService: AuthService, private authService: AuthService
private router: Router
) {} ) {}
onSubmit(): void { onSubmit(): void {
this.loginError = undefined; this.loginService.login(this.model).subscribe((pUser: User) => {
console.log('Login success.');
this.loginService.login(this.model).subscribe(user => { this.authService.setAuthenticated(pUser);
this.authService.setToken(user.token);
this.authService.setUser(user);
this.router.navigate(['/myPosts']); this.router.navigate(['/myPosts']);
}, error => { }, (error) => {
this.setMessage('Email ou password incorrect.'); if (error.status === 401) {
console.log('Login attempt failed.');
this.setMessage('Adresse email ou mot de passe incorrect.');
} else {
console.error('Error during login attempt.', error);
this.setMessage('Une erreur est survenue lors de la connexion.');
}
}); });
} }

View File

@@ -1,18 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { User } from '../core/entities'; import { User } from '../core/entities';
import { environment } from '../../environments/environment'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
const LOGIN_URL = environment.apiUrl + '/api/account/login';
@Injectable() @Injectable()
export class LoginService { export class LoginService {
constructor(private http: HttpClient) {} constructor(
private http: HttpClient
) {}
login(user: User): Observable<User> { public login(user: User): Observable<User> {
return this.http.post<User>(LOGIN_URL, user); return this.http.post<User>('/api/account/login', user);
} }
} }

0
src/main/ts/src/app/not-found/not-found.component.ts Executable file → Normal file
View File

View File

View File

View File

@@ -1,23 +1,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { Post, Category } from '../../core/entities'; import { Post, Category } from '../../core/entities';
const CATEGORIES_URL = environment.apiUrl + '/api/categories';
const POSTS_URL = environment.apiUrl + '/api/posts';
@Injectable() @Injectable()
export class ByCategoryService { export class ByCategoryService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getCategoryById(categoryId: number): Observable<Category> { getCategoryById(categoryId: number): Observable<Category> {
return this.http.get<Category>(`${CATEGORIES_URL}/${categoryId}`); return this.http.get<Category>(`/api/categories/${categoryId}`);
} }
getPostsByCategory(category: Category): Observable<Array<Post>> { getPostsByCategory(category: Category): Observable<Array<Post>> {
return this.http.get<Array<Post>>(`${POSTS_URL}/byCategory/${category.id}`); return this.http.get<Array<Post>>(`/api/posts/byCategory/${category.id}`);
} }
} }

View File

@@ -18,7 +18,7 @@
<div class="card-body"> <div class="card-body">
<div *ngIf="activatedTab === 'Édition'"> <div *ngIf="activatedTab === 'Édition'">
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="title" id="title"
name="title" name="title"
type="text" type="text"
@@ -26,12 +26,12 @@
[(ngModel)]="model.title" [(ngModel)]="model.title"
#title="ngModel" #title="ngModel"
data-error="Veuillez saisir un titre d'article" data-error="Veuillez saisir un titre d'article"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="title">Titre de l'article</label> <label for="title">Titre de l'article</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="image" id="image"
name="image" name="image"
type="text" type="text"
@@ -39,12 +39,12 @@
[(ngModel)]="model.image" [(ngModel)]="model.image"
#image="ngModel" #image="ngModel"
data-error="Veuillez saisir une adresse URL ou uploader une image" data-error="Veuillez saisir une adresse URL ou uploader une image"
data-success="" [validateSuccess]="false"
required /> required />
<label for="image">Image de l'article</label> <label for="image">Image de l'article</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<input mdbActive <input mdbInputDirective
id="description" id="description"
name="description" name="description"
type="text" type="text"
@@ -52,13 +52,13 @@
[(ngModel)]="model.description" [(ngModel)]="model.description"
#description="ngModel" #description="ngModel"
data-error="Veuillez saisir la description de l'article" data-error="Veuillez saisir la description de l'article"
data-success="" [validateSuccess]="false"
required /> required />
<label for="description">Description de l'article</label> <label for="description">Description de l'article</label>
</div> </div>
<div class="input-group mb-3 wrap"> <div class="input-group mb-3 wrap">
<div class="select"> <div class="select">
<select id="category" class="select-text" [(ngModel)]="model.category" required> <select id="category" class="select-text" [(ngModel)]="model.category" [compareWith]="compareCategories" required>
<option value="" disabled selected></option> <option value="" disabled selected></option>
<option *ngFor="let category of listCategories" [ngValue]="category">{{category.name}}</option> <option *ngFor="let category of listCategories" [ngValue]="category">{{category.name}}</option>
</select> </select>
@@ -73,19 +73,19 @@
(click)="injectHeader('h1')" (click)="injectHeader('h1')"
mdbTooltip="Titre 1" mdbTooltip="Titre 1"
placement="bottom" placement="bottom"
mdbRippleRadius>H1</button> mdbRippleRadius><b>H1</b></button>
<button type="button" <button type="button"
class="btn btn-floating waves-light" class="btn btn-floating waves-light"
(click)="injectHeader('h2')" (click)="injectHeader('h2')"
mdbTooltip="Titre 2" mdbTooltip="Titre 2"
placement="bottom" placement="bottom"
mdbRippleRadius>H2</button> mdbRippleRadius><b>H2</b></button>
<button type="button" <button type="button"
class="btn btn-floating waves-light" class="btn btn-floating waves-light"
(click)="injectHeader('h3')" (click)="injectHeader('h3')"
mdbTooltip="Titre 3" mdbTooltip="Titre 3"
placement="bottom" placement="bottom"
mdbRippleRadius>H3</button> mdbRippleRadius><b>H3</b></button>
<button type="button" <button type="button"
class="btn btn-floating waves-light" class="btn btn-floating waves-light"
(click)="openImagesModal()" (click)="openImagesModal()"
@@ -112,20 +112,20 @@
</button> </button>
</div> </div>
<div class="md-form"> <div class="md-form">
<textarea mdbActive <textarea mdbInputDirective
id="text" id="text"
name="text" name="text"
type="text" type="text"
class="md-textarea" class="md-textarea form-control"
[(ngModel)]="model.text" [(ngModel)]="model.text"
#text="ngModel" #text="ngModel"
data-error="Veuillez saisir le contenu de l'article" data-error="Veuillez saisir le contenu de l'article"
data-sucess="" [validateSuccess]="false"
required> required>
</textarea> </textarea>
<label for="text">Contenu de l'article</label> <label for="text">Contenu de l'article</label>
</div> </div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2"> <!-- <div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body"> <div class="card-body">
<p class="white-text mb-0">{{modelError}}</p> <p class="white-text mb-0">{{modelError}}</p>
</div> </div>
@@ -134,7 +134,7 @@
<div class="card-body"> <div class="card-body">
<p class="white-text mb-0">{{result}}</p> <p class="white-text mb-0">{{result}}</p>
</div> </div>
</div> </div> -->
</div> </div>
<div *ngIf="activatedTab === 'Aperçu'"> <div *ngIf="activatedTab === 'Aperçu'">
<app-spinner *ngIf="!parsedPost"></app-spinner> <app-spinner *ngIf="!parsedPost"></app-spinner>
@@ -148,6 +148,16 @@
</div> </div>
</div> </div>
</div> </div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{modelError}}</p>
</div>
</div>
<div id="resultMsg" class="card green lighten-2 text-center z-depth-2" >
<div class="card-body">
<p class="white-text mb-0">{{result}}</p>
</div>
</div>
<div id="footer"> <div id="footer">
<a routerLink="/myPosts">Annuler</a> <a routerLink="/myPosts">Annuler</a>
<button type="button" <button type="button"
@@ -183,12 +193,14 @@
</div> </div>
</div> </div>
<div class="md-form" style="margin-top: 15px; margin-bottom: 0;"> <div class="md-form" style="margin-top: 15px; margin-bottom: 0;">
<textarea mdbActive <textarea mdbInputDirective
id="codeTmp" id="codeTmp"
name="codeTmp" name="codeTmp"
type="text" type="text"
class="md-textarea" class="md-textarea form-control"
[(ngModel)]="codeTmp" [(ngModel)]="codeTmp"
data-error="Veuillez écrire ou coller votre extrait de code dans cette zone de saisie"
[validateSuccess]="false"
required> required>
</textarea> </textarea>
<label for="codeTmp">Extrait de code</label> <label for="codeTmp">Extrait de code</label>

View File

@@ -18,11 +18,6 @@
border-bottom: 4px solid white; border-bottom: 4px solid white;
} }
textarea {
height: 250px;
overflow-y: scroll;
}
.custom-select { .custom-select {
border: 0px; border: 0px;
border-bottom: 1px #aaa solid; border-bottom: 1px #aaa solid;
@@ -68,6 +63,8 @@ textarea {
#text { #text {
padding: 0; padding: 0;
height: 250px;
overflow-y: scroll;
} }
#resultMsg, #errorMsg { #resultMsg, #errorMsg {
@@ -83,13 +80,13 @@ textarea {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
/* select starting stylings ------------------------------*/ /* select starting stylings ------------------------------*/
.select { .select {
position: relative; position: relative;
width: 100%; width: 100%;
} }
.select-text { .select-text {
position: relative; position: relative;
font-family: inherit; font-family: inherit;
@@ -101,19 +98,19 @@ textarea {
border: none; border: none;
border-bottom: 1px solid #bdbdbd; border-bottom: 1px solid #bdbdbd;
} }
/* Remove focus */ /* Remove focus */
.select-text:focus { .select-text:focus {
outline: none; outline: none;
border-bottom: 1px solid rgba(0,0,0, 0); border-bottom: 1px solid rgba(0,0,0, 0);
} }
/* Use custom arrow */ /* Use custom arrow */
.select .select-text { .select .select-text {
appearance: none; appearance: none;
-webkit-appearance:none -webkit-appearance:none
} }
.select:after { .select:after {
position: absolute; position: absolute;
top: 18px; top: 18px;
@@ -128,8 +125,8 @@ textarea {
border-top: 6px solid #bdbdbd; border-top: 6px solid #bdbdbd;
pointer-events: none; pointer-events: none;
} }
/* LABEL ======================================= */ /* LABEL ======================================= */
.select-label { .select-label {
color: #757575; color: #757575;
@@ -141,7 +138,7 @@ textarea {
top: -5px; top: -5px;
transition: 0.2s ease all; transition: 0.2s ease all;
} }
/* active state */ /* active state */
.select-text:focus ~ .select-label { .select-text:focus ~ .select-label {
color: #2F80ED; color: #2F80ED;
@@ -151,14 +148,14 @@ textarea {
transition: 0.2s ease all; transition: 0.2s ease all;
font-size: 14px; font-size: 14px;
} }
/* BOTTOM BARS ================================= */ /* BOTTOM BARS ================================= */
.select-bar { .select-bar {
position: relative; position: relative;
display: block; display: block;
width: 100%; width: 100%;
} }
.select-bar:before, .select-bar:after { .select-bar:before, .select-bar:after {
content: ''; content: '';
height: 2px; height: 2px;
@@ -168,20 +165,20 @@ textarea {
background: #2F80ED; background: #2F80ED;
transition: 0.2s ease all; transition: 0.2s ease all;
} }
.select-bar:before { .select-bar:before {
left: 50%; left: 50%;
} }
.select-bar:after { .select-bar:after {
right: 50%; right: 50%;
} }
/* active state */ /* active state */
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after { .select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
width: 50%; width: 50%;
} }
/* HIGHLIGHTER ================================== */ /* HIGHLIGHTER ================================== */
.select-highlight { .select-highlight {
position: absolute; position: absolute;
@@ -220,7 +217,7 @@ $btnSize: 45px;
#image-div { #image-div {
height: 60vh; height: 60vh;
overflow-y: scroll; overflow-y: scroll;
} }
.uploaded-image { .uploaded-image {
@@ -234,4 +231,4 @@ $btnSize: 45px;
cursor:pointer; cursor:pointer;
margin-right: 15px; margin-right: 15px;
margin-bottom: 15px; margin-bottom: 15px;
} }

View File

@@ -1,14 +1,13 @@
import { Component, OnInit, SecurityContext, ViewChild } from '@angular/core'; import { Component, OnInit, SecurityContext, ViewChild } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Router, ActivatedRoute, RoutesRecognized } from '@angular/router'; import { Router, ActivatedRoute, RoutesRecognized, NavigationEnd } from '@angular/router';
import { Post, Category, Image } from '../../core/entities'; import { Post, Category, Image } from '../../core/entities';
import { AuthService } from '../../core/services/auth.service'; import { AuthService } from '../../core/services/auth.service';
import { CreateUpdatePostService } from './create-update-post.service'; import { CreateUpdatePostService } from './create-update-post.service';
import 'rxjs/add/operator/filter'; import { filter, pairwise } from 'rxjs/operators';
import 'rxjs/add/operator/pairwise';
import { environment } from '../../../environments/environment';
import { HttpEventType, HttpResponse } from '@angular/common/http'; import { HttpEventType, HttpResponse } from '@angular/common/http';
enum Tabs { enum Tabs {
@@ -78,12 +77,10 @@ export class CreateUpdatePostComponent implements OnInit {
} }
}); });
this.router.events.filter(e => e instanceof RoutesRecognized).pairwise().subscribe((event: any[]) => { // FIXME: The message isn't shown and the method ngOnInit is too much called, also during others components navigation.
if (event[0].urlAfterRedirects === '/posts/new') { this.router.events.pipe(filter(e => e instanceof RoutesRecognized), pairwise()).subscribe((events: any) => {
this.result = 'Article créé.'; if (events[0].urlAfterRedirects === '/posts/new') {
setTimeout(() => { this.setMessage('Article créé.', false);
this.result = undefined;
}, 3500);
} }
}); });
} }
@@ -196,7 +193,7 @@ export class CreateUpdatePostComponent implements OnInit {
} }
getLinkSrc(pLink: string): string { getLinkSrc(pLink: string): string {
return `${environment.apiUrl}/api/images/${pLink}`; return `/api/images/${pLink}`;
} }
openNewImageInput(): void { openNewImageInput(): void {
@@ -229,4 +226,8 @@ export class CreateUpdatePostComponent implements OnInit {
this.injectElement(imgTag, imgTag.length); this.injectElement(imgTag, imgTag.length);
this.imagesModal.hide(); this.imagesModal.hide();
} }
compareCategories(cat1: Category, cat2: Category): boolean {
return cat1 && cat2 ? cat1.id === cat2.id : cat1 === cat2;
}
} }

View File

@@ -1,13 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http'; import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Post, Category, Image } from '../../core/entities'; import { Post, Category, Image } from '../../core/entities';
import { environment } from '../../../environments/environment';
const IMAGES_URL = environment.apiUrl + '/api/images';
const POSTS_URL = environment.apiUrl + '/api/posts';
const CATEGORIES_URL = environment.apiUrl + '/api/categories';
@Injectable() @Injectable()
export class CreateUpdatePostService { export class CreateUpdatePostService {
@@ -15,27 +9,27 @@ export class CreateUpdatePostService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
processPreview(post: Post): Observable<Post> { processPreview(post: Post): Observable<Post> {
return this.http.post<Post>(`${POSTS_URL}/preview`, post); return this.http.post<Post>(`/api/posts/preview`, post);
} }
getCategories(): Observable<Array<Category>> { getCategories(): Observable<Array<Category>> {
return this.http.get<Array<Category>>(`${CATEGORIES_URL}/`); return this.http.get<Array<Category>>(`/api/categories/`);
} }
addPost(post: Post): Observable<Post> { addPost(post: Post): Observable<Post> {
return this.http.post<Post>(`${POSTS_URL}/`, post); return this.http.post<Post>(`/api/posts/`, post);
} }
updatePost(post: Post): Observable<Post> { updatePost(post: Post): Observable<Post> {
return this.http.put<Post>(`${POSTS_URL}/`, post); return this.http.put<Post>(`/api/posts/`, post);
} }
getPost(postKey: string): Observable<Post> { getPost(postKey: string): Observable<Post> {
return this.http.get<Post>(`${POSTS_URL}/${postKey}/source`); return this.http.get<Post>(`/api/posts/${postKey}/source`);
} }
getImages(): Observable<Array<Image>> { getImages(): Observable<Array<Image>> {
return this.http.get<Array<Image>>(`${IMAGES_URL}/myImages`); return this.http.get<Array<Image>>(`/api/images/myImages`);
} }
uploadPicture(file: File): Observable<HttpEvent<{}>> { uploadPicture(file: File): Observable<HttpEvent<{}>> {
@@ -44,7 +38,7 @@ export class CreateUpdatePostService {
formData.append('file', file); formData.append('file', file);
return this.http.request(new HttpRequest( return this.http.request(new HttpRequest(
'POST', IMAGES_URL, formData, { 'POST', '/api/images', formData, {
reportProgress: true, reportProgress: true,
responseType: 'text' responseType: 'text'
} }
@@ -52,6 +46,6 @@ export class CreateUpdatePostService {
} }
getImageDetails(imageLink: string): Observable<Image> { getImageDetails(imageLink: string): Observable<Image> {
return this.http.get<Image>(`${IMAGES_URL}/${imageLink}/details`); return this.http.get<Image>(`/api/images/${imageLink}/details`);
} }
} }

View File

@@ -3,6 +3,9 @@
<app-spinner *ngIf="!listPosts"></app-spinner> <app-spinner *ngIf="!listPosts"></app-spinner>
<div *ngIf="listPosts" class="col-lg-8 offset-lg-2"> <div *ngIf="listPosts" class="col-lg-8 offset-lg-2">
<app-post-card *ngFor="let post of listPosts" [post]="post"></app-post-card> <app-post-card *ngFor="let post of listPosts" [post]="post"></app-post-card>
</div> </div>
<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">+</a>
</div> </div>

View File

@@ -1,18 +1,15 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { Post } from '../../core/entities'; import { Post } from '../../core/entities';
const POSTS_URL = environment.apiUrl + '/api/posts';
@Injectable() @Injectable()
export class MyPostsService { export class MyPostsService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getMyPosts(): Observable<Array<Post>> { getMyPosts(): Observable<Array<Post>> {
return this.http.get<Array<Post>>(`${POSTS_URL}/myPosts`); return this.http.get<Array<Post>>(`/api/posts/myPosts`);
} }
} }

4
src/main/ts/src/app/posts/post.component.html Executable file → Normal file
View File

@@ -5,7 +5,7 @@
<a *ngIf="owned" class="btn-card-floating waves-light white-text" <a *ngIf="owned" class="btn-card-floating waves-light white-text"
routerLink="/posts/update/{{post.key}}"> routerLink="/posts/update/{{post.key}}">
<i class="fa fa-pencil"></i> <i class="fa fa-pen"></i>
</a> </a>
<div class="card-body"> <div class="card-body">
@@ -22,7 +22,7 @@
[mdbTooltip]="post.author.name" [mdbTooltip]="post.author.name"
placement="bottom"/> placement="bottom"/>
Article écrit par {{post.author.name}} Article écrit par {{post.author.name}}
<span class="creation-date-area">({{post.creationDate | date:'yyyy-MM-dd HH:mm:ss'}})</span> <span class="creation-date-area">({{post.creationDate | date:'HH:mm:ss dd/MM/yyyy'}})</span>
<button *ngIf="owned" type="button" class="btn btn-danger waves-light float-right" (click)="alertDelete.show()" mdbRippleRadius> <button *ngIf="owned" type="button" class="btn btn-danger waves-light float-right" (click)="alertDelete.show()" mdbRippleRadius>
<i class="fa fa-trash mr-1"></i> Supprimer <i class="fa fa-trash mr-1"></i> Supprimer
</button> </button>

3
src/main/ts/src/app/posts/post.component.ts Executable file → Normal file
View File

@@ -6,7 +6,6 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Post, User } from '../core/entities'; import { Post, User } from '../core/entities';
import { PostService } from './post.service'; import { PostService } from './post.service';
import { AuthService } from '../core/services/auth.service'; import { AuthService } from '../core/services/auth.service';
import { environment } from '../../environments/environment';
declare let Prism: any; declare let Prism: any;
@@ -87,7 +86,7 @@ export class PostComponent implements OnInit {
getAvatarUrl(): string { getAvatarUrl(): string {
return this.post.author.image return this.post.author.image
? `${environment.apiUrl}/api/images/loadAvatar/${this.post.author.image}` ? `./api/images/loadAvatar/${this.post.author.image}`
: './assets/images/default_user.png'; : './assets/images/default_user.png';
} }

10
src/main/ts/src/app/posts/post.service.ts Executable file → Normal file
View File

@@ -1,22 +1,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Post } from '../core/entities'; import { Post } from '../core/entities';
const POSTS_URL = environment.apiUrl + '/api/posts';
@Injectable() @Injectable()
export class PostService { export class PostService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getPost(postKey: string): Observable<Post> { getPost(postKey: string): Observable<Post> {
return this.http.get<Post>(`${POSTS_URL}/${postKey}`); return this.http.get<Post>(`/api/posts/${postKey}`);
} }
deletePost(postToDelete: Post): Observable<any> { deletePost(postToDelete: Post): Observable<any> {
return this.http.delete(`${POSTS_URL}/${postToDelete.key}`); return this.http.delete(`/api/posts/${postToDelete.key}`);
} }
} }

0
src/main/ts/src/app/search/search.component.ts Executable file → Normal file
View File

8
src/main/ts/src/app/search/search.service.ts Executable file → Normal file
View File

@@ -1,18 +1,14 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Post } from '../core/entities'; import { Post } from '../core/entities';
const POSTS_URL = environment.apiUrl + '/api/posts';
@Injectable() @Injectable()
export class SearchService { export class SearchService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
search(searchCriteria: string): Observable<Array<Post>> { search(searchCriteria: string): Observable<Array<Post>> {
return this.http.get<Array<Post>>(`${POSTS_URL}/search/${searchCriteria}`); return this.http.get<Array<Post>>(`/api/posts/search/${searchCriteria}`);
} }
} }

16
src/main/ts/src/app/signin/signin.component.html Executable file → Normal file
View File

@@ -4,7 +4,7 @@
<form (ngSubmit)="onSubmit()" #signinForm="ngForm"> <form (ngSubmit)="onSubmit()" #signinForm="ngForm">
<div class="md-form"> <div class="md-form">
<i class="fa fa-id-badge prefix grey-text"></i> <i class="fa fa-id-badge prefix grey-text"></i>
<input mdbActive <input mdbInputDirective
id="name" id="name"
name="name" name="name"
type="text" type="text"
@@ -12,13 +12,13 @@
[(ngModel)]="model.name" [(ngModel)]="model.name"
#name="ngModel" #name="ngModel"
data-error="Veuillez saisir un nom d'utilisateur" data-error="Veuillez saisir un nom d'utilisateur"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="name">Nom d'utilisateur</label> <label for="name">Nom d'utilisateur</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<i class="fa fa-envelope prefix grey-text"></i> <i class="fa fa-envelope prefix grey-text"></i>
<input mdbActive <input mdbInputDirective
id="email" id="email"
name="email" name="email"
type="email" type="email"
@@ -26,13 +26,13 @@
[(ngModel)]="model.email" [(ngModel)]="model.email"
#email="ngModel" #email="ngModel"
data-error="Veuillez saisir une adresse email valide" data-error="Veuillez saisir une adresse email valide"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="email">Email</label> <label for="email">Email</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<i class="fa fa-lock prefix grey-text"></i> <i class="fa fa-lock prefix grey-text"></i>
<input mdbActive <input mdbInputDirective
id="password" id="password"
name="password" name="password"
type="password" type="password"
@@ -40,20 +40,20 @@
[(ngModel)]="model.password" [(ngModel)]="model.password"
#password="ngModel" #password="ngModel"
data-error="Veuillez saisir votre mot de passe" data-error="Veuillez saisir votre mot de passe"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="password">Password</label> <label for="password">Password</label>
</div> </div>
<div class="md-form"> <div class="md-form">
<i class="fa fa-lock prefix grey-text"></i> <i class="fa fa-lock prefix grey-text"></i>
<input mdbActive <input mdbInputDirective
id="confirmPassword" id="confirmPassword"
name="confirmPassword" name="confirmPassword"
type="password" type="password"
class="form-control" class="form-control"
[(ngModel)]="confirmPassword" [(ngModel)]="confirmPassword"
data-error="Veuillez confirmer votre mot de passe" data-error="Veuillez confirmer votre mot de passe"
data-sucess="" [validateSuccess]="false"
required /> required />
<label for="confirmPassword">Confirmez votre mot de passe</label> <label for="confirmPassword">Confirmez votre mot de passe</label>
</div> </div>

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