KONTAKTY
Usuwanie istnialo w API, ale nie mialo przycisku na liscie. Doszedl, wraz
z endpointem `GET /admin/contacts/{id}/deletion-impact`: panel pyta
o potwierdzenie DOPIERO Z LICZBAMI, tak samo jak przy usuwaniu webinaru.
Jest to konieczne, bo usuniecie kontaktu kasuje KASKADOWO zapisy na kursy
i zgody. Przy zwyklym wpisie marketingowym odpowiedz jest zupelnie inna niz
przy kursancie z oplaconym kursem, ktoremu usuniecie odbiera dostep do
materialow. Pytanie mowi to wprost i dodaje, ze zamowienia zostaja: sa
dokumentem finansowym i maja `ON DELETE SET NULL`.
Gdy pobranie skutkow sie nie uda, potwierdzenie NIE JEST pokazywane wcale.
Pytanie o zgode na cos, czego skutkow nie udalo sie ustalic, byloby gorsze
niz brak pytania.
UZYTKOWNICY
Byla sama dezaktywacja. Doszly:
- odblokowanie (`POST /{id}/activate`), bo dezaktywacja jest odwracalna
z zalozenia: konto zablokowane na czas urlopu ma dac sie wlaczyc bez
zakladania nowego i bez tracenia sladu w dzienniku,
- usuniecie trwale (`DELETE /{id}`) dla kont zalozonych przez pomylke.
APLIKACJA NIE MOZE ZOSTAC BEZ ADMINISTRATORA
Usuwanie ma te same dwie bariery co dezaktywacja i uzywa TEJ SAMEJ metody
`assertNotLastActiveAdmin`, a nie drugiej kopii reguly: nie mozna usunac
wlasnego konta ani ostatniego aktywnego administratora. Jedno i drugie
konczyloby sie utrata dostepu do panelu bez mozliwosci odzyskania go
z poziomu aplikacji.
PRACA UZYTKOWNIKA NIE PRZEPADA
Wszystkie trzy odwolania do `users` maja `ON DELETE SET NULL`: pliki
w bibliotece, ogloszone zadania i odpowiedzi na pytania zostaja, tracac
jedynie informacje o autorze. Kasowanie kaskadowe zabieraloby tresc szkolen
razem z kontem, ktore ja wprowadzilo.
DEZAKTYWACJA NIE JEST OPERACJA NISZCZACA
Jej potwierdzenie celowo NIE ma czerwonego ostrzezenia: tresc mowi wprost, ze
konto da sie wlaczyc z powrotem, a od teraz jednym klknieciem obok. Czerwien
przeczylaby wlasnemu komunikatowi i rozmydlala sygnal tam, gdzie strata jest
prawdziwa, czyli przy usuwaniu konta i kontaktu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
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;
|
||||
@@ -60,6 +61,26 @@ public class UserAdminController {
|
||||
return userService.deactivate(id, principal);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/activate")
|
||||
@Operation(summary = "Przywrocenie dostepu zablokowanemu kontu")
|
||||
public UserView activate(@PathVariable Long id) {
|
||||
return userService.activate(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trwale usuniecie konta.
|
||||
*
|
||||
* Osobne od dezaktywacji, bo to inna decyzja: dezaktywacja odbiera dostep
|
||||
* i zostawia slad, usuniecie zabiera konto z listy. Praca uzytkownika
|
||||
* zostaje w obu przypadkach.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Trwale usuniecie konta")
|
||||
public void delete(@PathVariable Long id,
|
||||
@AuthenticationPrincipal AuthenticatedUser principal) {
|
||||
userService.delete(id, principal);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reset-password")
|
||||
@Operation(summary = "Reset hasla. Nowe haslo zwracane jeden raz.")
|
||||
public ResetPasswordResponse resetPassword(@PathVariable Long id) {
|
||||
|
||||
@@ -116,6 +116,59 @@ public class UserService {
|
||||
return AuthService.toView(userRepository.save(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Przywrocenie dostepu wczesniej zablokowanemu kontu.
|
||||
*
|
||||
* Dezaktywacja jest odwracalna wlasnie po to: konto zablokowane na czas
|
||||
* urlopu albo do wyjasnienia ma dac sie wlaczyc z powrotem, bez zakladania
|
||||
* nowego i bez tracenia sladu w dzienniku.
|
||||
*/
|
||||
@Transactional
|
||||
public UserView activate(Long id) {
|
||||
User user = require(id);
|
||||
user.setActive(true);
|
||||
log.info("Aktywowano konto {}", user.getEmail());
|
||||
return AuthService.toView(userRepository.save(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trwale usuniecie konta.
|
||||
*
|
||||
* KIEDY ZAMIAST DEZAKTYWACJI
|
||||
* -----------------------------------------------------------------------
|
||||
* Dezaktywacja odbiera dostep, ale zostawia konto na liscie i w dzienniku.
|
||||
* Usuniecie ma sens przy koncie zalozonym przez pomylke albo dla osoby,
|
||||
* ktora nigdy nie powinna go dostac. Przy zwyklym odejsciu pracownika
|
||||
* lepsza jest dezaktywacja: zostaje wtedy widoczne, kto co zrobil.
|
||||
*
|
||||
* CO SIE DZIEJE Z JEGO PRACA
|
||||
* -----------------------------------------------------------------------
|
||||
* Nic nie przepada. Pliki w bibliotece i ogloszone zadania maja
|
||||
* `ON DELETE SET NULL` na kolumnie autora, wiec zostaja na miejscu,
|
||||
* tracac jedynie informacje o tym, kto je dodal. Kasowanie kaskadowe
|
||||
* zabieraloby tresc szkolen razem z kontem, ktore ja wprowadzilo.
|
||||
*
|
||||
* TE SAME DWIE BARIERY CO PRZY DEZAKTYWACJI
|
||||
* -----------------------------------------------------------------------
|
||||
* Nie mozna usunac wlasnego konta ani ostatniego aktywnego administratora:
|
||||
* jedno i drugie konczyloby sie utrata dostepu do panelu bez mozliwosci
|
||||
* odzyskania go z poziomu aplikacji.
|
||||
*/
|
||||
@Transactional
|
||||
public void delete(Long id, AuthenticatedUser actor) {
|
||||
User user = require(id);
|
||||
|
||||
if (user.getId().equals(actor.id())) {
|
||||
throw new ValidationException("Nie można usunąć własnego konta");
|
||||
}
|
||||
if (user.getRole() == UserRole.ADMIN && user.isActive()) {
|
||||
assertNotLastActiveAdmin(user, "To ostatnie aktywne konto administratora, nie można go usunąć");
|
||||
}
|
||||
|
||||
userRepository.delete(user);
|
||||
log.info("Usunieto konto {}", user.getEmail());
|
||||
}
|
||||
|
||||
/** Reset hasla przez administratora. Nowe haslo pokazywane jeden raz. */
|
||||
@Transactional
|
||||
public ResetPasswordResponse resetPassword(Long id) {
|
||||
|
||||
@@ -86,6 +86,19 @@ public class ContactAdminController {
|
||||
return contacts.update(id, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Co przepadnie razem z kontaktem.
|
||||
*
|
||||
* Panel pobiera to PRZED pytaniem o potwierdzenie, tak samo jak przy
|
||||
* usuwaniu webinaru: liczby zamieniaja "czy na pewno?" w decyzje opartą
|
||||
* na tym, co faktycznie zniknie.
|
||||
*/
|
||||
@GetMapping("/{id}/deletion-impact")
|
||||
@Operation(summary = "Co przepadnie razem z kontaktem")
|
||||
public ContactService.DeletionImpact deletionImpact(@PathVariable Long id) {
|
||||
return contacts.deletionImpact(id);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Usuniecie kontaktu")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
|
||||
@@ -56,17 +56,20 @@ public class ContactService {
|
||||
private final ContactGroupMemberRepository memberRepository;
|
||||
private final ConsentRepository consentRepository;
|
||||
private final SettingsService settings;
|
||||
private final com.softspm.webinar.repository.EnrollmentRepository enrollmentRepository;
|
||||
|
||||
public ContactService(ContactRepository contactRepository,
|
||||
ContactGroupRepository groupRepository,
|
||||
ContactGroupMemberRepository memberRepository,
|
||||
ConsentRepository consentRepository,
|
||||
SettingsService settings) {
|
||||
SettingsService settings,
|
||||
com.softspm.webinar.repository.EnrollmentRepository enrollmentRepository) {
|
||||
this.contactRepository = contactRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.memberRepository = memberRepository;
|
||||
this.consentRepository = consentRepository;
|
||||
this.settings = settings;
|
||||
this.enrollmentRepository = enrollmentRepository;
|
||||
}
|
||||
|
||||
// --- odczyt ---
|
||||
@@ -171,6 +174,32 @@ public class ContactService {
|
||||
return get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Co przepadnie razem z kontaktem.
|
||||
*
|
||||
* Panel pyta o potwierdzenie DOPIERO Z TYMI LICZBAMI, tak samo jak przy
|
||||
* usuwaniu webinaru. "Czy na pewno?" bez nich nie daje administratorowi
|
||||
* niczego, na czym moglby oprzec decyzje: przy zwyklym wpisie
|
||||
* marketingowym odpowiedz jest zupelnie inna niz przy kursancie
|
||||
* z oplaconym kursem.
|
||||
*
|
||||
* `student` rozstrzyga, czy kontakt ma konto w Akademii. Usuniecie takiego
|
||||
* kontaktu **odbiera dostep do kursow**, bo zapisy wisza na nim kaskadowo.
|
||||
* Zamowienia zostaja: sa dokumentem finansowym i maja `ON DELETE SET NULL`.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public DeletionImpact deletionImpact(Long id) {
|
||||
Contact contact = require(id);
|
||||
return new DeletionImpact(
|
||||
enrollmentRepository.countByContactId(id),
|
||||
consentRepository.countByContactId(id),
|
||||
contact.hasAccount());
|
||||
}
|
||||
|
||||
/** @param student czy kontakt ma konto kursanta, czyli dostep do Akademii */
|
||||
public record DeletionImpact(long enrollments, long consents, boolean student) {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
Contact contact = require(id);
|
||||
|
||||
@@ -12,6 +12,14 @@ public interface ConsentRepository extends JpaRepository<Consent, Long> {
|
||||
|
||||
List<Consent> findByRegistrationId(Long registrationId);
|
||||
|
||||
/**
|
||||
* Ile zgod ma kontakt.
|
||||
*
|
||||
* Uzywane przy ostrzezeniu przed usunieciem: zgody sa sladem rozliczalnym,
|
||||
* czyli dowodem na to, na jakiej podstawie wolno bylo cokolwiek wyslac.
|
||||
*/
|
||||
long countByContactId(Long contactId);
|
||||
|
||||
/** Zgody dla wielu zapisow naraz, do listy i eksportu. */
|
||||
@Query("select c from Consent c where c.registration.id in :registrationIds")
|
||||
List<Consent> findByRegistrationIdIn(@Param("registrationIds") Collection<Long> registrationIds);
|
||||
|
||||
@@ -43,6 +43,9 @@ public interface EnrollmentRepository extends JpaRepository<Enrollment, Long> {
|
||||
/** Wszystkie zapisy kursu, bez wzgledu na stan. Do podsumowania skutkow skasowania. */
|
||||
long countByCourseId(Long courseId);
|
||||
|
||||
/** Ile zapisow na kursy ma kontakt. Uzywane przy ostrzezeniu przed usunieciem. */
|
||||
long countByContactId(Long contactId);
|
||||
|
||||
/** Zapisani wraz z kontaktem, do listy w panelu i eksportu CSV. */
|
||||
@Query("""
|
||||
select e from Enrollment e
|
||||
|
||||
Reference in New Issue
Block a user