Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 498ea07b9e | |||
| 08f8058efc | |||
| 0efc31b000 | |||
| 3e00962828 |
@@ -2,8 +2,10 @@ package pl.polskalokalnie.admin;
|
|||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -11,9 +13,11 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import java.time.LocalDate;
|
||||||
import pl.polskalokalnie.moderation.ForbiddenWordResponse;
|
import pl.polskalokalnie.moderation.ForbiddenWordResponse;
|
||||||
import pl.polskalokalnie.moderation.TextModerationService;
|
import pl.polskalokalnie.moderation.TextModerationService;
|
||||||
import pl.polskalokalnie.auth.dto.UserResponse;
|
import pl.polskalokalnie.auth.dto.UserResponse;
|
||||||
@@ -36,23 +40,33 @@ import pl.polskalokalnie.user.UserRepository;
|
|||||||
@RequestMapping("/api/admin")
|
@RequestMapping("/api/admin")
|
||||||
public class AdminController {
|
public class AdminController {
|
||||||
|
|
||||||
|
private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%";
|
||||||
|
private static final int TEMP_PASSWORD_LENGTH = 12;
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final BlockedEmailRepository blockedEmailRepository;
|
private final BlockedEmailRepository blockedEmailRepository;
|
||||||
private final ListingService listingService;
|
private final ListingService listingService;
|
||||||
private final ListingReportService listingReportService;
|
private final ListingReportService listingReportService;
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
private final TextModerationService textModerationService;
|
private final TextModerationService textModerationService;
|
||||||
|
private final AdminStatsService adminStatsService;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
public AdminController(UserRepository userRepository, BlockedEmailRepository blockedEmailRepository,
|
public AdminController(UserRepository userRepository, BlockedEmailRepository blockedEmailRepository,
|
||||||
ListingService listingService, ListingReportService listingReportService,
|
ListingService listingService, ListingReportService listingReportService,
|
||||||
MessageService messageService,
|
MessageService messageService,
|
||||||
TextModerationService textModerationService) {
|
TextModerationService textModerationService,
|
||||||
|
AdminStatsService adminStatsService,
|
||||||
|
PasswordEncoder passwordEncoder) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.blockedEmailRepository = blockedEmailRepository;
|
this.blockedEmailRepository = blockedEmailRepository;
|
||||||
this.listingService = listingService;
|
this.listingService = listingService;
|
||||||
this.listingReportService = listingReportService;
|
this.listingReportService = listingReportService;
|
||||||
this.messageService = messageService;
|
this.messageService = messageService;
|
||||||
this.textModerationService = textModerationService;
|
this.textModerationService = textModerationService;
|
||||||
|
this.adminStatsService = adminStatsService;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Uzytkownicy ---
|
// --- Uzytkownicy ---
|
||||||
@@ -82,6 +96,50 @@ public class AdminController {
|
|||||||
return UserResponse.from(userRepository.save(user));
|
return UserResponse.from(userRepository.save(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/users/{id}/grant-admin")
|
||||||
|
public UserResponse grantAdmin(@PathVariable Long id) {
|
||||||
|
AppUser user = requireUser(id);
|
||||||
|
if (user.getRole() == Role.ADMIN) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Użytkownik już ma rolę administratora");
|
||||||
|
}
|
||||||
|
user.setRole(Role.ADMIN);
|
||||||
|
user.setVerified(true);
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/users/{id}/revoke-admin")
|
||||||
|
public UserResponse revokeAdmin(@PathVariable Long id, Authentication authentication) {
|
||||||
|
AppUser user = requireUser(id);
|
||||||
|
if (user.getRole() != Role.ADMIN) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Użytkownik nie ma roli administratora");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser currentAdmin = requireAdmin(authentication);
|
||||||
|
if (currentAdmin.getId().equals(user.getId())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nie możesz odebrać roli administratora samemu sobie");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRepository.countByRole(Role.ADMIN) <= 1) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nie można odebrać roli ostatniemu administratorowi");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setRole(Role.USER);
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/users/{id}/reset-password")
|
||||||
|
public ResetPasswordResponse resetPassword(@PathVariable Long id) {
|
||||||
|
AppUser user = requireUser(id);
|
||||||
|
if (user.getRole() == Role.ADMIN) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nie można resetować hasła konta administratora z tego widoku");
|
||||||
|
}
|
||||||
|
|
||||||
|
String temporaryPassword = generateTemporaryPassword();
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(temporaryPassword));
|
||||||
|
userRepository.save(user);
|
||||||
|
return new ResetPasswordResponse(temporaryPassword);
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/users/{id}")
|
@DeleteMapping("/users/{id}")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
public void deleteUser(@PathVariable Long id) {
|
public void deleteUser(@PathVariable Long id) {
|
||||||
@@ -205,4 +263,27 @@ public class AdminController {
|
|||||||
public void deleteForbiddenWord(@PathVariable Long id) {
|
public void deleteForbiddenWord(@PathVariable Long id) {
|
||||||
textModerationService.removeForbiddenWord(id);
|
textModerationService.removeForbiddenWord(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Statystyki ---
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public AdminStatsResponse stats(
|
||||||
|
@RequestParam(name = "days", defaultValue = "1") int days,
|
||||||
|
@RequestParam(name = "from", required = false) LocalDate from,
|
||||||
|
@RequestParam(name = "to", required = false) LocalDate to
|
||||||
|
) {
|
||||||
|
if (from != null || to != null) {
|
||||||
|
return adminStatsService.getStats(from, to);
|
||||||
|
}
|
||||||
|
return adminStatsService.getStats(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateTemporaryPassword() {
|
||||||
|
StringBuilder password = new StringBuilder(TEMP_PASSWORD_LENGTH);
|
||||||
|
for (int index = 0; index < TEMP_PASSWORD_LENGTH; index++) {
|
||||||
|
int randomIndex = SECURE_RANDOM.nextInt(PASSWORD_ALPHABET.length());
|
||||||
|
password.append(PASSWORD_ALPHABET.charAt(randomIndex));
|
||||||
|
}
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package pl.polskalokalnie.admin;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record AdminStatsResponse(
|
||||||
|
int periodDays,
|
||||||
|
LocalDate fromDate,
|
||||||
|
LocalDate toDate,
|
||||||
|
Instant generatedAt,
|
||||||
|
Summary summary,
|
||||||
|
Period period,
|
||||||
|
List<DailyPoint> daily
|
||||||
|
) {
|
||||||
|
public record Summary(
|
||||||
|
long activeListings,
|
||||||
|
long pendingListings,
|
||||||
|
long totalUsers,
|
||||||
|
long pendingVerificationUsers,
|
||||||
|
long openReports,
|
||||||
|
long messagesToAdminTotal,
|
||||||
|
long blockedUsers
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Period(
|
||||||
|
long messagesToAdmin,
|
||||||
|
long newListings,
|
||||||
|
long newReports,
|
||||||
|
long newPersonalAccounts,
|
||||||
|
long newCompanyAccounts
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DailyPoint(
|
||||||
|
LocalDate date,
|
||||||
|
long messagesToAdmin,
|
||||||
|
long newListings,
|
||||||
|
long newReports,
|
||||||
|
long newPersonalAccounts,
|
||||||
|
long newCompanyAccounts
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package pl.polskalokalnie.admin;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import pl.polskalokalnie.listing.ListingRepository;
|
||||||
|
import pl.polskalokalnie.listing.ListingStatus;
|
||||||
|
import pl.polskalokalnie.listing.PropertyListing;
|
||||||
|
import pl.polskalokalnie.message.Message;
|
||||||
|
import pl.polskalokalnie.message.MessageRepository;
|
||||||
|
import pl.polskalokalnie.report.ListingReport;
|
||||||
|
import pl.polskalokalnie.report.ListingReportRepository;
|
||||||
|
import pl.polskalokalnie.report.ListingReportStatus;
|
||||||
|
import pl.polskalokalnie.user.AccountType;
|
||||||
|
import pl.polskalokalnie.user.AppUser;
|
||||||
|
import pl.polskalokalnie.user.Role;
|
||||||
|
import pl.polskalokalnie.user.UserRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminStatsService {
|
||||||
|
|
||||||
|
private static final int MIN_PERIOD_DAYS = 1;
|
||||||
|
private static final int MAX_PERIOD_DAYS = 60;
|
||||||
|
private static final ZoneId ZONE_ID = ZoneId.of("Europe/Warsaw");
|
||||||
|
|
||||||
|
private final ListingRepository listingRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final ListingReportRepository listingReportRepository;
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
|
||||||
|
public AdminStatsService(
|
||||||
|
ListingRepository listingRepository,
|
||||||
|
UserRepository userRepository,
|
||||||
|
ListingReportRepository listingReportRepository,
|
||||||
|
MessageRepository messageRepository
|
||||||
|
) {
|
||||||
|
this.listingRepository = listingRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.listingReportRepository = listingReportRepository;
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminStatsResponse getStats(int requestedPeriodDays) {
|
||||||
|
int periodDays = Math.max(MIN_PERIOD_DAYS, Math.min(MAX_PERIOD_DAYS, requestedPeriodDays));
|
||||||
|
Instant now = Instant.now();
|
||||||
|
LocalDate today = LocalDate.now(ZONE_ID);
|
||||||
|
LocalDate fromDate = today.minusDays(periodDays - 1L);
|
||||||
|
return buildStats(fromDate, today, periodDays, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminStatsResponse getStats(LocalDate requestedFromDate, LocalDate requestedToDate) {
|
||||||
|
LocalDate today = LocalDate.now(ZONE_ID);
|
||||||
|
LocalDate toDate = requestedToDate == null ? today : requestedToDate;
|
||||||
|
LocalDate fromDate = requestedFromDate == null ? toDate : requestedFromDate;
|
||||||
|
|
||||||
|
if (fromDate.isAfter(toDate)) {
|
||||||
|
LocalDate swapped = fromDate;
|
||||||
|
fromDate = toDate;
|
||||||
|
toDate = swapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toDate.isAfter(today)) {
|
||||||
|
toDate = today;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromDate.isAfter(toDate)) {
|
||||||
|
fromDate = toDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
long rawDays = java.time.temporal.ChronoUnit.DAYS.between(fromDate, toDate) + 1L;
|
||||||
|
int periodDays = (int) Math.max(MIN_PERIOD_DAYS, Math.min(MAX_PERIOD_DAYS, rawDays));
|
||||||
|
if (rawDays > MAX_PERIOD_DAYS) {
|
||||||
|
fromDate = toDate.minusDays(MAX_PERIOD_DAYS - 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant now = Instant.now();
|
||||||
|
return buildStats(fromDate, toDate, periodDays, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminStatsResponse buildStats(LocalDate fromDate, LocalDate toDate, int periodDays, Instant generatedAt) {
|
||||||
|
Instant periodStart = fromDate.atStartOfDay(ZONE_ID).toInstant();
|
||||||
|
Instant periodEndExclusive = toDate.plusDays(1L).atStartOfDay(ZONE_ID).toInstant();
|
||||||
|
Instant periodEndInclusive = generatedAt.isBefore(periodEndExclusive) ? generatedAt : periodEndExclusive.minusNanos(1L);
|
||||||
|
|
||||||
|
List<PropertyListing> listings = listingRepository.findAll();
|
||||||
|
List<AppUser> users = userRepository.findAll();
|
||||||
|
List<ListingReport> reports = listingReportRepository.findAll();
|
||||||
|
List<Message> messages = messageRepository.findAll();
|
||||||
|
|
||||||
|
Long adminId = users.stream()
|
||||||
|
.filter(user -> user.getRole() == Role.ADMIN)
|
||||||
|
.map(AppUser::getId)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
List<AppUser> regularUsers = users.stream()
|
||||||
|
.filter(user -> user.getRole() != Role.ADMIN)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Message> messagesToAdmin = adminId == null
|
||||||
|
? List.of()
|
||||||
|
: messages.stream()
|
||||||
|
.filter(message -> message.getRecipientId().equals(adminId) && !message.getSenderId().equals(adminId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
AdminStatsResponse.Summary summary = new AdminStatsResponse.Summary(
|
||||||
|
listings.stream().filter(item -> item.getStatus() == ListingStatus.APPROVED).count(),
|
||||||
|
listings.stream().filter(item -> item.getStatus() == ListingStatus.PENDING).count(),
|
||||||
|
regularUsers.size(),
|
||||||
|
regularUsers.stream().filter(item -> !item.isVerified()).count(),
|
||||||
|
reports.stream().filter(item -> item.getStatus() == ListingReportStatus.OPEN).count(),
|
||||||
|
messagesToAdmin.size(),
|
||||||
|
regularUsers.stream().filter(AppUser::isBlocked).count()
|
||||||
|
);
|
||||||
|
|
||||||
|
AdminStatsResponse.Period period = new AdminStatsResponse.Period(
|
||||||
|
messagesToAdmin.stream().filter(item -> inRange(item.getCreatedAt(), periodStart, periodEndInclusive)).count(),
|
||||||
|
listings.stream().filter(item -> inRange(item.getCreatedAt(), periodStart, periodEndInclusive)).count(),
|
||||||
|
reports.stream().filter(item -> inRange(item.getCreatedAt(), periodStart, periodEndInclusive)).count(),
|
||||||
|
regularUsers.stream()
|
||||||
|
.filter(item -> item.getAccountType() == AccountType.PERSONAL)
|
||||||
|
.filter(item -> inRange(item.getCreatedAt(), periodStart, periodEndInclusive))
|
||||||
|
.count(),
|
||||||
|
regularUsers.stream()
|
||||||
|
.filter(item -> item.getAccountType() == AccountType.COMPANY)
|
||||||
|
.filter(item -> inRange(item.getCreatedAt(), periodStart, periodEndInclusive))
|
||||||
|
.count()
|
||||||
|
);
|
||||||
|
|
||||||
|
List<AdminStatsResponse.DailyPoint> daily = new ArrayList<>();
|
||||||
|
for (LocalDate current = fromDate; !current.isAfter(toDate); current = current.plusDays(1)) {
|
||||||
|
LocalDate nextDate = current.plusDays(1);
|
||||||
|
Instant dayStart = current.atStartOfDay(ZONE_ID).toInstant();
|
||||||
|
Instant dayEnd = nextDate.atStartOfDay(ZONE_ID).toInstant();
|
||||||
|
|
||||||
|
daily.add(new AdminStatsResponse.DailyPoint(
|
||||||
|
current,
|
||||||
|
messagesToAdmin.stream().filter(item -> inRangeExclusiveEnd(item.getCreatedAt(), dayStart, dayEnd)).count(),
|
||||||
|
listings.stream().filter(item -> inRangeExclusiveEnd(item.getCreatedAt(), dayStart, dayEnd)).count(),
|
||||||
|
reports.stream().filter(item -> inRangeExclusiveEnd(item.getCreatedAt(), dayStart, dayEnd)).count(),
|
||||||
|
regularUsers.stream()
|
||||||
|
.filter(item -> item.getAccountType() == AccountType.PERSONAL)
|
||||||
|
.filter(item -> inRangeExclusiveEnd(item.getCreatedAt(), dayStart, dayEnd))
|
||||||
|
.count(),
|
||||||
|
regularUsers.stream()
|
||||||
|
.filter(item -> item.getAccountType() == AccountType.COMPANY)
|
||||||
|
.filter(item -> inRangeExclusiveEnd(item.getCreatedAt(), dayStart, dayEnd))
|
||||||
|
.count()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
daily.sort(Comparator.comparing(AdminStatsResponse.DailyPoint::date));
|
||||||
|
|
||||||
|
return new AdminStatsResponse(periodDays, fromDate, toDate, generatedAt, summary, period, daily);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean inRange(Instant timestamp, Instant startInclusive, Instant endInclusive) {
|
||||||
|
return timestamp != null
|
||||||
|
&& !timestamp.isBefore(startInclusive)
|
||||||
|
&& !timestamp.isAfter(endInclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean inRangeExclusiveEnd(Instant timestamp, Instant startInclusive, Instant endExclusive) {
|
||||||
|
return timestamp != null
|
||||||
|
&& !timestamp.isBefore(startInclusive)
|
||||||
|
&& timestamp.isBefore(endExclusive);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package pl.polskalokalnie.admin;
|
||||||
|
|
||||||
|
public record ResetPasswordResponse(
|
||||||
|
String temporaryPassword
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ public record ListingCreateRequest(
|
|||||||
Double lng,
|
Double lng,
|
||||||
List<String> media,
|
List<String> media,
|
||||||
List<String> amenities,
|
List<String> amenities,
|
||||||
List<String> photos
|
List<String> photos,
|
||||||
|
String virtualTourUrl
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public record ListingDetailResponse(
|
|||||||
List<String> media,
|
List<String> media,
|
||||||
List<String> amenities,
|
List<String> amenities,
|
||||||
List<String> photos,
|
List<String> photos,
|
||||||
|
String virtualTourUrl,
|
||||||
String ownerEmail,
|
String ownerEmail,
|
||||||
ListingStatus status,
|
ListingStatus status,
|
||||||
Instant createdAt,
|
Instant createdAt,
|
||||||
@@ -86,6 +87,7 @@ public record ListingDetailResponse(
|
|||||||
List.copyOf(listing.getMedia()),
|
List.copyOf(listing.getMedia()),
|
||||||
List.copyOf(listing.getAmenities()),
|
List.copyOf(listing.getAmenities()),
|
||||||
List.copyOf(listing.getPhotos()),
|
List.copyOf(listing.getPhotos()),
|
||||||
|
listing.getVirtualTourUrl(),
|
||||||
listing.getOwnerEmail(),
|
listing.getOwnerEmail(),
|
||||||
listing.getStatus(),
|
listing.getStatus(),
|
||||||
listing.getCreatedAt(),
|
listing.getCreatedAt(),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package pl.polskalokalnie.listing;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -100,6 +102,7 @@ public class ListingService {
|
|||||||
List<String> photos = limitPhotos(request.photos());
|
List<String> photos = limitPhotos(request.photos());
|
||||||
listing.setPhotos(photos);
|
listing.setPhotos(photos);
|
||||||
listing.setCoverPhoto(photos.isEmpty() ? null : photos.get(0));
|
listing.setCoverPhoto(photos.isEmpty() ? null : photos.get(0));
|
||||||
|
listing.setVirtualTourUrl(normalizeVirtualTourUrl(request.virtualTourUrl()));
|
||||||
|
|
||||||
listing.setOwnerEmail(ownerEmail);
|
listing.setOwnerEmail(ownerEmail);
|
||||||
listing.setStatus(ListingStatus.PENDING);
|
listing.setStatus(ListingStatus.PENDING);
|
||||||
@@ -165,4 +168,29 @@ public class ListingService {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String normalizeVirtualTourUrl(String value) {
|
||||||
|
String normalized = trimToNull(value);
|
||||||
|
if (normalized == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (normalized.length() > 1200) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Link do wirtualnego spaceru jest zbyt długi");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URI uri = new URI(normalized);
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
String host = uri.getHost();
|
||||||
|
if (scheme == null || host == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Podaj poprawny link URL do wirtualnego spaceru");
|
||||||
|
}
|
||||||
|
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Link do wirtualnego spaceru musi zaczynać się od http:// lub https://");
|
||||||
|
}
|
||||||
|
return uri.toString();
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Podaj poprawny link URL do wirtualnego spaceru");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ public class PropertyListing {
|
|||||||
@Column(columnDefinition = "text")
|
@Column(columnDefinition = "text")
|
||||||
private String coverPhoto;
|
private String coverPhoto;
|
||||||
|
|
||||||
|
@Column(length = 1200)
|
||||||
|
private String virtualTourUrl;
|
||||||
|
|
||||||
@Column(length = 180)
|
@Column(length = 180)
|
||||||
private String ownerEmail;
|
private String ownerEmail;
|
||||||
|
|
||||||
@@ -446,6 +449,14 @@ public class PropertyListing {
|
|||||||
this.coverPhoto = coverPhoto;
|
this.coverPhoto = coverPhoto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getVirtualTourUrl() {
|
||||||
|
return virtualTourUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVirtualTourUrl(String virtualTourUrl) {
|
||||||
|
this.virtualTourUrl = virtualTourUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public String getOwnerEmail() {
|
public String getOwnerEmail() {
|
||||||
return ownerEmail;
|
return ownerEmail;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ public interface UserRepository extends JpaRepository<AppUser, Long> {
|
|||||||
boolean existsByEmailIgnoreCase(String email);
|
boolean existsByEmailIgnoreCase(String email);
|
||||||
|
|
||||||
Optional<AppUser> findFirstByRole(Role role);
|
Optional<AppUser> findFirstByRole(Role role);
|
||||||
|
|
||||||
|
long countByRole(Role role);
|
||||||
}
|
}
|
||||||
|
|||||||
+2347
-62
File diff suppressed because it is too large
Load Diff
+1621
-1
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user