Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
/.mvn/
/.tools/

### STS ###
.apt_generated
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# MyPortfolios GEIADE API

## Run locally

On Windows PowerShell, use the Maven wrapper included in this repository:

```powershell
.\mvnw.cmd spring-boot:run
```

If you prefer a global Maven installation, `mvn spring-boot:run` also works once `mvn` is available on your `PATH`.

Template for a Spring Boot project including Spring REST, HATEOAS, JPA, etc. Additional details: [HELP.md](HELP.md)

[![Open Issues](https://img.shields.io/github/issues-raw/UdL-EPS-SoftArch/MyPortfoliosGEIADE-API?logo=github)](https://github.com/orgs/UdL-EPS-SoftArch/projects/28)
Expand Down
36 changes: 36 additions & 0 deletions mvnw.cmd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,35 @@ protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exce
.requestMatchers(HttpMethod.POST, "/users").anonymous()
.requestMatchers(HttpMethod.GET, "/users/{username}").anonymous()
.requestMatchers(HttpMethod.POST, "/users/*").denyAll()
//Admins
// Admins
.requestMatchers(HttpMethod.GET, "/admins").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/admins").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/admins/{username}").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/admins/*/suspend").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/admins/*").denyAll()
//Creators
// Creators
.requestMatchers(HttpMethod.GET, "/creators").permitAll()
.requestMatchers(HttpMethod.POST, "/creators").permitAll()
.requestMatchers(HttpMethod.GET, "/creators/{username}").permitAll()
.requestMatchers(HttpMethod.PUT, "/creators/{username}").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/creators/*").hasRole("ADMIN")
//Projects

// Projects
.requestMatchers(HttpMethod.GET, "/projects/search/findByVisibility").permitAll()
.requestMatchers(HttpMethod.GET, "/projects/search/findByPortfolioAndVisibility").permitAll()
.requestMatchers(HttpMethod.GET, "/projects/**").authenticated()
.requestMatchers(HttpMethod.POST, "/projects").authenticated()
.requestMatchers(HttpMethod.PUT, "/projects/*").authenticated()
.requestMatchers(HttpMethod.PATCH, "/projects/*").authenticated()
.requestMatchers(HttpMethod.DELETE, "/projects/*").authenticated()
//Portfolios
// Portfolios
.requestMatchers(HttpMethod.GET, "/portfolios/search/findByVisibility").permitAll()
.requestMatchers(HttpMethod.GET, "/portfolios/*/owner").permitAll()
.requestMatchers(HttpMethod.GET, "/portfolios/**").authenticated()
//Profile
// Profile
.requestMatchers(HttpMethod.POST, "/profiles").hasRole("CREATOR")
//Default
// Default

.requestMatchers(HttpMethod.POST, "/*/*").authenticated()
.requestMatchers(HttpMethod.PUT, "/*/*").authenticated()
.requestMatchers(HttpMethod.PATCH, "/*/*").authenticated()
Expand Down
15 changes: 6 additions & 9 deletions src/main/java/cat/udl/eps/softarch/demo/domain/Project.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cat.udl.eps.softarch.demo.domain;

import com.fasterxml.jackson.annotation.JsonIdentityReference;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
Expand Down Expand Up @@ -29,10 +30,10 @@ public class Project extends UriEntity<Long> {

private ZonedDateTime modified;

// Relació amb Portfolio (segons el diagrama, un Project pertany a un Portfolio)
//@ManyToOne
//@JoinColumn(name = "portfolio_id")
//private Portfolio portfolio;
@ManyToOne(optional = false)
@JoinColumn(name = "portfolio_id", nullable = false)
@JsonIdentityReference(alwaysAsId = true)
private Portfolio portfolio;

public Project() {
this.created = ZonedDateTime.now();
Expand All @@ -58,8 +59,4 @@ public void setModified(ZonedDateTime modified) {

public void setDescription(String description) {this.description = description;}

//public void setPortfolio(Portfolio portfolio) {
//this.portfolio = portfolio;
//}

}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,77 @@
package cat.udl.eps.softarch.demo.handler;

import cat.udl.eps.softarch.demo.domain.Portfolio;
import cat.udl.eps.softarch.demo.domain.Project;
import cat.udl.eps.softarch.demo.repository.ProjectRepository;
import org.springframework.data.rest.core.annotation.*;
import cat.udl.eps.softarch.demo.repository.PortfolioRepository;
import org.springframework.data.rest.core.annotation.HandleBeforeCreate;
import org.springframework.data.rest.core.annotation.HandleBeforeDelete;
import org.springframework.data.rest.core.annotation.HandleBeforeSave;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;

import java.time.ZonedDateTime;

@Component
@RepositoryEventHandler
@RepositoryEventHandler(Project.class)
public class ProjectEventHandler {
final ProjectRepository projectRepository;
private final PortfolioRepository portfolioRepository;

public ProjectEventHandler(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
public ProjectEventHandler(PortfolioRepository portfolioRepository) {
this.portfolioRepository = portfolioRepository;
}

@HandleBeforeCreate
public void handleProjectPreCreate(Project project) {
String username = requireAuthenticatedUsername();
Portfolio portfolio = requireOwnedPortfolio(project, username);

ZonedDateTime timeStamp = ZonedDateTime.now();
project.setPortfolio(portfolio);
project.setCreated(timeStamp);
project.setModified(timeStamp);
}

@HandleBeforeSave
public void handleProjectPreSave(Project project) {
ZonedDateTime timeStamp = ZonedDateTime.now();
project.setModified(timeStamp);
String username = requireAuthenticatedUsername();
Portfolio portfolio = requireOwnedPortfolio(project, username);

project.setPortfolio(portfolio);
project.setModified(ZonedDateTime.now());
}

@HandleBeforeDelete
public void handleProjectPreDelete(Project project) {
String username = requireAuthenticatedUsername();
requireOwnedPortfolio(project, username);
}

private String requireAuthenticatedUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth == null || !auth.isAuthenticated() || auth.getName().equals("anonymousUser")) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User must be logged in");
}

return auth.getName();
}

private Portfolio requireOwnedPortfolio(Project project, String username) {
if (project.getPortfolio() == null || project.getPortfolio().getId() == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Project must belong to a portfolio");
}

Portfolio portfolio = portfolioRepository.findById(project.getPortfolio().getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Portfolio does not exist"));

if (portfolio.getOwner() == null || !portfolio.getOwner().getUsername().equals(username)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You cannot manage projects in another user's portfolio");
}

return portfolio;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package cat.udl.eps.softarch.demo.repository;

import cat.udl.eps.softarch.demo.domain.Portfolio;
import cat.udl.eps.softarch.demo.domain.Project;
import cat.udl.eps.softarch.demo.domain.User;
import cat.udl.eps.softarch.demo.domain.Visibility;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
Expand All @@ -13,5 +14,11 @@
public interface ProjectRepository extends CrudRepository<Project, Long>, PagingAndSortingRepository<Project, Long> {

List<Project> findByNameContaining(@Param("name") String name);

}

List<Project> findByPortfolio(@Param("portfolio") Portfolio portfolio);

List<Project> findByPortfolioAndVisibility(@Param("portfolio") Portfolio portfolio,
@Param("visibility") Visibility visibility);

List<Project> findByVisibility(@Param("visibility") Visibility visibility);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package cat.udl.eps.softarch.demo.steps;

import cat.udl.eps.softarch.demo.domain.Project;
import cat.udl.eps.softarch.demo.domain.Portfolio;
import cat.udl.eps.softarch.demo.domain.User;
import cat.udl.eps.softarch.demo.domain.Visibility;
import cat.udl.eps.softarch.demo.repository.PortfolioRepository;
import cat.udl.eps.softarch.demo.repository.ProjectRepository;
import cat.udl.eps.softarch.demo.repository.UserRepository;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
Expand All @@ -24,11 +28,18 @@ public class ManageProjectStepDefs {

private final StepDefs stepDefs;
private final ProjectRepository projectRepository;
private final PortfolioRepository portfolioRepository;
private final UserRepository userRepository;
private Project preparedProject;

public ManageProjectStepDefs(StepDefs stepDefs, ProjectRepository projectRepository) {
public ManageProjectStepDefs(StepDefs stepDefs,
ProjectRepository projectRepository,
PortfolioRepository portfolioRepository,
UserRepository userRepository) {
this.stepDefs = stepDefs;
this.projectRepository = projectRepository;
this.portfolioRepository = portfolioRepository;
this.userRepository = userRepository;
this.stepDefs.mapper.registerModule(new JavaTimeModule());
}

Expand All @@ -40,6 +51,7 @@ public void iPrepareAProject(String name, String description, String visibility)
preparedProject.setName(name);
preparedProject.setDescription(description);
preparedProject.setVisibility(Visibility.valueOf(visibility));
preparedProject.setPortfolio(createPortfolioForCurrentContext("prepared-" + normalizeName(name)));
}

@And("There is a project with name {string} and description {string} and visibility {string}")
Expand All @@ -48,6 +60,7 @@ public void thereIsAProject(String name, String description, String visibility)
p.setName(name);
p.setDescription(description);
p.setVisibility(Visibility.valueOf(visibility));
p.setPortfolio(createPortfolioForCurrentContext("stored-" + normalizeName(name)));
projectRepository.save(p);
}

Expand Down Expand Up @@ -229,5 +242,34 @@ public void theProjectListIsEmpty() throws Exception {
stepDefs.result.andExpect(jsonPath("$._embedded.projects", empty()));
}

private Portfolio createPortfolioForCurrentContext(String baseName) {
String username = AuthenticationStepDefs.currentUsername;
if (username == null || username.isBlank()) {
username = "project-owner";
}
final String ownerUsername = username;

User owner = userRepository.findById(ownerUsername).orElseGet(() -> {
User user = new User();
user.setId(ownerUsername);
user.setEmail(ownerUsername + "@sample.app");
user.setPassword("password");
user.encodePassword();
return userRepository.save(user);
});

Portfolio portfolio = new Portfolio();
portfolio.setName(baseName + "-portfolio");
portfolio.setVisibility(Visibility.PUBLIC);
portfolio.setOwner(owner);
return portfolioRepository.save(portfolio);
}

private String normalizeName(String value) {
if (value == null || value.isBlank()) {
return "project";
}
return value.toLowerCase().replaceAll("[^a-z0-9]+", "-");
}

}
}