mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
Images and Image Meta data, with Controllers and repos
This commit is contained in:
56
sql/ImageMetaManagement.sql
Normal file
56
sql/ImageMetaManagement.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
-- Auto-update updated_at on UPDATE (PostgreSQL)
|
||||||
|
|
||||||
|
-- 1) Generic trigger function
|
||||||
|
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 2) Tables
|
||||||
|
CREATE TABLE IF NOT EXISTS image_meta (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
original_name TEXT,
|
||||||
|
content_type TEXT NOT NULL,
|
||||||
|
byte_size BIGINT NOT NULL CHECK (byte_size >= 0),
|
||||||
|
sha256 CHAR(64),
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS image_blob (
|
||||||
|
image_id BIGINT PRIMARY KEY,
|
||||||
|
data BYTEA NOT NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ NULL,
|
||||||
|
|
||||||
|
CONSTRAINT fk_image_blob_meta
|
||||||
|
FOREIGN KEY (image_id)
|
||||||
|
REFERENCES image_meta(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3) Triggers (drop first to make script re-runnable)
|
||||||
|
DROP TRIGGER IF EXISTS trg_image_meta_set_updated_at ON image_meta;
|
||||||
|
CREATE TRIGGER trg_image_meta_set_updated_at
|
||||||
|
BEFORE UPDATE ON image_meta
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_image_blob_set_updated_at ON image_blob;
|
||||||
|
CREATE TRIGGER trg_image_blob_set_updated_at
|
||||||
|
BEFORE UPDATE ON image_blob
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- 4) Helpful indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_image_meta_created_at ON image_meta(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_image_meta_sha256 ON image_meta(sha256);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_image_meta_deleted_at ON image_meta(deleted_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_image_blob_deleted_at ON image_blob(deleted_at);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog package for the BattlBuilder application.
|
||||||
|
* <p>
|
||||||
|
* This package contains classes responsible for platform resolution,
|
||||||
|
* rule compilation, and product context classification.
|
||||||
|
*
|
||||||
|
* @author Forward Group, LLC
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2025-12-10
|
||||||
|
*/
|
||||||
|
package group.goforward.battlbuilder.catalog;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package group.goforward.battlbuilder.controllers.api;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImageMeta;
|
||||||
|
import group.goforward.battlbuilder.security.UserPrincipal;
|
||||||
|
import group.goforward.battlbuilder.services.impl.ImageService;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/images")
|
||||||
|
public class ImagesController {
|
||||||
|
|
||||||
|
private final ImageService imageService;
|
||||||
|
|
||||||
|
public ImagesController(ImageService imageService) {
|
||||||
|
this.imageService = imageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<StreamingResponseBody> getImage(@PathVariable long id,
|
||||||
|
@AuthenticationPrincipal UserPrincipal user,
|
||||||
|
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
|
||||||
|
|
||||||
|
ImageMeta meta = imageService.requireAuthorizedMeta(id, user); // 403/404 (depending on exception mapping)
|
||||||
|
String etag = meta.getEtag();
|
||||||
|
|
||||||
|
if (ifNoneMatch != null && ifNoneMatch.contains(etag)) {
|
||||||
|
return ResponseEntity.status(304)
|
||||||
|
.eTag(etag)
|
||||||
|
.cacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePrivate())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream in = imageService.openImageStream(id);
|
||||||
|
StreamingResponseBody body = out -> {
|
||||||
|
try (in) {
|
||||||
|
in.transferTo(out);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.parseMediaType(meta.getContentType()))
|
||||||
|
.contentLength(meta.getSize())
|
||||||
|
.eTag(etag)
|
||||||
|
.cacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePrivate())
|
||||||
|
.body(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { sendEmailAction, type ApiResponse, type EmailRequest } from "./actions/sendEmail";
|
||||||
|
|
||||||
|
export default function SendEmailForm(): JSX.Element {
|
||||||
|
const [result, setResult] = useState<ApiResponse<EmailRequest> | { error: string } | null>(null);
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||||
|
e.preventDefault();
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
const form = new FormData(e.currentTarget);
|
||||||
|
|
||||||
|
const recipient = String(form.get("recipient") ?? "");
|
||||||
|
const subject = String(form.get("subject") ?? "");
|
||||||
|
const body = String(form.get("body") ?? "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await sendEmailAction({ recipient, subject, body });
|
||||||
|
setResult(data);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Unknown error";
|
||||||
|
setResult({ error: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<input name="recipient" placeholder="recipient" defaultValue="user@example.com" />
|
||||||
|
<input name="subject" placeholder="subject" defaultValue="Test subject" />
|
||||||
|
<textarea name="body" placeholder="body" defaultValue="Test body" />
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
|
||||||
|
<pre style={{ marginTop: 12 }}>
|
||||||
|
{result ? JSON.stringify(result, null, 2) : "No response yet."}
|
||||||
|
</pre>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -50,10 +50,13 @@ public class EmailRequest {
|
|||||||
@PrePersist
|
@PrePersist
|
||||||
protected void onCreate() {
|
protected void onCreate() {
|
||||||
createdAt = LocalDateTime.now();
|
createdAt = LocalDateTime.now();
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
status = "PENDING";
|
status = "PENDING";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Column(name = "updated_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
@@ -119,4 +122,12 @@ public class EmailRequest {
|
|||||||
public void setCreatedAt(LocalDateTime createdAt) {
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package group.goforward.battlbuilder.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "image_blob")
|
||||||
|
public class ImageBlob {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private Long id; // same as ImageMeta.id (1:1)
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Basic(fetch = FetchType.LAZY)
|
||||||
|
@Column(name = "data", nullable = false)
|
||||||
|
private byte[] data;
|
||||||
|
|
||||||
|
protected ImageBlob() {}
|
||||||
|
|
||||||
|
public ImageBlob(Long id, byte[] data) {
|
||||||
|
this.id = id;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package group.goforward.battlbuilder.model;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "image_meta")
|
||||||
|
public class ImageMeta {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 255)
|
||||||
|
private String filename;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String contentType; // e.g., "image/jpeg", "image/png"
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private long size; // file size in bytes
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 64)
|
||||||
|
private String sha256; // hex-encoded SHA-256 hash used for ETag
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Long ownerId; // Who is allowed to access this image
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Instant uploadedAt = Instant.now();
|
||||||
|
|
||||||
|
public ImageMeta() {}
|
||||||
|
|
||||||
|
public ImageMeta(String filename, String contentType, long size, String sha256, Long ownerId) {
|
||||||
|
this.filename = filename;
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.size = size;
|
||||||
|
this.sha256 = sha256;
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
this.uploadedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
|
||||||
|
public String getFilename() { return filename; }
|
||||||
|
public void setFilename(String filename) { this.filename = filename; }
|
||||||
|
|
||||||
|
public String getContentType() { return contentType; }
|
||||||
|
public void setContentType(String contentType) { this.contentType = contentType; }
|
||||||
|
|
||||||
|
public long getSize() { return size; }
|
||||||
|
public void setSize(long size) { this.size = size; }
|
||||||
|
|
||||||
|
public String getSha256() { return sha256; }
|
||||||
|
public void setSha256(String sha256) { this.sha256 = sha256; }
|
||||||
|
|
||||||
|
public Long getOwnerId() { return ownerId; }
|
||||||
|
public void setOwnerId(Long ownerId) { this.ownerId = ownerId; }
|
||||||
|
|
||||||
|
public Instant getUploadedAt() { return uploadedAt; }
|
||||||
|
public void setUploadedAt(Instant uploadedAt) { this.uploadedAt = uploadedAt; }
|
||||||
|
|
||||||
|
/** Returns a correct HTTP ETag string */
|
||||||
|
public String getEtag() {
|
||||||
|
return "\"" + sha256 + "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package group.goforward.battlbuilder.repos;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImageBlob;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ImageBlobRepository extends JpaRepository<ImageBlob, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package group.goforward.battlbuilder.repos;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImageMeta;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ImageMetaRepository extends JpaRepository<ImageMeta, Long> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package group.goforward.battlbuilder.security;
|
||||||
|
|
||||||
|
|
||||||
|
public class UserPrincipal {
|
||||||
|
private final Long id;
|
||||||
|
private final boolean admin;
|
||||||
|
|
||||||
|
public UserPrincipal(Long id, boolean admin) {
|
||||||
|
this.id = id;
|
||||||
|
this.admin = admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAdmin() {
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package group.goforward.battlbuilder.services.impl;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImageBlob;
|
||||||
|
import group.goforward.battlbuilder.model.ImageMeta;
|
||||||
|
import group.goforward.battlbuilder.repos.ImageBlobRepository;
|
||||||
|
import group.goforward.battlbuilder.repos.ImageMetaRepository;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.security.UserPrincipal;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ImageService {
|
||||||
|
|
||||||
|
private final ImageMetaRepository metaRepository;
|
||||||
|
private final ImageBlobRepository blobRepository;
|
||||||
|
|
||||||
|
public ImageService(ImageMetaRepository metaRepository,
|
||||||
|
ImageBlobRepository blobRepository) {
|
||||||
|
this.metaRepository = metaRepository;
|
||||||
|
this.blobRepository = blobRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load metadata for an image and ensure the current user is allowed to see it.
|
||||||
|
*
|
||||||
|
* @throws jakarta.persistence.EntityNotFoundException if not found
|
||||||
|
* @throws org.springframework.security.access.AccessDeniedException if not authorized
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ImageMeta requireAuthorizedMeta(long id, UserPrincipal user) {
|
||||||
|
ImageMeta meta = metaRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new jakarta.persistence.EntityNotFoundException("Image not found: " + id));
|
||||||
|
|
||||||
|
if (!isAuthorized(meta, user)) {
|
||||||
|
throw new org.springframework.security.access.AccessDeniedException("Not allowed to access this image");
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAuthorized(ImageMeta meta, UserPrincipal user) {
|
||||||
|
if (user == null) return false;
|
||||||
|
if (user.isAdmin()) return true;
|
||||||
|
return meta.getOwnerId() != null && meta.getOwnerId().equals(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an InputStream for the image data.
|
||||||
|
* (Currently loads the blob into memory; acceptable for moderate sizes.)
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public InputStream openImageStream(long id) {
|
||||||
|
ImageBlob blob = blobRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new jakarta.persistence.EntityNotFoundException("Image data not found: " + id));
|
||||||
|
|
||||||
|
return new ByteArrayInputStream(blob.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new image for the given owner.
|
||||||
|
* Returns the persisted ImageMeta.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ImageMeta storeImage(MultipartFile file, Long ownerId) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = file.getBytes();
|
||||||
|
String sha256 = sha256Hex(bytes);
|
||||||
|
|
||||||
|
ImageMeta meta = new ImageMeta(
|
||||||
|
file.getOriginalFilename(),
|
||||||
|
file.getContentType() != null ? file.getContentType() : "application/octet-stream",
|
||||||
|
bytes.length,
|
||||||
|
sha256,
|
||||||
|
ownerId
|
||||||
|
);
|
||||||
|
meta.setUploadedAt(Instant.now());
|
||||||
|
|
||||||
|
// Save meta first so it gets an ID
|
||||||
|
meta = metaRepository.save(meta);
|
||||||
|
|
||||||
|
// Save blob using same ID
|
||||||
|
ImageBlob blob = new ImageBlob(meta.getId(), bytes);
|
||||||
|
blobRepository.save(blob);
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to store image", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete image (meta + blob) – useful for cleanup.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteImage(long id, UserPrincipal user) {
|
||||||
|
ImageMeta meta = requireAuthorizedMeta(id, user);
|
||||||
|
blobRepository.deleteById(meta.getId());
|
||||||
|
metaRepository.deleteById(meta.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
private String sha256Hex(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = digest.digest(data);
|
||||||
|
return HexFormat.of().formatHex(hash);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Could not compute SHA-256", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,11 +5,13 @@ import group.goforward.battlbuilder.repos.EmailRequestRepository;
|
|||||||
import group.goforward.battlbuilder.services.utils.EmailService;
|
import group.goforward.battlbuilder.services.utils.EmailService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -36,12 +38,20 @@ public class EmailServiceImpl implements EmailService {
|
|||||||
emailRequest = emailRequestRepository.save(emailRequest);
|
emailRequest = emailRequestRepository.save(emailRequest);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send email
|
// Send email as HTML
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
message.setFrom(fromEmail);
|
MimeMessageHelper helper = new MimeMessageHelper(
|
||||||
message.setTo(recipient);
|
message,
|
||||||
message.setSubject(subject);
|
false,
|
||||||
message.setText(body);
|
StandardCharsets.UTF_8.name()
|
||||||
|
);
|
||||||
|
|
||||||
|
helper.setFrom(fromEmail);
|
||||||
|
helper.setTo(recipient);
|
||||||
|
helper.setSubject(subject);
|
||||||
|
|
||||||
|
// IMPORTANT: second argument 'true' means "this is HTML"
|
||||||
|
helper.setText(body, true);
|
||||||
|
|
||||||
mailSender.send(message);
|
mailSender.send(message);
|
||||||
|
|
||||||
@@ -60,6 +70,6 @@ public class EmailServiceImpl implements EmailService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteById(Integer id) {
|
public void deleteById(Integer id) {
|
||||||
deleteById(id);
|
emailRequestRepository.deleteById(Long.valueOf(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user