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
8 changes: 7 additions & 1 deletion backend/project-manager/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// JSON Web Token (JWT)
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

// CSV & PDF
implementation 'com.opencsv:opencsv:5.9'
implementation 'com.openhtmltopdf:openhtmltopdf-core:1.0.10'
implementation 'com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.10'

// TOOLS
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
Expand All @@ -68,4 +74,4 @@ tasks.named('test') {

jar {
enabled = false
}
}
1 change: 1 addition & 0 deletions backend/project-manager/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.java.installations.auto-download=true
4 changes: 4 additions & 0 deletions backend/project-manager/settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

rootProject.name = 'project-manager'
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneRequest;
import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskRequest;
import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskRequest;
import pl.edu.agh.project_manager.service.command.project.ProjectCreationCommand;

import java.time.LocalDate;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.edu.agh.project_manager.controller.dto.project.projectrisk;
package pl.edu.agh.project_manager.controller.dto.project_risk;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.edu.agh.project_manager.controller.dto.project.projectrisk;
package pl.edu.agh.project_manager.controller.dto.project_risk;

import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskRequest;
import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskResponse;
import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskRequest;
import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse;
import pl.edu.agh.project_manager.service.project.ProjectRiskService;

import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package pl.edu.agh.project_manager.controller.report;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import pl.edu.agh.project_manager.security.UserPrincipal;
import pl.edu.agh.project_manager.service.report.ReportService;

import java.util.UUID;

