diff --git a/.gitignore b/.gitignore index 5942b03..9d5d62a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,15 @@ build/ ### ENVIRONMENT ### .env +secret.json +.jdk + +### Build ### +target/ + +### Demo ### +certificate.db +*.pdf +uploads/ +*.py +*.csv diff --git a/pom.xml b/pom.xml index e5e78c9..2784524 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,16 @@ postgresql runtime + + org.xerial + sqlite-jdbc + 3.42.0.0 + + + org.hibernate.orm + hibernate-community-dialects + 6.2.2.Final + org.springframework.boot spring-boot-configuration-processor @@ -81,10 +91,6 @@ jaxb-api 2.4.0-b180830.0359 - - org.springframework.boot - spring-boot-starter-security - org.springdoc springdoc-openapi-starter-webmvc-ui @@ -99,6 +105,63 @@ commonmark 0.21.0 + + + + com.itextpdf + kernel + 8.0.5 + + + com.itextpdf + layout + 8.0.5 + + + com.itextpdf + forms + 8.0.5 + + + + + com.google.api-client + google-api-client + 2.2.0 + + + com.google.oauth-client + google-oauth-client-jetty + 1.34.1 + + + com.google.apis + google-api-services-gmail + v1-rev20220404-2.0.0 + + + com.google.http-client + google-http-client-gson + 1.43.3 + + + com.google.auth + google-auth-library-oauth2-http + 1.16.0 + + + + + com.opencsv + opencsv + 5.9 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + @@ -115,6 +178,21 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + 1.18.32 + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/com/pecacm/backend/configuration/SecurityConfiguration.java b/src/main/java/com/pecacm/backend/configuration/SecurityConfiguration.java index b8e6ca4..8a24c67 100644 --- a/src/main/java/com/pecacm/backend/configuration/SecurityConfiguration.java +++ b/src/main/java/com/pecacm/backend/configuration/SecurityConfiguration.java @@ -38,11 +38,10 @@ public SecurityConfiguration(UserService userService, JwtFilter jwtFilter, Simpl @Bean public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder, UserService userService) throws Exception { - // TODO: and() is deprecated, replace in future - return http.getSharedObject(AuthenticationManagerBuilder.class) - .userDetailsService(userService) - .passwordEncoder(passwordEncoder) - .and().build(); + AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class); + builder.userDetailsService(userService) + .passwordEncoder(passwordEncoder); + return builder.build(); } @Bean @@ -52,6 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( a -> a.requestMatchers("/error").anonymous() + .requestMatchers("/api/certificates/**", "/api/templates/**").permitAll() // Adjust security as needed .anyRequest().permitAll() ) .sessionManagement( diff --git a/src/main/java/com/pecacm/backend/controllers/CertificateController.java b/src/main/java/com/pecacm/backend/controllers/CertificateController.java new file mode 100644 index 0000000..18c2d1b --- /dev/null +++ b/src/main/java/com/pecacm/backend/controllers/CertificateController.java @@ -0,0 +1,124 @@ +package com.pecacm.backend.controllers; + +import com.pecacm.backend.entities.Certificate; +import com.pecacm.backend.entities.Event; +import com.pecacm.backend.repository.EventRepository; +import com.pecacm.backend.services.CertificateService; +import com.pecacm.backend.services.MassMailService; +import com.pecacm.backend.services.TemplateGeneratorService; +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvValidationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping("/api/certificates") +@RequiredArgsConstructor +@Slf4j +public class CertificateController { + + private final CertificateService certificateService; + private final MassMailService massMailService; + private final TemplateGeneratorService generatorService; + private final EventRepository eventRepository; + + @GetMapping + public List getAllCertificates(@RequestParam(required = false) Integer eventId) { + if (eventId != null) { + return certificateService.getCertificatesByEventId(eventId); + } + return certificateService.getAllCertificates(); + } + + @GetMapping("/{id}") + public Certificate getCertificate(@PathVariable Long id) { + return certificateService.getCertificateById(id); + } + + @PostMapping + public Certificate createCertificate(@RequestBody Certificate certificate) { + return certificateService.createCertificate(certificate); + } + + @PutMapping("/{id}") + public Certificate updateCertificate(@PathVariable Long id, @RequestBody Certificate certificate) { + return certificateService.updateCertificate(id, certificate); + } + + @DeleteMapping("/{id}") + public void deleteCertificate(@PathVariable Long id) { + certificateService.deleteCertificate(id); + } + + @GetMapping("/{id}/download") + public ResponseEntity downloadCertificate(@PathVariable Long id) { + Certificate certificate = certificateService.getCertificateById(id); + byte[] pdfBytes = generatorService.generateCertificatePdf(certificate); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"certificate.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(pdfBytes); + } + + @PostMapping("/mass-mail/{eventId}") + public ResponseEntity sendMassMail(@PathVariable Integer eventId) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found with id: " + eventId)); + + List certificates = certificateService.getCertificatesByEventId(eventId); + + if (certificates.isEmpty()) { + return ResponseEntity.badRequest().body("No certificates found for this event."); + } + + massMailService.sendCertificates(event, certificates); + return ResponseEntity.ok("Mass mail job started for " + certificates.size() + " recipients."); + } + + @PostMapping("/upload-csv/{eventId}") + public ResponseEntity uploadCsv(@PathVariable Integer eventId, @RequestParam("file") MultipartFile file) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found with id: " + eventId)); + + List certificates = new ArrayList<>(); + try (CSVReader reader = new CSVReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + String[] nextLine; + // Skip header: Name, Email + reader.readNext(); + + while ((nextLine = reader.readNext()) != null) { + if (nextLine.length >= 2) { + String name = nextLine[0].trim(); + String email = nextLine[1].trim(); + if (!certificateService.existsByEmailAndEvent(email, eventId)) { + Certificate cert = Certificate.builder() + .recipientName(name) + .recipientEmail(email) + .event(event) + .issueDate(LocalDate.now().toString()) + .build(); + certificates.add(certificateService.createCertificate(cert)); + } + } + } + } catch (IOException | CsvValidationException e) { + log.error("CSV processing error", e); + return ResponseEntity.internalServerError().body("Error processing CSV: " + e.getMessage()); + } + + return ResponseEntity.ok("Successfully imported " + certificates.size() + " participants."); + } +} diff --git a/src/main/java/com/pecacm/backend/controllers/CertificateUIController.java b/src/main/java/com/pecacm/backend/controllers/CertificateUIController.java new file mode 100644 index 0000000..8a6c000 --- /dev/null +++ b/src/main/java/com/pecacm/backend/controllers/CertificateUIController.java @@ -0,0 +1,200 @@ +package com.pecacm.backend.controllers; + +import com.pecacm.backend.entities.Certificate; +import com.pecacm.backend.entities.Event; +import com.pecacm.backend.entities.Template; +import com.pecacm.backend.entities.Transaction; +import com.pecacm.backend.enums.EventRole; +import com.pecacm.backend.repository.EventRepository; +import com.pecacm.backend.repository.TransactionRepository; +import com.pecacm.backend.services.CertificateService; +import com.pecacm.backend.services.MassMailService; +import com.pecacm.backend.services.TemplateGeneratorService; +import com.opencsv.CSVReader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Controller +@RequestMapping("/ui") +@RequiredArgsConstructor +@Slf4j +public class CertificateUIController { + + private final EventRepository eventRepository; + private final TransactionRepository transactionRepository; + private final CertificateService certificateService; + private final TemplateGeneratorService generatorService; + private final MassMailService massMailService; + + private static final String UPLOAD_DIR = "uploads/"; + + @GetMapping + public String showPortal(Model model) { + model.addAttribute("events", eventRepository.findAll()); + return "index"; + } + + @PostMapping("/upload") + public String handleTemplateUpload(@RequestParam("eventId") Integer eventId, + @RequestParam("file") MultipartFile file, + Model model) { + try { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + + Path uploadPath = Paths.get(UPLOAD_DIR); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + String filename = "template_" + eventId + "_" + System.currentTimeMillis() + ".pdf"; + Path filePath = uploadPath.resolve(filename); + Files.copy(file.getInputStream(), filePath); + + Template template = event.getTemplate(); + if (template == null) { + template = new Template(); + } + template.setTemplateName("Template for " + event.getTitle()); + template.setTemplatePdfPath(filePath.toString()); + + event.setTemplate(template); + eventRepository.save(event); + + return "redirect:/ui?success=true"; + } catch (Exception e) { + log.error("Upload failed", e); + model.addAttribute("error", "Upload failed: " + e.getMessage()); + return "index"; + } + } + + @PostMapping("/generate") + public ResponseEntity handleCertificateDownload(@RequestParam("eventId") Integer eventId, + @RequestParam("recipientName") String recipientName) { + try { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + + Certificate certificate = Certificate.builder() + .recipientName(recipientName) + .recipientEmail("demo@example.com") + .event(event) + .issueDate(LocalDate.now().toString()) + .build(); + + byte[] pdfBytes = generatorService.generateCertificatePdf(certificate); + ByteArrayResource resource = new ByteArrayResource(pdfBytes); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"certificate.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .contentLength(pdfBytes.length) + .body(resource); + } catch (Exception e) { + log.error("Generation failed", e); + return ResponseEntity.internalServerError().build(); + } + } + + @PostMapping("/mass-mail") + public String handleMassMail(@RequestParam("eventId") Integer eventId, + @RequestParam("file") MultipartFile file, + Model model) { + try { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + + List certificates = new ArrayList<>(); + try (CSVReader reader = new CSVReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + reader.readNext(); // Skip header + String[] line; + while ((line = reader.readNext()) != null) { + if (line.length >= 2) { + String name = line[0].trim(); + String email = line[1].trim(); + if (!certificateService.existsByEmailAndEvent(email, eventId)) { + Certificate cert = Certificate.builder() + .recipientName(name) + .recipientEmail(email) + .event(event) + .issueDate(LocalDate.now().toString()) + .build(); + certificates.add(certificateService.createCertificate(cert)); + } + } + } + } + + massMailService.sendCertificates(event, certificates); + return "redirect:/ui?count=" + certificates.size(); + } catch (Exception e) { + log.error("Mass mail failed", e); + model.addAttribute("error", "Mass mail failed: " + e.getMessage()); + return "index"; + } + } + + @PostMapping("/send-to-registered") + public String handleSendToRegistered(@RequestParam("eventId") Integer eventId, + Model model) { + try { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + + // Fetch only PARTICIPANTS + List transactions = transactionRepository.findListByEventIdAndRoles( + eventId, List.of(EventRole.PARTICIPANT) + ); + + if (transactions.isEmpty()) { + model.addAttribute("error", "No participants found for this event in the database."); + model.addAttribute("events", eventRepository.findAll()); + return "index"; + } + + List certificates = new ArrayList<>(); + for (Transaction tx : transactions) { + String email = tx.getUser().getEmail(); + String name = tx.getUser().getName(); + + if (!certificateService.existsByEmailAndEvent(email, eventId)) { + Certificate cert = Certificate.builder() + .recipientName(name) + .recipientEmail(email) + .event(event) + .issueDate(LocalDate.now().toString()) + .build(); + + certificates.add(certificateService.createCertificate(cert)); + } + } + + massMailService.sendCertificates(event, certificates); + return "redirect:/ui?count=" + certificates.size(); + + } catch (Exception e) { + log.error("Auto-sync failed", e); + model.addAttribute("error", "Auto-sync failed: " + e.getMessage()); + model.addAttribute("events", eventRepository.findAll()); + return "index"; + } + } +} diff --git a/src/main/java/com/pecacm/backend/controllers/TemplateController.java b/src/main/java/com/pecacm/backend/controllers/TemplateController.java new file mode 100644 index 0000000..7e3209f --- /dev/null +++ b/src/main/java/com/pecacm/backend/controllers/TemplateController.java @@ -0,0 +1,73 @@ +package com.pecacm.backend.controllers; + +import com.pecacm.backend.entities.Event; +import com.pecacm.backend.entities.Template; +import com.pecacm.backend.repository.EventRepository; +import com.pecacm.backend.repository.TemplateRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@RestController +@RequestMapping("/api/templates") +@RequiredArgsConstructor +@Slf4j +public class TemplateController { + + private final TemplateRepository templateRepository; + private final EventRepository eventRepository; + + @Value("${app.upload.dir:${user.home}/acm-uploads}") + private String uploadDir; + + @GetMapping + public List