Dev/v1.0 (#1)
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local> Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -31,3 +31,6 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Uploads ###
|
||||
uploads/
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
container_name: bbqms-mysql
|
||||
container_name: qms-mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: bbqms
|
||||
MYSQL_DATABASE: qms
|
||||
volumes:
|
||||
- bbqms-mysql-data:/var/lib/mysql
|
||||
- qms-mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ppassword"]
|
||||
interval: 5s
|
||||
@@ -16,4 +16,4 @@ services:
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
bbqms-mysql-data:
|
||||
qms-mysql-data:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package ba.unsa.etf.si.bbqms.admin_service.api;
|
||||
|
||||
import ba.unsa.etf.si.bbqms.domain.Advertisement;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AdvertisementService {
|
||||
Advertisement create(String tenantCode,
|
||||
MultipartFile file,
|
||||
String title,
|
||||
Integer durationSeconds,
|
||||
Integer sortOrder,
|
||||
Boolean active) throws Exception;
|
||||
|
||||
List<Advertisement> listByTenant(String tenantCode);
|
||||
|
||||
List<Advertisement> listActiveByTenant(String tenantCode);
|
||||
|
||||
Advertisement update(String tenantCode,
|
||||
long adId,
|
||||
String title,
|
||||
Integer durationSeconds,
|
||||
Integer sortOrder,
|
||||
Boolean active) throws Exception;
|
||||
|
||||
void delete(String tenantCode, long adId) throws Exception;
|
||||
|
||||
Advertisement getForTenant(String tenantCode, long adId);
|
||||
|
||||
Advertisement findById(long adId);
|
||||
|
||||
Resource loadMedia(long adId) throws Exception;
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package ba.unsa.etf.si.bbqms.admin_service.implementation;
|
||||
|
||||
import ba.unsa.etf.si.bbqms.admin_service.api.AdvertisementService;
|
||||
import ba.unsa.etf.si.bbqms.domain.Advertisement;
|
||||
import ba.unsa.etf.si.bbqms.domain.AdvertisementMediaType;
|
||||
import ba.unsa.etf.si.bbqms.domain.Tenant;
|
||||
import ba.unsa.etf.si.bbqms.repository.AdvertisementRepository;
|
||||
import ba.unsa.etf.si.bbqms.tenant_service.api.TenantService;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class DefaultAdvertisementService implements AdvertisementService {
|
||||
private static final int DEFAULT_DURATION_SECONDS = 10;
|
||||
private static final Set<String> IMAGE_TYPES = Set.of(
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/gif"
|
||||
);
|
||||
private static final Set<String> VIDEO_TYPES = Set.of(
|
||||
"video/mp4",
|
||||
"video/webm"
|
||||
);
|
||||
|
||||
private final AdvertisementRepository advertisementRepository;
|
||||
private final TenantService tenantService;
|
||||
private final Path uploadRoot;
|
||||
|
||||
public DefaultAdvertisementService(final AdvertisementRepository advertisementRepository,
|
||||
final TenantService tenantService,
|
||||
@Value("${ads.upload-dir:uploads/ads}") final String uploadDir) {
|
||||
this.advertisementRepository = advertisementRepository;
|
||||
this.tenantService = tenantService;
|
||||
this.uploadRoot = Path.of(uploadDir).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advertisement create(final String tenantCode,
|
||||
final MultipartFile file,
|
||||
final String title,
|
||||
final Integer durationSeconds,
|
||||
final Integer sortOrder,
|
||||
final Boolean active) throws Exception {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File is required.");
|
||||
}
|
||||
|
||||
final String contentType = normalizeContentType(file.getContentType());
|
||||
final AdvertisementMediaType mediaType = resolveMediaType(contentType);
|
||||
final Tenant tenant = this.tenantService.findByCode(tenantCode);
|
||||
|
||||
final String originalName = file.getOriginalFilename() != null ? file.getOriginalFilename() : "upload";
|
||||
final String safeFileName = sanitizeFileName(originalName);
|
||||
final String storedName = UUID.randomUUID() + "-" + safeFileName;
|
||||
final Path tenantDir = this.uploadRoot.resolve(tenantCode).normalize();
|
||||
Files.createDirectories(tenantDir);
|
||||
|
||||
final Path destination = tenantDir.resolve(storedName).normalize();
|
||||
if (!destination.startsWith(tenantDir)) {
|
||||
throw new IllegalArgumentException("Invalid file path.");
|
||||
}
|
||||
|
||||
try {
|
||||
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (final IOException exception) {
|
||||
throw new IllegalStateException("Failed to store advertisement file.", exception);
|
||||
}
|
||||
|
||||
final String relativePath = this.uploadRoot.relativize(destination).toString().replace('\\', '/');
|
||||
final int nextSortOrder = sortOrder != null
|
||||
? sortOrder
|
||||
: this.advertisementRepository.findByTenant_CodeOrderBySortOrderAscIdAsc(tenantCode).stream()
|
||||
.mapToInt(Advertisement::getSortOrder)
|
||||
.max()
|
||||
.orElse(-1) + 1;
|
||||
|
||||
final Advertisement advertisement = new Advertisement(
|
||||
tenant,
|
||||
title,
|
||||
mediaType,
|
||||
originalName,
|
||||
contentType,
|
||||
relativePath,
|
||||
nextSortOrder,
|
||||
active == null || active,
|
||||
durationSeconds != null ? durationSeconds : DEFAULT_DURATION_SECONDS,
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
return this.advertisementRepository.save(advertisement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Advertisement> listByTenant(final String tenantCode) {
|
||||
this.tenantService.findByCode(tenantCode);
|
||||
return this.advertisementRepository.findByTenant_CodeOrderBySortOrderAscIdAsc(tenantCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Advertisement> listActiveByTenant(final String tenantCode) {
|
||||
this.tenantService.findByCode(tenantCode);
|
||||
return this.advertisementRepository.findByTenant_CodeAndActiveTrueOrderBySortOrderAscIdAsc(tenantCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advertisement update(final String tenantCode,
|
||||
final long adId,
|
||||
final String title,
|
||||
final Integer durationSeconds,
|
||||
final Integer sortOrder,
|
||||
final Boolean active) {
|
||||
final Advertisement advertisement = getForTenant(tenantCode, adId);
|
||||
|
||||
if (title != null) {
|
||||
advertisement.setTitle(title);
|
||||
}
|
||||
if (durationSeconds != null) {
|
||||
advertisement.setDurationSeconds(durationSeconds);
|
||||
}
|
||||
if (sortOrder != null) {
|
||||
advertisement.setSortOrder(sortOrder);
|
||||
}
|
||||
if (active != null) {
|
||||
advertisement.setActive(active);
|
||||
}
|
||||
|
||||
return this.advertisementRepository.save(advertisement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(final String tenantCode, final long adId) throws Exception {
|
||||
final Advertisement advertisement = getForTenant(tenantCode, adId);
|
||||
final Path filePath = resolveStoredPath(advertisement.getStoragePath());
|
||||
|
||||
this.advertisementRepository.delete(advertisement);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(filePath);
|
||||
} catch (final IOException exception) {
|
||||
// Row is already removed; log-worthy but don't fail the API for orphan files.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advertisement getForTenant(final String tenantCode, final long adId) {
|
||||
final Advertisement advertisement = this.advertisementRepository.get(adId);
|
||||
if (!advertisement.getTenant().getCode().equals(tenantCode)) {
|
||||
throw new EntityNotFoundException("Advertisement not found for tenant: " + tenantCode);
|
||||
}
|
||||
return advertisement;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advertisement findById(final long adId) {
|
||||
return this.advertisementRepository.get(adId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource loadMedia(final long adId) throws Exception {
|
||||
final Advertisement advertisement = this.advertisementRepository.get(adId);
|
||||
final Path filePath = resolveStoredPath(advertisement.getStoragePath());
|
||||
final Resource resource = new UrlResource(filePath.toUri());
|
||||
if (!resource.exists() || !resource.isReadable()) {
|
||||
throw new EntityNotFoundException("Advertisement media file not found.");
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
private Path resolveStoredPath(final String storagePath) {
|
||||
final Path resolved = this.uploadRoot.resolve(storagePath).normalize();
|
||||
if (!resolved.startsWith(this.uploadRoot)) {
|
||||
throw new IllegalArgumentException("Invalid storage path.");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static String normalizeContentType(final String contentType) {
|
||||
if (contentType == null || contentType.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing content type.");
|
||||
}
|
||||
return contentType.toLowerCase(Locale.ROOT).split(";")[0].trim();
|
||||
}
|
||||
|
||||
private static AdvertisementMediaType resolveMediaType(final String contentType) {
|
||||
if (IMAGE_TYPES.contains(contentType)) {
|
||||
return AdvertisementMediaType.IMAGE;
|
||||
}
|
||||
if (VIDEO_TYPES.contains(contentType)) {
|
||||
return AdvertisementMediaType.VIDEO;
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported media type: " + contentType);
|
||||
}
|
||||
|
||||
private static String sanitizeFileName(final String originalName) {
|
||||
final String name = Path.of(originalName).getFileName().toString();
|
||||
final String sanitized = name.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
return sanitized.isBlank() ? "upload" : sanitized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package ba.unsa.etf.si.bbqms.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "advertisement")
|
||||
public class Advertisement {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "tenant_id", referencedColumnName = "id")
|
||||
private Tenant tenant;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "media_type", nullable = false)
|
||||
private AdvertisementMediaType mediaType;
|
||||
|
||||
@Column(name = "file_name", nullable = false)
|
||||
private String fileName;
|
||||
|
||||
@Column(name = "content_type", nullable = false)
|
||||
private String contentType;
|
||||
|
||||
@Column(name = "storage_path", nullable = false)
|
||||
private String storagePath;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private int sortOrder;
|
||||
|
||||
@Column(name = "active", nullable = false)
|
||||
private boolean active;
|
||||
|
||||
@Column(name = "duration_seconds", nullable = false)
|
||||
private int durationSeconds;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
public Advertisement() {
|
||||
}
|
||||
|
||||
public Advertisement(final Tenant tenant,
|
||||
final String title,
|
||||
final AdvertisementMediaType mediaType,
|
||||
final String fileName,
|
||||
final String contentType,
|
||||
final String storagePath,
|
||||
final int sortOrder,
|
||||
final boolean active,
|
||||
final int durationSeconds,
|
||||
final Instant createdAt) {
|
||||
this.tenant = tenant;
|
||||
this.title = title;
|
||||
this.mediaType = mediaType;
|
||||
this.fileName = fileName;
|
||||
this.contentType = contentType;
|
||||
this.storagePath = storagePath;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
this.durationSeconds = durationSeconds;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(final long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Tenant getTenant() {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
public void setTenant(final Tenant tenant) {
|
||||
this.tenant = tenant;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(final String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public AdvertisementMediaType getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
public void setMediaType(final AdvertisementMediaType mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(final String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(final String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
public void setStoragePath(final String storagePath) {
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
|
||||
public int getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(final int sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(final boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public int getDurationSeconds() {
|
||||
return durationSeconds;
|
||||
}
|
||||
|
||||
public void setDurationSeconds(final int durationSeconds) {
|
||||
this.durationSeconds = durationSeconds;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(final Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ba.unsa.etf.si.bbqms.domain;
|
||||
|
||||
public enum AdvertisementMediaType {
|
||||
IMAGE,
|
||||
VIDEO
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ba.unsa.etf.si.bbqms.repository;
|
||||
|
||||
import ba.unsa.etf.si.bbqms.domain.Advertisement;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AdvertisementRepository extends BaseRepository<Advertisement, Long> {
|
||||
List<Advertisement> findByTenant_CodeOrderBySortOrderAscIdAsc(String tenantCode);
|
||||
|
||||
List<Advertisement> findByTenant_CodeAndActiveTrueOrderBySortOrderAscIdAsc(String tenantCode);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package ba.unsa.etf.si.bbqms.ws.controllers;
|
||||
|
||||
import ba.unsa.etf.si.bbqms.admin_service.api.AdvertisementService;
|
||||
import ba.unsa.etf.si.bbqms.auth_service.api.AuthService;
|
||||
import ba.unsa.etf.si.bbqms.domain.Advertisement;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.AdvertisementDto;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.SimpleMessageDto;
|
||||
import org.springframework.core.io.Resource;
|
||||
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.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ads")
|
||||
public class AdvertisementController {
|
||||
private final AdvertisementService advertisementService;
|
||||
private final AuthService authService;
|
||||
|
||||
public AdvertisementController(final AdvertisementService advertisementService,
|
||||
final AuthService authService) {
|
||||
this.advertisementService = advertisementService;
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@GetMapping("/media/{adId}")
|
||||
public ResponseEntity streamMedia(@PathVariable final long adId) {
|
||||
try {
|
||||
final Advertisement advertisement = this.advertisementService.findById(adId);
|
||||
final Resource resource = this.advertisementService.loadMedia(adId);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + advertisement.getFileName() + "\"")
|
||||
.contentType(MediaType.parseMediaType(advertisement.getContentType()))
|
||||
.body(resource);
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantCode}/active")
|
||||
public ResponseEntity listActiveAdvertisements(@PathVariable final String tenantCode) {
|
||||
try {
|
||||
final List<AdvertisementDto> ads = this.advertisementService.listActiveByTenant(tenantCode).stream()
|
||||
.map(AdvertisementDto::fromEntity)
|
||||
.toList();
|
||||
return ResponseEntity.ok().body(ads);
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{tenantCode}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity createAdvertisement(@PathVariable final String tenantCode,
|
||||
@RequestParam("file") final MultipartFile file,
|
||||
@RequestParam(value = "title", required = false) final String title,
|
||||
@RequestParam(value = "durationSeconds", required = false) final Integer durationSeconds,
|
||||
@RequestParam(value = "sortOrder", required = false) final Integer sortOrder,
|
||||
@RequestParam(value = "active", required = false) final Boolean active) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final Advertisement created = this.advertisementService.create(
|
||||
tenantCode,
|
||||
file,
|
||||
title,
|
||||
durationSeconds,
|
||||
sortOrder,
|
||||
active
|
||||
);
|
||||
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(created));
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantCode}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity listAdvertisements(@PathVariable final String tenantCode) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final List<AdvertisementDto> ads = this.advertisementService.listByTenant(tenantCode).stream()
|
||||
.map(AdvertisementDto::fromEntity)
|
||||
.toList();
|
||||
return ResponseEntity.ok().body(ads);
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{tenantCode}/{adId}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity updateAdvertisement(@PathVariable final String tenantCode,
|
||||
@PathVariable final long adId,
|
||||
@RequestBody final AdvertisementUpdateRequest request) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final Advertisement updated = this.advertisementService.update(
|
||||
tenantCode,
|
||||
adId,
|
||||
request.title(),
|
||||
request.durationSeconds(),
|
||||
request.sortOrder(),
|
||||
request.active()
|
||||
);
|
||||
return ResponseEntity.ok().body(AdvertisementDto.fromEntity(updated));
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{tenantCode}/{adId}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity deleteAdvertisement(@PathVariable final String tenantCode,
|
||||
@PathVariable final long adId) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
this.advertisementService.delete(tenantCode, adId);
|
||||
return ResponseEntity.ok().body(new SimpleMessageDto("Deleted advertisement with id: " + adId));
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().body(new SimpleMessageDto(exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public record AdvertisementUpdateRequest(String title,
|
||||
Integer durationSeconds,
|
||||
Integer sortOrder,
|
||||
Boolean active) {
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
@@ -186,6 +187,7 @@ public class BranchController {
|
||||
@GetMapping("/{tenantCode}/{branchId}/queue")
|
||||
public ResponseEntity getBranchQueue(@PathVariable final String tenantCode,
|
||||
@PathVariable final String branchId,
|
||||
@RequestParam(defaultValue = "false") final boolean activeOnly,
|
||||
final QueueStateParams queueStateParams,
|
||||
final Sort sort) {
|
||||
final Branch branch = this.branchService.findById(Long.parseLong(branchId)).orElseThrow();
|
||||
@@ -193,6 +195,9 @@ public class BranchController {
|
||||
final TicketRepository ticketRepository = this.ticketService.unwrap(TicketRepository.class);
|
||||
|
||||
Specification<Ticket> filter = TicketSpecs.branchIdEquals(branch.getId());
|
||||
if (activeOnly) {
|
||||
filter = filter.and(TicketSpecs.deletedEquals(false));
|
||||
}
|
||||
if (queueStateParams.serviceId() != null && queueStateParams.serviceId().isPresent()) {
|
||||
filter = filter.and(TicketSpecs.serviceIdEquals(queueStateParams.serviceId().get()));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import ba.unsa.etf.si.bbqms.ticket_service.api.TicketService;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.DisplayDto;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.ErrorResponseDto;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.ServiceDto;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.ServiceResponseDto;
|
||||
import ba.unsa.etf.si.bbqms.ws.models.TicketDto;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -28,8 +27,8 @@ public class TellerStationController {
|
||||
private final TicketService ticketService;
|
||||
|
||||
public TellerStationController(final StationService stationService,
|
||||
final AuthService authService,
|
||||
final TicketService ticketService) {
|
||||
final AuthService authService,
|
||||
final TicketService ticketService) {
|
||||
this.stationService = stationService;
|
||||
this.authService = authService;
|
||||
this.ticketService = ticketService;
|
||||
@@ -37,7 +36,7 @@ public class TellerStationController {
|
||||
|
||||
@GetMapping("/{tenantCode}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity getAll(@PathVariable final String tenantCode){
|
||||
public ResponseEntity getAll(@PathVariable final String tenantCode) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
@@ -56,43 +55,45 @@ public class TellerStationController {
|
||||
@GetMapping("/{tenantCode}/{stationId}/services")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity getServices(@PathVariable final String tenantCode,
|
||||
@PathVariable final String stationId,
|
||||
@RequestParam(defaultValue = "true") final boolean assigned) {
|
||||
@PathVariable final String stationId,
|
||||
@RequestParam(defaultValue = "true") final boolean assigned) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final Set<Service> serviceSet = this.stationService.getServicesByAssigned(Long.parseLong(stationId),assigned);
|
||||
final Set<ServiceResponseDto> serviceResponseDtoSet = serviceSet.stream()
|
||||
.map(ServiceResponseDto::fromEntity)
|
||||
final Set<Service> serviceSet = this.stationService.getServicesByAssigned(Long.parseLong(stationId),
|
||||
assigned);
|
||||
final Set<ServiceDto> serviceDtoSet = serviceSet.stream()
|
||||
.map(ServiceDto::fromEntity)
|
||||
.collect(Collectors.toSet());
|
||||
return ResponseEntity.ok().body(serviceResponseDtoSet);
|
||||
}
|
||||
catch (final Exception e) {
|
||||
return ResponseEntity.ok().body(serviceDtoSet);
|
||||
} catch (final Exception e) {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponseDto(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantCode}/{stationId}/services/assignable")
|
||||
public ResponseEntity findAssignableServices(@PathVariable final String stationId,
|
||||
@PathVariable final String tenantCode) {
|
||||
@PathVariable final String tenantCode) {
|
||||
return ResponseEntity.ok().body(
|
||||
this.stationService.findAssignableServices(Long.parseLong(stationId))
|
||||
);
|
||||
this.stationService.findAssignableServices(Long.parseLong(stationId)).stream()
|
||||
.map(ServiceDto::fromEntity)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@PutMapping("/{tenantCode}/{stationId}/services/{serviceId}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity addTellerStationService(@PathVariable final String tenantCode,
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String serviceId) {
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String serviceId) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final TellerStation updatedTellerStation = this.stationService.addTellerStationService(Long.parseLong(stationId), Long.parseLong(serviceId));
|
||||
final TellerStation updatedTellerStation = this.stationService
|
||||
.addTellerStationService(Long.parseLong(stationId), Long.parseLong(serviceId));
|
||||
return ResponseEntity.ok().body(TellerStationResponseDto.fromEntity(updatedTellerStation));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
@@ -102,14 +103,15 @@ public class TellerStationController {
|
||||
@DeleteMapping("/{tenantCode}/{stationId}/services/{serviceId}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity deleteTellerStationService(@PathVariable final String tenantCode,
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String serviceId) {
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String serviceId) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final TellerStation updatedTellerStation = this.stationService.deleteTellerStationService(Long.parseLong(stationId), Long.parseLong(serviceId));
|
||||
final TellerStation updatedTellerStation = this.stationService
|
||||
.deleteTellerStationService(Long.parseLong(stationId), Long.parseLong(serviceId));
|
||||
return ResponseEntity.ok().body(TellerStationResponseDto.fromEntity(updatedTellerStation));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponseDto(e.getMessage()));
|
||||
@@ -119,16 +121,17 @@ public class TellerStationController {
|
||||
@PutMapping("/{tenantCode}/{stationId}/displays/{displayId}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity addDisplayToStation(@PathVariable final String tenantCode,
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String displayId) {
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String displayId) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final TellerStation updatedTellerStation = this.stationService.addTellerStationDisplay(Long.parseLong(stationId), Long.parseLong(displayId));
|
||||
final TellerStation updatedTellerStation = this.stationService
|
||||
.addTellerStationDisplay(Long.parseLong(stationId), Long.parseLong(displayId));
|
||||
return ResponseEntity.ok().body(TellerStationResponseDto.fromEntity(updatedTellerStation));
|
||||
} catch(final Exception exception) {
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
@@ -136,29 +139,29 @@ public class TellerStationController {
|
||||
@DeleteMapping("/{tenantCode}/{stationId}/displays/{displayId}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN', 'ROLE_BRANCH_ADMIN')")
|
||||
public ResponseEntity removeDisplayFromStation(@PathVariable final String tenantCode,
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String displayId) {
|
||||
@PathVariable final String stationId,
|
||||
@PathVariable final String displayId) {
|
||||
if (!this.authService.canChangeTenant(tenantCode)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
final TellerStation updatedTellerStation = this.stationService.deleteTellerStationDisplay(Long.parseLong(stationId), Long.parseLong(displayId));
|
||||
final TellerStation updatedTellerStation = this.stationService
|
||||
.deleteTellerStationDisplay(Long.parseLong(stationId), Long.parseLong(displayId));
|
||||
return ResponseEntity.ok().body(TellerStationResponseDto.fromEntity(updatedTellerStation));
|
||||
} catch(final Exception exception) {
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantCode}/{branchId}")
|
||||
public ResponseEntity getBranchStations(@PathVariable final String tenantCode,
|
||||
@PathVariable final String branchId) {
|
||||
try{
|
||||
@PathVariable final String branchId) {
|
||||
try {
|
||||
return ResponseEntity.ok().body(
|
||||
this.stationService.getAllByBranch(Long.parseLong(branchId)).stream()
|
||||
.map(TellerStationResponseDto::fromEntity)
|
||||
.toList()
|
||||
);
|
||||
.toList());
|
||||
} catch (final Exception exception) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
@@ -168,7 +171,8 @@ public class TellerStationController {
|
||||
public ResponseEntity getTicketsForTellerStation(@PathVariable final long stationId) {
|
||||
try {
|
||||
final List<TicketDto> ticketDtos = this.ticketService.findWithStation(stationId).stream()
|
||||
.filter(ticket -> ticket.getTellerStation() == null || ticket.getTellerStation().getId() == stationId)
|
||||
.filter(ticket -> ticket.getTellerStation() == null
|
||||
|| ticket.getTellerStation().getId() == stationId)
|
||||
.map(TicketDto::fromEntity)
|
||||
.toList();
|
||||
|
||||
@@ -178,15 +182,14 @@ public class TellerStationController {
|
||||
}
|
||||
}
|
||||
|
||||
public record TellerStationResponseDto(long id, String name, DisplayDto display, Set<ServiceResponseDto> services) {
|
||||
public record TellerStationResponseDto(long id, String name, DisplayDto display, Set<ServiceDto> services) {
|
||||
public static TellerStationResponseDto fromEntity(final TellerStation tellerStation) {
|
||||
final Set<Service> serviceSet = tellerStation.getServices();
|
||||
return new TellerStationResponseDto(
|
||||
tellerStation.getId(),
|
||||
tellerStation.getName(),
|
||||
tellerStation.getDisplay() != null ? DisplayDto.fromEntity(tellerStation.getDisplay()) : null,
|
||||
serviceSet.stream().map(ServiceResponseDto::fromEntity).collect(Collectors.toSet())
|
||||
);
|
||||
serviceSet.stream().map(ServiceDto::fromEntity).collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ public class TenantController {
|
||||
private final AuthService authService;
|
||||
|
||||
public TenantController(final TenantService tenantService,
|
||||
final BranchService branchService,
|
||||
final GroupService groupService,
|
||||
final AuthService authService) {
|
||||
final BranchService branchService,
|
||||
final GroupService groupService,
|
||||
final AuthService authService) {
|
||||
this.tenantService = tenantService;
|
||||
this.branchService = branchService;
|
||||
this.groupService = groupService;
|
||||
@@ -57,7 +57,8 @@ public class TenantController {
|
||||
|
||||
@PutMapping("/{code}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity updateTenant(@PathVariable final String code, @RequestBody final TenantDto request) throws Exception {
|
||||
public ResponseEntity updateTenant(@PathVariable final String code, @RequestBody final TenantDto request)
|
||||
throws Exception {
|
||||
if (!this.authService.canChangeTenant(code)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
@@ -71,7 +72,8 @@ public class TenantController {
|
||||
|
||||
@PostMapping("/{code}/services")
|
||||
@PreAuthorize("hasAnyRole('ROLE_BRANCH_ADMIN', 'ROLE_SUPER_ADMIN')")
|
||||
public ResponseEntity addService(@PathVariable(name = "code") final String code, @RequestBody final ServiceRequestDto serviceRequestDto) {
|
||||
public ResponseEntity addService(@PathVariable(name = "code") final String code,
|
||||
@RequestBody final ServiceRequestDto serviceRequestDto) {
|
||||
if (!this.authService.canChangeTenant(code)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
@@ -92,7 +94,8 @@ public class TenantController {
|
||||
}
|
||||
|
||||
@GetMapping("/{code}/services/group/{groupId}")
|
||||
public ResponseEntity listGroupAssignableServices(@PathVariable final String code, @PathVariable final String groupId) {
|
||||
public ResponseEntity listGroupAssignableServices(@PathVariable final String code,
|
||||
@PathVariable final String groupId) {
|
||||
final List<Service> possibleServices = this.tenantService.getAllServicesByTenant(code);
|
||||
final List<Service> assignedServices = this.groupService.get(Long.parseLong(groupId))
|
||||
.getServices().stream().toList();
|
||||
@@ -105,8 +108,8 @@ public class TenantController {
|
||||
@PutMapping("/{code}/services/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_BRANCH_ADMIN', 'ROLE_SUPER_ADMIN')")
|
||||
public ResponseEntity updateService(@PathVariable(name = "code") final String code,
|
||||
@PathVariable(name = "id") final Long id,
|
||||
@RequestBody final ServiceRequestDto request) {
|
||||
@PathVariable(name = "id") final Long id,
|
||||
@RequestBody final ServiceRequestDto request) {
|
||||
if (!this.authService.canChangeTenant(code)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
@@ -120,7 +123,8 @@ public class TenantController {
|
||||
|
||||
@DeleteMapping("/{code}/services/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_BRANCH_ADMIN', 'ROLE_SUPER_ADMIN')")
|
||||
public ResponseEntity deleteService(@PathVariable(name = "code") final String code, @PathVariable(name = "id") final Long id) {
|
||||
public ResponseEntity deleteService(@PathVariable(name = "code") final String code,
|
||||
@PathVariable(name = "id") final Long id) {
|
||||
if (!this.authService.canChangeTenant(code)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package ba.unsa.etf.si.bbqms.ws.models;
|
||||
|
||||
import ba.unsa.etf.si.bbqms.domain.Advertisement;
|
||||
import ba.unsa.etf.si.bbqms.domain.AdvertisementMediaType;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record AdvertisementDto(
|
||||
long id,
|
||||
String title,
|
||||
AdvertisementMediaType mediaType,
|
||||
String fileName,
|
||||
String contentType,
|
||||
String mediaUrl,
|
||||
int sortOrder,
|
||||
boolean active,
|
||||
int durationSeconds,
|
||||
Instant createdAt
|
||||
) {
|
||||
public static AdvertisementDto fromEntity(final Advertisement advertisement) {
|
||||
return new AdvertisementDto(
|
||||
advertisement.getId(),
|
||||
advertisement.getTitle(),
|
||||
advertisement.getMediaType(),
|
||||
advertisement.getFileName(),
|
||||
advertisement.getContentType(),
|
||||
"/api/v1/ads/media/" + advertisement.getId(),
|
||||
advertisement.getSortOrder(),
|
||||
advertisement.isActive(),
|
||||
advertisement.getDurationSeconds(),
|
||||
advertisement.getCreatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
spring:
|
||||
application:
|
||||
name: bbqms
|
||||
name: qms
|
||||
jpa:
|
||||
database: mysql
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/bbqms
|
||||
url: jdbc:mysql://localhost:3306/qms
|
||||
username: root
|
||||
password: password
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
@@ -16,19 +20,21 @@ spring:
|
||||
google:
|
||||
client-id: dummy-google-client-id
|
||||
flyway:
|
||||
schemas: bbqms
|
||||
schemas: qms
|
||||
|
||||
jwt:
|
||||
header-title: Authorization
|
||||
token-prefix: Bearer
|
||||
secret-key: dummy-jwt-secret-key-for-local-dev-only-min-32-chars
|
||||
secret-key: a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF
|
||||
authorities-key: USER_AUTHORITIES
|
||||
token-validity-time: PT30M
|
||||
tfa:
|
||||
label: BBQMS
|
||||
issuer: BBQMS
|
||||
label: QMS
|
||||
issuer: QMS
|
||||
tenancy:
|
||||
default-code: DFLT
|
||||
notifications:
|
||||
expo-url: https://exp.host/--/api/v2/push/send
|
||||
mock: true
|
||||
ads:
|
||||
upload-dir: uploads/ads
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS advertisement
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NULL,
|
||||
media_type VARCHAR(32) NOT NULL,
|
||||
file_name VARCHAR(512) NOT NULL,
|
||||
content_type VARCHAR(128) NOT NULL,
|
||||
storage_path VARCHAR(1024) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
active BOOL NOT NULL DEFAULT TRUE,
|
||||
duration_seconds INTEGER NOT NULL DEFAULT 10,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT FK_advertisement_tenant FOREIGN KEY (tenant_id) REFERENCES tenant (id)
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user