@RestController
@RequestMapping("/api/reports")
@RequiredArgsConstructor
public class ReportController {

private final ReportService reportService;

@PreAuthorize("hasAnyAuthority('ADMINISTRATOR', 'AUTHORITY') or " +
"(#type.name().startsWith('PROJECT_') and @projectAccess.canAccessProject(#resourceId, principal)) or " +
"(#type.name().startsWith('GROUP_') and @groupAccess.canAccessGroup(#resourceId, principal))")
@GetMapping("/{resourceId}")
public ResponseEntity<byte[]> downloadReport(
@PathVariable UUID resourceId,
@RequestParam ReportType type,
@AuthenticationPrincipal UserPrincipal user
) {
byte[] reportData = switch (type) {
case PROJECT_CARD_PDF -> reportService.generateProjectCardPdf(resourceId, user);
case PROJECT_RISKS_PDF -> reportService.generateProjectRisksPdf(resourceId, user);
case PROJECT_RISKS_CSV -> reportService.generateProjectRisksCsv(resourceId, user);
case GROUP_PROJECTS_CSV -> reportService.generatePortfolioCsv(resourceId, user);
};

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(type.getContentType()));

ContentDisposition disposition = ContentDisposition.attachment()
.filename(type.getFilename())
.build();
headers.setContentDisposition(disposition);

return ResponseEntity.ok()
.headers(headers)
.body(reportData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pl.edu.agh.project_manager.controller.report;

public enum ReportType {
PROJECT_CARD_PDF("karta_projektu.pdf", "application/pdf"),
PROJECT_RISKS_PDF("rejestr_ryzyk.pdf", "application/pdf"),
PROJECT_RISKS_CSV("rejestr_ryzyk.csv", "text/csv"),
GROUP_PROJECTS_CSV("lista_projektow_grupy.csv", "text/csv");

private final String filename;
private final String contentType;

ReportType(String filename, String contentType) {
this.filename = filename;
this.contentType = contentType;
}

public String getFilename() {
return filename;
}

public String getContentType() {
return contentType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public enum ApiErrorCode {
INVALID_CURRENT_PASSWORD("USR_003", HttpStatus.UNAUTHORIZED, "Current password is incorrect"),
PASSWORD_SAME_AS_CURRENT("USR_004", HttpStatus.BAD_REQUEST, "New password must be different from current password"),

REPORT_GENERATION_ERROR("REP_001", HttpStatus.INTERNAL_SERVER_ERROR, "Wystąpił błąd podczas generowania raportu"),

ACCESS_DENIED("GEN_002", HttpStatus.FORBIDDEN, "Access denied"),
VALIDATION_FAILED("GEN_998", HttpStatus.BAD_REQUEST, "Validation failed"),
INTERNAL_SERVER_ERROR("GEN_999", HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected internal server error");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pl.edu.agh.project_manager.security.access;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import pl.edu.agh.project_manager.repository.projectgroup.ProjectGroupRepository;
import pl.edu.agh.project_manager.security.UserPrincipal;

import java.util.UUID;

@Component("groupAccess")
@RequiredArgsConstructor
public class GroupAccess {

private final ProjectGroupRepository projectGroupRepository;
private final ProjectAccess projectAccess;

public boolean canAccessGroup(UUID groupId, UserPrincipal currentUser) {
if (currentUser == null) {
return false;
}

return projectGroupRepository.findById(groupId)
.map(group -> {
if (group.getOwner().getId().equals(currentUser.userId())) {
return true;
}

return group.getProjects().stream()
.anyMatch(project -> projectAccess.canAccessProject(project.getId(), currentUser));
})
.orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskResponse;
import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pl.edu.agh.project_manager.service.report;

import java.util.List;

public interface CsvGenerator<T> {
byte[] generate(List<T> data);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pl.edu.agh.project_manager.service.report;

public interface PdfGenerator<T> {
byte[] generate(T data, String templateName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package pl.edu.agh.project_manager.service.report;

import com.opencsv.CSVWriter;
import org.springframework.stereotype.Component;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Component
public class PortfolioCsvGenerator implements CsvGenerator<ProjectResponse> {

private static final byte[] BOM = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
private static final String[] HEADERS = {"Tytuł Projektu", "Opis", "Data rozpoczęcia", "Data zakończenia", "Status"};

@Override
public byte[] generate(List<ProjectResponse> data) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
baos.write(BOM);

try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
CSVWriter csvWriter = new CSVWriter(writer)) {

csvWriter.writeNext(HEADERS);

for (ProjectResponse project : data) {
String[] row = {
project.title(),
project.description(),
project.startDate().toString(),
project.endDate().toString(),
project.isActive() ? "Aktywny" : "Zakończony"
};
csvWriter.writeNext(row);
}

csvWriter.flush();
}

return baos.toByteArray();
} catch (IOException e) {
throw new ApplicationException(ApiErrorCode.REPORT_GENERATION_ERROR, "Błąd podczas generowania pliku CSV portfela: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pl.edu.agh.project_manager.service.report;

import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;

import java.io.ByteArrayOutputStream;

@Component
@RequiredArgsConstructor
public class ProjectPdfGenerator implements PdfGenerator<ProjectResponse> {

private final TemplateEngine templateEngine;

@Override
public byte[] generate(ProjectResponse data, String templateName) {
Context context = new Context();
context.setVariable("project", data);

String htmlContent = templateEngine.process(templateName, context);

try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFastMode();
builder.withHtmlContent(htmlContent, null);
builder.toStream(os);
builder.run();

return os.toByteArray();
} catch (Exception e) {
throw new ApplicationException(ApiErrorCode.REPORT_GENERATION_ERROR, "Błąd podczas generowania pliku PDF: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pl.edu.agh.project_manager.service.report;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse;
import pl.edu.agh.project_manager.security.UserPrincipal;
import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand;
import pl.edu.agh.project_manager.service.project.ProjectRiskService;
import pl.edu.agh.project_manager.service.project.ProjectService;

import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class ReportService {

private final ProjectService projectService;
private final ProjectRiskService projectRiskService;
private final RiskCsvGenerator riskCsvGenerator;
private final ProjectPdfGenerator projectPdfGenerator;
private final RiskPdfGenerator riskPdfGenerator;
private final PortfolioCsvGenerator portfolioCsvGenerator;

public byte[] generateProjectRisksCsv(UUID projectId, UserPrincipal user) {
List<ProjectRiskResponse> risks = projectRiskService.getProjectRisks(projectId);
return riskCsvGenerator.generate(risks);
}

public byte[] generateProjectCardPdf(UUID projectId, UserPrincipal user) {
ProjectResponse project = projectService.getProject(projectId);
return projectPdfGenerator.generate(project, "project-card-template");
}

public byte[] generateProjectRisksPdf(UUID projectId, UserPrincipal user) {
List<ProjectRiskResponse> risks = projectRiskService.getProjectRisks(projectId);
return riskPdfGenerator.generate(risks, "project-risks-template");
}

public byte[] generatePortfolioCsv(UUID groupId, UserPrincipal user) {
SearchProjectCommand command = new SearchProjectCommand(user, null, groupId, null, false);
List<ProjectResponse> projects = projectService.searchProjects(command);
return portfolioCsvGenerator.generate(projects);
}
}
Loading
Loading