Regulamin i polityka prywatnosci zgodne ze stanem serwisu
Deploy Polska Lokalnie / deploy (push) Successful in 22s
Deploy Polska Lokalnie / deploy (push) Successful in 22s
Audyt pokazal rozbieznosci w obie strony: dokumenty opisywaly funkcje, ktorych nie ma (logowanie Google/Facebook), a kod robil rzeczy, ktorych dokumenty nie ujawnialy. Kod: - zgody marketingowe domyslnie WYLACZONE (byly true dla kazdego nowego konta) + skrypt fix-marketing-consents.sql czyszczacy stare wpisy, - usuniete tlumaczenie przez nieoficjalne API Google - przy zmianie jezyka wysylalo do USA tresci stron, w tym wiadomosci i nazwiska, - osadzone spacery (YouTube, Matterport, Kuula) laduja sie dopiero po zgodzie; YouTube w trybie bez ciasteczek; mapy jak dotad, - usuniecie konta kasuje dane osobowe (AccountErasureService), a nie tylko wiersz app_users, - wygasle tokeny i kody SMS kasowane po 7 dniach (ExpiredTokenCleaner), - zapisujemy moment akceptacji regulaminu przy rejestracji. Dokumenty: opisuja realne funkcje, wszystkich odbiorcow danych (OVH, Autopay, SMTP, SMS, Esri, OpenStreetMap, YouTube/Matterport/Kuula, GUS), transfery poza EOG, konkretne okresy przechowywania, procedure zgloszen z DSA, brak profilowania i tabele pamieci przegladarki zamiast fikcyjnych cookies analitycznych. Zakres NIS2 i DORA opisany zgodnie z prawda. Uklad stron prawnych jak reszta serwisu: kontener 1760 px, karty, spis tresci z boku. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,11 +38,13 @@ import pl.polskalokalnie.lead.AdvertisingEnquiryResponse;
|
||||
import pl.polskalokalnie.lead.AdvertisingEnquiryService;
|
||||
import pl.polskalokalnie.support.SupportMessageResponse;
|
||||
import pl.polskalokalnie.support.SupportMessageService;
|
||||
import pl.polskalokalnie.user.AccountErasureService;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.BlockedEmail;
|
||||
import pl.polskalokalnie.user.BlockedEmailRepository;
|
||||
import pl.polskalokalnie.user.Role;
|
||||
import pl.polskalokalnie.user.UserRepository;
|
||||
import pl.polskalokalnie.user.UserSettingsService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
@@ -64,6 +66,8 @@ public class AdminController {
|
||||
private final AdminStatsService adminStatsService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final NotificationService notificationService;
|
||||
private final AccountErasureService accountErasureService;
|
||||
private final UserSettingsService userSettingsService;
|
||||
|
||||
public AdminController(UserRepository userRepository, BlockedEmailRepository blockedEmailRepository,
|
||||
ListingService listingService, ListingReportService listingReportService,
|
||||
@@ -74,7 +78,9 @@ public class AdminController {
|
||||
TextModerationService textModerationService,
|
||||
AdminStatsService adminStatsService,
|
||||
PasswordEncoder passwordEncoder,
|
||||
NotificationService notificationService) {
|
||||
NotificationService notificationService,
|
||||
AccountErasureService accountErasureService,
|
||||
UserSettingsService userSettingsService) {
|
||||
this.userRepository = userRepository;
|
||||
this.blockedEmailRepository = blockedEmailRepository;
|
||||
this.listingService = listingService;
|
||||
@@ -87,6 +93,8 @@ public class AdminController {
|
||||
this.adminStatsService = adminStatsService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.notificationService = notificationService;
|
||||
this.accountErasureService = accountErasureService;
|
||||
this.userSettingsService = userSettingsService;
|
||||
}
|
||||
|
||||
// --- Uzytkownicy ---
|
||||
|
||||
@@ -22,6 +22,7 @@ import pl.polskalokalnie.auth.dto.RegisterRequest;
|
||||
import pl.polskalokalnie.auth.dto.RegisterResponse;
|
||||
import pl.polskalokalnie.auth.dto.UpdateProfileRequest;
|
||||
import pl.polskalokalnie.auth.dto.UserResponse;
|
||||
import pl.polskalokalnie.user.AccountErasureService;
|
||||
import pl.polskalokalnie.user.AccountType;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.AuthProvider;
|
||||
@@ -53,6 +54,7 @@ public class AuthService {
|
||||
private final PhoneVerificationService phoneVerificationService;
|
||||
private final NickService nickService;
|
||||
private final UserSettingsService userSettingsService;
|
||||
private final AccountErasureService accountErasureService;
|
||||
private final ListingService listingService;
|
||||
|
||||
public AuthService(
|
||||
@@ -67,6 +69,7 @@ public class AuthService {
|
||||
PhoneVerificationService phoneVerificationService,
|
||||
NickService nickService,
|
||||
UserSettingsService userSettingsService,
|
||||
AccountErasureService accountErasureService,
|
||||
ListingService listingService
|
||||
) {
|
||||
this.userRepository = userRepository;
|
||||
@@ -80,6 +83,7 @@ public class AuthService {
|
||||
this.phoneVerificationService = phoneVerificationService;
|
||||
this.nickService = nickService;
|
||||
this.userSettingsService = userSettingsService;
|
||||
this.accountErasureService = accountErasureService;
|
||||
this.listingService = listingService;
|
||||
}
|
||||
|
||||
@@ -109,6 +113,10 @@ public class AuthService {
|
||||
user.setPhone(phone);
|
||||
user.setNip(request.nip() != null && !request.nip().isBlank() ? request.nip().trim() : null);
|
||||
user.setAddress(request.address() != null && !request.address().isBlank() ? request.address().trim() : null);
|
||||
// Rejestracja jest mozliwa tylko po zaznaczeniu zgody w formularzu - zapisujemy jej moment.
|
||||
if (Boolean.TRUE.equals(request.acceptTerms())) {
|
||||
user.setTermsAcceptedAt(Instant.now());
|
||||
}
|
||||
// Konto wymaga aktywacji przez uzytkownika (link e-mail). Do tego czasu logowanie jest zablokowane.
|
||||
user.setVerified(false);
|
||||
user.setPhoneVerified(false);
|
||||
@@ -442,6 +450,7 @@ public class AuthService {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Hasło jest nieprawidłowe");
|
||||
}
|
||||
|
||||
accountErasureService.eraseFor(user.getId(), user.getEmail());
|
||||
userSettingsService.deleteForUser(user.getId());
|
||||
listingService.deleteAllOfOwner(user.getEmail());
|
||||
userRepository.delete(user);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package pl.polskalokalnie.auth;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Sprzatanie jednorazowych tokenow i kodow SMS. Wczesniej zostawaly w bazie bezterminowo,
|
||||
* mimo ze po wygasnieciu sa bezuzyteczne - a wiersz phone_otps zawiera numer telefonu
|
||||
* i kod w jawnej postaci. Zasada ograniczenia przechowywania (art. 5 ust. 1 lit. e RODO)
|
||||
* wymaga, zeby znikaly, gdy przestaja byc potrzebne.
|
||||
*
|
||||
* <p>Zostawiamy krotki bufor od wygasniecia: uzytkownik, ktory kliknie stary link, dostaje
|
||||
* czytelny komunikat zamiast bledu "nie znaleziono".</p>
|
||||
*/
|
||||
@Component
|
||||
public class ExpiredTokenCleaner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExpiredTokenCleaner.class);
|
||||
|
||||
/** Bufor po wygasnieciu - tyle jeszcze trzymamy zuzyty albo przeterminowany wpis. */
|
||||
private static final int GRACE_DAYS = 7;
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public ExpiredTokenCleaner(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 15 3 * * *")
|
||||
@Transactional
|
||||
public void cleanDaily() {
|
||||
Instant cutoff = Instant.now().minus(GRACE_DAYS, ChronoUnit.DAYS);
|
||||
try {
|
||||
int tokens = jdbcTemplate.update("DELETE FROM auth_tokens WHERE expires_at < ?", cutoff);
|
||||
int otps = jdbcTemplate.update("DELETE FROM phone_otps WHERE expires_at < ?", cutoff);
|
||||
if (tokens > 0 || otps > 0) {
|
||||
log.info("Sprzatanie: usunieto {} wygaslych tokenow i {} kodow SMS", tokens, otps);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("Sprzatanie wygaslych tokenow nie powiodlo sie: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ public class SecurityConfig {
|
||||
.requestMatchers("/api/auth/activate", "/api/auth/resend-activation",
|
||||
"/api/auth/forgot-password", "/api/auth/reset-password",
|
||||
"/api/auth/verify-phone", "/api/auth/resend-phone-otp").permitAll()
|
||||
.requestMatchers("/api/i18n/translate").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/fx/rates").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/advertising-enquiries").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/gus/company").permitAll()
|
||||
|
||||
@@ -12,6 +12,8 @@ public record RegisterRequest(
|
||||
AccountType accountType,
|
||||
String phone,
|
||||
String nip,
|
||||
String address
|
||||
String address,
|
||||
/** Potwierdzenie akceptacji Regulaminu i Polityki prywatnosci - zapisujemy moment zgody. */
|
||||
Boolean acceptTerms
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
package pl.polskalokalnie.i18n;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import pl.polskalokalnie.user.PreferredLanguage;
|
||||
|
||||
@Service
|
||||
public class RuntimeTranslationService {
|
||||
|
||||
private static final int MAX_TEXTS_PER_REQUEST = 180;
|
||||
private static final int MAX_TEXT_LENGTH = 400;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(6))
|
||||
.build();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public Map<String, String> translateBatch(PreferredLanguage language, List<String> texts) {
|
||||
if (texts == null || texts.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Brak tekstów do tłumaczenia");
|
||||
}
|
||||
if (texts.size() > MAX_TEXTS_PER_REQUEST) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Zbyt wiele tekstów do tłumaczenia w jednym żądaniu");
|
||||
}
|
||||
|
||||
if (language == PreferredLanguage.PL) {
|
||||
Map<String, String> passthrough = new LinkedHashMap<>();
|
||||
texts.forEach((text) -> passthrough.put(text, text));
|
||||
return passthrough;
|
||||
}
|
||||
|
||||
String targetCode = toGoogleLang(language);
|
||||
Map<String, String> results = new LinkedHashMap<>();
|
||||
List<String> toTranslate = new ArrayList<>();
|
||||
|
||||
for (String text : texts) {
|
||||
String safeText = text != null ? text : "";
|
||||
if (safeText.length() > MAX_TEXT_LENGTH) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Jeden z tekstów jest zbyt długi do tłumaczenia");
|
||||
}
|
||||
|
||||
String cacheKey = targetCode + "|" + safeText;
|
||||
String cached = cache.get(cacheKey);
|
||||
if (cached != null) {
|
||||
results.put(safeText, cached);
|
||||
} else {
|
||||
toTranslate.add(safeText);
|
||||
}
|
||||
}
|
||||
|
||||
for (String text : toTranslate) {
|
||||
String translated = translateSingle(text, targetCode);
|
||||
cache.put(targetCode + "|" + text, translated);
|
||||
results.put(text, translated);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private String translateSingle(String source, String targetCode) {
|
||||
if (source.isBlank()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
String encoded = URLEncoder.encode(source, StandardCharsets.UTF_8);
|
||||
String url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=pl&tl="
|
||||
+ targetCode + "&dt=t&q=" + encoded;
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.GET()
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "PolskaLokalnie/1.0")
|
||||
.build();
|
||||
|
||||
try {
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Usługa tłumaczeń jest chwilowo niedostępna");
|
||||
}
|
||||
return parseTranslatedText(response.body(), source);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Nie udało się pobrać tłumaczenia", ex);
|
||||
} catch (IOException ex) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Nie udało się pobrać tłumaczenia", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String parseTranslatedText(String payload, String fallback) throws IOException {
|
||||
JsonNode root = objectMapper.readTree(payload);
|
||||
JsonNode topSegments = root.path(0);
|
||||
if (!topSegments.isArray() || topSegments.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
StringBuilder translated = new StringBuilder();
|
||||
for (JsonNode segment : topSegments) {
|
||||
JsonNode part = segment.path(0);
|
||||
if (part.isTextual()) {
|
||||
translated.append(part.asText());
|
||||
}
|
||||
}
|
||||
String value = translated.toString();
|
||||
return value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private String toGoogleLang(PreferredLanguage language) {
|
||||
return switch (language) {
|
||||
case EN -> "en";
|
||||
case UK -> "uk";
|
||||
case DE -> "de";
|
||||
case PL -> "pl";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package pl.polskalokalnie.i18n;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import pl.polskalokalnie.user.PreferredLanguage;
|
||||
|
||||
public record TranslateRequest(
|
||||
@NotNull PreferredLanguage language,
|
||||
@NotEmpty List<String> texts
|
||||
) {
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package pl.polskalokalnie.i18n;
|
||||
|
||||
import java.util.Map;
|
||||
import pl.polskalokalnie.user.PreferredLanguage;
|
||||
|
||||
public record TranslateResponse(
|
||||
PreferredLanguage language,
|
||||
Map<String, String> translations
|
||||
) {
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package pl.polskalokalnie.i18n;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/i18n")
|
||||
public class TranslationController {
|
||||
|
||||
private final RuntimeTranslationService runtimeTranslationService;
|
||||
|
||||
public TranslationController(RuntimeTranslationService runtimeTranslationService) {
|
||||
this.runtimeTranslationService = runtimeTranslationService;
|
||||
}
|
||||
|
||||
@PostMapping("/translate")
|
||||
public TranslateResponse translate(@Valid @RequestBody TranslateRequest request) {
|
||||
List<String> normalizedTexts = request.texts().stream()
|
||||
.map(text -> text == null ? "" : text)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<String, String> translations = runtimeTranslationService.translateBatch(request.language(), normalizedTexts);
|
||||
return new TranslateResponse(request.language(), translations);
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,17 @@ public class Lead {
|
||||
@Column(name = "tag", length = 60)
|
||||
private List<String> tags = new ArrayList<>();
|
||||
|
||||
// Zgoda marketingowa musi byc wyrazna i uprzednia (art. 4 pkt 11 i art. 7 RODO oraz Prawo
|
||||
// komunikacji elektronicznej), wiec domyslnie jej NIE MA. Wlacza ja sam uzytkownik
|
||||
// w ustawieniach konta (MarketingConsentController) albo administrator przy imporcie leada,
|
||||
// majac podstawe zebrana poza serwisem.
|
||||
@Column(nullable = false)
|
||||
@ColumnDefault("true")
|
||||
private boolean emailConsent = true;
|
||||
@ColumnDefault("false")
|
||||
private boolean emailConsent = false;
|
||||
|
||||
@Column(nullable = false)
|
||||
@ColumnDefault("true")
|
||||
private boolean smsConsent = true;
|
||||
@ColumnDefault("false")
|
||||
private boolean smsConsent = false;
|
||||
|
||||
// Trwala rezygnacja - raz ustawiona nie jest cofana przez kampanie ani synchronizacje.
|
||||
@Column(nullable = false)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package pl.polskalokalnie.user;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Usuniecie danych osobowych konta (art. 17 RODO). Dotad kasowany byl tylko wiersz app_users,
|
||||
* ustawienia i ogloszenia - reszta zostawala w bazie jako sieroty: wiadomosci, powiadomienia,
|
||||
* alerty, spotkania, tokeny, kody SMS z numerem telefonu i lead marketingowy.
|
||||
*
|
||||
* <p>Zostaja wylacznie dane, ktorych usuniecie byloby sprzeczne z innym obowiazkiem:
|
||||
* zamowienia promowania (dokumentacja rozliczeniowa), zgloszenia ogloszen i uzytkownikow
|
||||
* oraz korespondencja z obsluga (obrona przed roszczeniami i obowiazki moderacyjne).
|
||||
* Te kategorie maja wlasne okresy przechowywania opisane w Polityce prywatnosci.</p>
|
||||
*
|
||||
* <p>Schemat nie ma kluczy obcych z kaskada (ddl-auto=update), wiec kasujemy jawnie,
|
||||
* tabela po tabeli - dzieki temu widac wprost, co znika.</p>
|
||||
*/
|
||||
@Service
|
||||
public class AccountErasureService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AccountErasureService.class);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AccountErasureService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void eraseFor(Long userId, String email) {
|
||||
if (userId == null || email == null || email.isBlank()) {
|
||||
return;
|
||||
}
|
||||
int removed = 0;
|
||||
|
||||
// Dane powiazane z identyfikatorem konta.
|
||||
removed += update("DELETE FROM messages WHERE sender_id = ? OR recipient_id = ?", userId, userId);
|
||||
removed += update("DELETE FROM conversations WHERE buyer_id = ? OR seller_id = ?", userId, userId);
|
||||
removed += update("DELETE FROM user_blocks WHERE blocker_id = ? OR blocked_id = ?", userId, userId);
|
||||
removed += update("DELETE FROM meetings WHERE proposer_id = ? OR counterpart_id = ?", userId, userId);
|
||||
|
||||
// Dane powiazane z adresem e-mail.
|
||||
removed += update("DELETE FROM listing_favorites WHERE lower(user_email) = lower(?)", email);
|
||||
removed += update("DELETE FROM saved_searches WHERE lower(user_email) = lower(?)", email);
|
||||
removed += update("DELETE FROM price_alert_events WHERE lower(user_email) = lower(?)", email);
|
||||
removed += update("DELETE FROM price_alerts WHERE lower(user_email) = lower(?)", email);
|
||||
removed += update("DELETE FROM notifications WHERE lower(user_email) = lower(?)", email);
|
||||
removed += update("DELETE FROM auth_tokens WHERE lower(user_email) = lower(?)", email);
|
||||
removed += update("DELETE FROM phone_otps WHERE lower(user_email) = lower(?)", email);
|
||||
|
||||
// Baza marketingowa: lead, jego tagi i pozycje w kolejkach kampanii.
|
||||
removed += update("DELETE FROM campaign_recipients WHERE lead_id IN (SELECT id FROM leads WHERE lower(email) = lower(?))", email);
|
||||
removed += update("DELETE FROM lead_tags WHERE lead_id IN (SELECT id FROM leads WHERE lower(email) = lower(?))", email);
|
||||
removed += update("DELETE FROM leads WHERE lower(email) = lower(?)", email);
|
||||
|
||||
// Poradniki uzytkownika znikaja razem z kontem - tak samo jak jego ogloszenia.
|
||||
removed += update("DELETE FROM article_attachments WHERE article_id IN (SELECT id FROM articles WHERE lower(author_email) = lower(?))", email);
|
||||
removed += update("DELETE FROM articles WHERE lower(author_email) = lower(?)", email);
|
||||
|
||||
log.info("Usunieto {} rekordow powiazanych z kontem id={}", removed, userId);
|
||||
}
|
||||
|
||||
private int update(String sql, Object... args) {
|
||||
try {
|
||||
return jdbcTemplate.update(sql, args);
|
||||
} catch (RuntimeException ex) {
|
||||
// Brak tabeli w starszej bazie nie moze zablokowac usuniecia konta.
|
||||
log.warn("Nie udalo sie wykonac czyszczenia: {} ({})", sql, ex.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,13 @@ public class AppUser {
|
||||
// Na karcie sprzedajacego pokazujemy z tego "ostatnio aktywny/a"; null = jeszcze nie odnotowano.
|
||||
private Instant lastActiveAt;
|
||||
|
||||
/**
|
||||
* Moment akceptacji Regulaminu i Polityki prywatnosci przy rejestracji. Zapisujemy go,
|
||||
* zeby mozna bylo wykazac, kiedy uzytkownik zaakceptowal dokumenty (art. 7 ust. 1 RODO).
|
||||
*/
|
||||
@Column(name = "terms_accepted_at")
|
||||
private Instant termsAcceptedAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
@@ -274,6 +281,14 @@ public class AppUser {
|
||||
this.sessionsValidFrom = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS);
|
||||
}
|
||||
|
||||
public Instant getTermsAcceptedAt() {
|
||||
return termsAcceptedAt;
|
||||
}
|
||||
|
||||
public void setTermsAcceptedAt(Instant termsAcceptedAt) {
|
||||
this.termsAcceptedAt = termsAcceptedAt;
|
||||
}
|
||||
|
||||
public Instant getLastActiveAt() {
|
||||
return lastActiveAt;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user