Skip to content

Commit 7300ea1

Browse files
authored
Merge branch 'develop' into #2087-löschung-nach-deadline-backend
2 parents 23c7832 + 0b60402 commit 7300ea1

60 files changed

Lines changed: 3326 additions & 391 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bogenliga/bogenliga-application/src/main/java/de/bogenliga/application/services/v1/download/DownloadService.java

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import de.bogenliga.application.business.wettkampf.api.types.WettkampfDO;
99
import de.bogenliga.application.business.wettkampf.api.WettkampfComponent;
1010
import de.bogenliga.application.springconfiguration.security.permissions.RequiresPermission;
11+
import de.bogenliga.application.springconfiguration.security.permissions.RequiresOnePermissions;
1112
import de.bogenliga.application.springconfiguration.security.types.UserPermission;
1213
import org.slf4j.Logger;
1314
import org.slf4j.LoggerFactory;
@@ -18,16 +19,19 @@
1819
import org.springframework.http.ResponseEntity;
1920
import org.springframework.web.bind.annotation.*;
2021
import de.bogenliga.application.business.bogenkontrollliste.api.BogenkontrolllisteComponent;
22+
import de.bogenliga.application.business.dsbmannschaft.api.DsbMannschaftComponent;
2123
import de.bogenliga.application.business.meldezettel.api.MeldezettelComponent;
2224
import de.bogenliga.application.business.schusszettel.api.SchusszettelComponent;
2325
import de.bogenliga.application.business.setzliste.api.SetzlisteComponent;
2426
import de.bogenliga.application.business.lizenz.api.LizenzComponent;
2527
import de.bogenliga.application.business.rueckennummern.api.RueckennummernComponent;
2628
import de.bogenliga.application.common.errorhandling.ErrorCode;
29+
import de.bogenliga.application.common.errorhandling.exception.BusinessException;
2730
import de.bogenliga.application.common.errorhandling.exception.TechnicalException;
2831
import de.bogenliga.application.common.service.ServiceFacade;
2932
import de.bogenliga.application.common.validation.Preconditions;
3033
import de.bogenliga.application.services.v1.setzliste.service.SetzlisteService;
34+
import de.bogenliga.application.springconfiguration.security.permissions.RequiresOnePermissionAspect;
3135

3236

3337
/**
@@ -65,6 +69,8 @@ public class DownloadService implements ServiceFacade {
6569
private final BogenkontrolllisteComponent bogenkontrolllisteComponent;
6670
private final RueckennummernComponent rueckennummernComponent;
6771
private final WettkampfComponent wettkampfComponent;
72+
private final DsbMannschaftComponent dsbMannschaftComponent;
73+
private final RequiresOnePermissionAspect requiresOnePermissionAspect;
6874

6975

7076
/**
@@ -76,16 +82,20 @@ public DownloadService(final SetzlisteComponent setzlisteComponent, final Lizenz
7682
final MeldezettelComponent meldezettelComponent,
7783
final BogenkontrolllisteComponent bogenkontrolllisteComponent,
7884
final RueckennummernComponent rueckennummernComponent,
79-
final WettkampfComponent wettkampfComponent) {
85+
final WettkampfComponent wettkampfComponent,
86+
final DsbMannschaftComponent dsbMannschaftComponent,
87+
final RequiresOnePermissionAspect requiresOnePermissionAspect) {
8088
this.lizenzComponent = lizenzComponent;
8189
this.setzlisteComponent = setzlisteComponent;
8290
this.schusszettelComponent = schusszettelComponent;
8391
this.meldezettelComponent = meldezettelComponent;
8492
this.bogenkontrolllisteComponent = bogenkontrolllisteComponent;
8593
this.rueckennummernComponent = rueckennummernComponent;
8694
this.wettkampfComponent = wettkampfComponent;
95+
this.dsbMannschaftComponent = dsbMannschaftComponent;
96+
this.requiresOnePermissionAspect = requiresOnePermissionAspect;
8797
}
88-
98+
8999
/**
90100
* returns the Setzliste as pdf file for client download
91101
* <p>
@@ -231,8 +241,8 @@ ResponseEntity<InputStreamResource> downloadbogenkontrolllistePdf(@RequestParam(
231241
*/
232242
@CrossOrigin(maxAge = 0)
233243
@GetMapping(
234-
path = "pdf/rueckennummern",
235-
produces = MediaType.APPLICATION_PDF_VALUE)
244+
path = "pdf/rueckennummern",
245+
produces = MediaType.APPLICATION_PDF_VALUE)
236246
@RequiresPermission(UserPermission.CAN_READ_DEFAULT)
237247
public
238248
ResponseEntity<InputStreamResource> downloadRueckennummernPdf(@RequestParam("mannschaftid") final long mannschaftid) {
@@ -291,11 +301,32 @@ private ResponseEntity<InputStreamResource> generateInputStream(byte[] fileBloB)
291301
@GetMapping(
292302
path = "pdf/schuetzenlizenz/{dsbMitgliedId}/{teamId}",
293303
produces = MediaType.APPLICATION_PDF_VALUE)
294-
@RequiresPermission(UserPermission.CAN_READ_WETTKAMPF)
304+
@RequiresOnePermissions(perm = {UserPermission.CAN_MODIFY_MY_VEREIN, UserPermission.CAN_MODIFY_MY_VERANSTALTUNG})
295305
public ResponseEntity<InputStreamResource> downloadLizenz(@PathVariable("dsbMitgliedId") final long dsbMitgliedID,
296-
@PathVariable("teamId") final long teamID) {
306+
@PathVariable("teamId") final long teamID) {
297307
LOG.debug("dsbMitgliedID: {}", dsbMitgliedID);
298308
LOG.debug("teamID: {}", teamID);
309+
310+
// Data-specific authorization check
311+
var mannschaft = dsbMannschaftComponent.findById(teamID);
312+
if (mannschaft == null) {
313+
throw new BusinessException(ErrorCode.NO_PERMISSION_ERROR,
314+
"Team not found");
315+
}
316+
317+
// Check if user has permission to modify their own club (Sportleiter)
318+
boolean isSportleiter = requiresOnePermissionAspect.hasSpecificPermissionSportleiter(
319+
UserPermission.CAN_MODIFY_MY_VEREIN, mannschaft.getVereinId());
320+
321+
// Check if user has permission to modify event (Ligaleiter)
322+
boolean isLigaleiter = requiresOnePermissionAspect.hasSpecificPermissionLigaLeiterID(
323+
UserPermission.CAN_MODIFY_MY_VERANSTALTUNG, mannschaft.getVeranstaltungId());
324+
325+
if (!isSportleiter && !isLigaleiter) {
326+
throw new BusinessException(ErrorCode.NO_PERMISSION_ERROR,
327+
String.format("User does not have permission to download license for team %d", teamID));
328+
}
329+
299330
final byte[] fileBloB = lizenzComponent.getLizenzPDFasByteArray(dsbMitgliedID, teamID);
300331

301332
return generateInputStream(fileBloB);
@@ -314,10 +345,29 @@ public ResponseEntity<InputStreamResource> downloadLizenz(@PathVariable("dsbMitg
314345
@GetMapping(
315346
path = "pdf/lizenzen",
316347
produces = MediaType.APPLICATION_PDF_VALUE)
317-
@RequiresPermission(UserPermission.CAN_READ_DEFAULT)
348+
@RequiresOnePermissions(perm = {UserPermission.CAN_MODIFY_MY_VEREIN, UserPermission.CAN_MODIFY_MY_VERANSTALTUNG})
318349
public
319350
ResponseEntity<InputStreamResource> downloadLizenzenPdf(@RequestParam("mannschaftid") final long mannschaftid) {
320351

352+
// Data-specific authorization check
353+
var mannschaft = dsbMannschaftComponent.findById(mannschaftid);
354+
if (mannschaft == null) {
355+
throw new BusinessException(ErrorCode.NO_PERMISSION_ERROR,
356+
"Team not found");
357+
}
358+
359+
// Check if user has permission to modify their own club (Sportleiter)
360+
boolean isSportleiter = requiresOnePermissionAspect.hasSpecificPermissionSportleiter(
361+
UserPermission.CAN_MODIFY_MY_VEREIN, mannschaft.getVereinId());
362+
363+
// Check if user has permission to modify event (Ligaleiter)
364+
boolean isLigaleiter = requiresOnePermissionAspect.hasSpecificPermissionLigaLeiterID(
365+
UserPermission.CAN_MODIFY_MY_VERANSTALTUNG, mannschaft.getVeranstaltungId());
366+
367+
if (!isSportleiter && !isLigaleiter) {
368+
throw new BusinessException(ErrorCode.NO_PERMISSION_ERROR,
369+
String.format("User does not have permission to download licenses for team %d", mannschaftid));
370+
}
321371

322372
final byte[] fileBloB = lizenzComponent.getMannschaftsLizenzenPDFasByteArray(mannschaftid);
323373

@@ -334,16 +384,16 @@ ResponseEntity<InputStreamResource> downloadLizenzenPdf(@RequestParam("mannschaf
334384
* <pre>{@code Request: GET /v1/download/pdf/Einzelstatistik/?werte=x}</pre>
335385
*
336386
* @return pdf as InputStreamRessource
337-
*/
387+
*/
338388
@CrossOrigin(maxAge = 0)
339389
@GetMapping(
340390
path = "pdf/Einzelstatistik",
341391
produces = MediaType.APPLICATION_PDF_VALUE)
342392
@RequiresPermission(UserPermission.CAN_READ_DEFAULT)
343393
public
344394
ResponseEntity<InputStreamResource> downloadEinzelstatistikPdf(@RequestParam("veranstaltungsid") final long veranstaltungsid,
345-
@RequestParam("manschaftsid") final long manschaftsid,
346-
@RequestParam("jahr") final int jahr)
395+
@RequestParam("manschaftsid") final long manschaftsid,
396+
@RequestParam("jahr") final int jahr)
347397
{
348398

349399
final byte[] fileBloB = wettkampfComponent.getPDFasByteArray("Einzelstatistik",veranstaltungsid,manschaftsid,jahr);

bogenliga/bogenliga-application/src/main/java/de/bogenliga/application/services/v1/dsbmannschaft/service/DsbMannschaftService.java

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class DsbMannschaftService implements ServiceFacade {
5050
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_VEREIN_ID_NEGATIVE = "DsbMannschaft Vereins Id must not be negative";
5151
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_NUMMER_NEGATIVE = "DsbMannschaft Nummer must not be negative";
5252
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_BENUTZER_ID_NEGATIVE = "DsbMannschaft Benutzer Id must not be negative";
53-
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_VERANSTALTUNG_FULL = "DsbMannschaft Veranstaltung has already reached its maximum capacity";
53+
private static final String ERROR_MSG_VERANSTALTUNG_FULL = "Die Veranstaltung hat ihre maximale Kapazität erreicht. Es kann keine weitere Mannschaft hinzugefügt werden.";
5454
private static final String PRECONDITION_MSG_ID_NEGATIVE = "ID must not be negative.";
5555
private static final String PRECONDITION_MSG_WRONG_YEAR = "Year has to be a valid year.";
5656
private static final String PRECONDITION_MSG_VERANSTALTUNG_SIZE_NEGATIV = "DsbMannschaft Veranstaltung size can not be negativ";
@@ -74,6 +74,7 @@ public class DsbMannschaftService implements ServiceFacade {
7474
private static final String ERROR_MSG_PLATZHALTER_NO_PERMISSION = "Sie haben keine Berechtigung für diese Aktion.";
7575
private static final String ERROR_MSG_DELETE_MANNSCHAFT_NO_PERMISSION = "Löschen einer Mannschaft ist nur mit entsprechender Berechtigung erlaubt.";
7676
private static final String ERROR_MSG_UPDATE_MANNSCHAFT_NO_PERMISSION = "Ändern einer Mannschaft ist nur mit entsprechender Berechtigung erlaubt.";
77+
private static final String ERROR_MSG_VERANSTALTUNG_LAUFEND = "Die Veranstaltung ist in der Phase 'Laufend'. Teilnehmende Mannschaften können in dieser Phase nicht hinzugefügt, geändert oder entfernt werden.";
7778

7879
MannschaftsmitgliedComponent mannschaftsmitgliedComponent;
7980

@@ -337,6 +338,9 @@ public DsbMannschaftDTO create(@RequestBody final DsbMannschaftDTO dsbMannschaft
337338
final Long userId = UserProvider.getCurrentUserId(principal);
338339
Preconditions.checkArgument(userId >= 0, PRECONDITION_MSG_DSBMANNSCHAFT_BENUTZER_ID_NEGATIVE);
339340

341+
// Bei laufender Veranstaltung duerfen keine Mannschaften (auch keine Platzhalter) angelegt werden.
342+
assertVeranstaltungNotLaufend(dsbMannschaftDTO.getVeranstaltungId());
343+
340344
// Check size of Veranstaltung and if it is full
341345
// If Veranstaltung does not exist choose 8 as its default size
342346
if(dsbMannschaftDTO.getVereinId().equals(PLATZHALTER_VEREIN_ID)) {
@@ -360,9 +364,9 @@ public DsbMannschaftDTO create(@RequestBody final DsbMannschaftDTO dsbMannschaft
360364
}
361365

362366

363-
Preconditions.checkArgument(actualMannschaftInVeranstaltungCount.size() < veranstaltungsgroesse
364-
365-
, PRECONDITION_MSG_DSBMANNSCHAFT_VERANSTALTUNG_FULL);
367+
if (actualMannschaftInVeranstaltungCount.size() >= veranstaltungsgroesse) {
368+
throw new BusinessException(ErrorCode.VERANSTALTUNG_MAX_CAPACITY_ERROR, ERROR_MSG_VERANSTALTUNG_FULL);
369+
}
366370
}
367371
LOG.debug("Receive 'create' request with verein id '{}', nummer '{}', benutzer id '{}', veranstaltung id '{}',",
368372

@@ -534,9 +538,11 @@ public void assignMannschaftToVeranstaltung(@PathVariable("VeranstaltungsId") fi
534538
throw new NoPermissionException();
535539
}
536540

537-
538541
dsbMannschaftDO.setVeranstaltungId(veranstaltungsId);
539542
VeranstaltungDO veranstaltungDO = veranstaltungComponent.findById(veranstaltungsId);
543+
// Bei laufender Veranstaltung duerfen keine Mannschaften zugeordnet werden
544+
// (Pruefung auf dem bereits geladenen DO -> kein erneutes findById).
545+
assertVeranstaltungNotLaufend(veranstaltungDO);
540546
dsbMannschaftDO.setSportjahr(veranstaltungDO.getVeranstaltungSportJahr());
541547

542548
DsbMannschaftDO neueMannschaft = dsbMannschaftComponent.update(dsbMannschaftDO, userId);
@@ -559,6 +565,9 @@ public void unassignMannschaftFromVeranstaltung(@PathVariable("MannschaftId") fi
559565

560566
DsbMannschaftDO dsbMannschaftDO = dsbMannschaftComponent.findById(mannschaftId);
561567

568+
// Bei laufender Veranstaltung darf die Zuordnung der Mannschaft nicht entfernt werden.
569+
assertVeranstaltungNotLaufend(dsbMannschaftDO.getVeranstaltungId());
570+
562571
// Prüfen, ob die Veranstaltung in der Phase "geplant" ist -
563572
// nur dann können wir löschen, ohne dass Daten verloren gehen könnten...
564573
// das Sportjahr wird immer parallel gelöscht/gesetzt
@@ -574,6 +583,29 @@ public void unassignMannschaftFromVeranstaltung(@PathVariable("MannschaftId") fi
574583
}
575584
}
576585

586+
/**
587+
* Stellt sicher, dass die Veranstaltung nicht in der Phase 'Laufend' ist.
588+
* In dieser Phase duerfen teilnehmende Mannschaften weder hinzugefuegt noch
589+
* entfernt oder umsortiert werden (serverseitige Absicherung der UI-Sperre
590+
* aus Ticket swt2#2157, vgl. swt2#2229). Bei {@code null} (keine Veranstaltung
591+
* zugeordnet) passiert nichts.
592+
*
593+
* @param veranstaltungsId id der zu pruefenden Veranstaltung (darf null sein)
594+
* @throws BusinessException wenn die Veranstaltung in der Phase 'Laufend' ist
595+
*/
596+
private void assertVeranstaltungNotLaufend(final Long veranstaltungsId) {
597+
if (veranstaltungsId != null && veranstaltungComponent.isVeranstaltungLaufend(veranstaltungsId)) {
598+
throw new BusinessException(ErrorCode.ENTITY_CONFLICT_ERROR, ERROR_MSG_VERANSTALTUNG_LAUFEND);
599+
}
600+
}
601+
602+
/** Variante fuer eine bereits geladene Veranstaltung (vermeidet erneutes findById). */
603+
private void assertVeranstaltungNotLaufend(final VeranstaltungDO veranstaltung) {
604+
if (veranstaltungComponent.isVeranstaltungLaufend(veranstaltung)) {
605+
throw new BusinessException(ErrorCode.ENTITY_CONFLICT_ERROR, ERROR_MSG_VERANSTALTUNG_LAUFEND);
606+
}
607+
}
608+
577609
/**
578610
* I persist a newer version of the dsbMannschaft in the database.
579611
*
@@ -613,8 +645,9 @@ public DsbMannschaftDTO update(@RequestBody final DsbMannschaftDTO dsbMannschaft
613645
if(veranstaltungDO != null) {
614646
veranstaltungsgroesse = veranstaltungDO.getVeranstaltungGroesse();
615647
}
616-
Preconditions.checkArgument(mannschaftenInZielveranstaltung.size() < veranstaltungsgroesse
617-
, PRECONDITION_MSG_DSBMANNSCHAFT_VERANSTALTUNG_FULL);
648+
if (mannschaftenInZielveranstaltung.size() >= veranstaltungsgroesse) {
649+
throw new BusinessException(ErrorCode.VERANSTALTUNG_MAX_CAPACITY_ERROR, ERROR_MSG_VERANSTALTUNG_FULL);
650+
}
618651
}
619652

620653

bogenliga/bogenliga-application/src/main/java/de/bogenliga/application/services/v1/dsbmannschaft/service/MannschaftSortierungService.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package de.bogenliga.application.services.v1.dsbmannschaft.service;
22

3+
import de.bogenliga.application.business.dsbmannschaft.api.DsbMannschaftComponent;
34
import de.bogenliga.application.business.dsbmannschaft.api.DsbMannschaftSortierungComponent;
45
import de.bogenliga.application.business.dsbmannschaft.api.types.DsbMannschaftDO;
6+
import de.bogenliga.application.business.veranstaltung.api.VeranstaltungComponent;
7+
import de.bogenliga.application.common.errorhandling.ErrorCode;
8+
import de.bogenliga.application.common.errorhandling.exception.BusinessException;
59
import de.bogenliga.application.common.service.ServiceFacade;
610
import de.bogenliga.application.common.service.UserProvider;
711
import de.bogenliga.application.common.validation.Preconditions;
@@ -25,6 +29,7 @@ public class MannschaftSortierungService implements ServiceFacade {
2529
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_DO = "DsbMannschaftDO must not be null";
2630
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_ID = "DsbMannschaft-ID must not be null or less than 0";
2731
private static final String PRECONDITION_MSG_DSBMANNSCHAFT_SORTIERUNG = "DsbMannschaft-Sortierung must not be null or less than 0";
32+
private static final String ERROR_MSG_VERANSTALTUNG_LAUFEND = "Die Veranstaltung ist in der Phase 'Laufend'. Der Tabellenplatz teilnehmender Mannschaften kann in dieser Phase nicht geändert werden.";
2833

2934
private static final Logger LOG = LoggerFactory.getLogger(MannschaftSortierungService.class);
3035

@@ -34,16 +39,24 @@ public class MannschaftSortierungService implements ServiceFacade {
3439
* dependency injection with {@link Autowired}
3540
*/
3641
private final DsbMannschaftSortierungComponent maSortierungComponent;
42+
private final DsbMannschaftComponent dsbMannschaftComponent;
43+
private final VeranstaltungComponent veranstaltungComponent;
3744

3845

3946
/**
4047
* Constructor with dependency injection
4148
*
4249
* @param maSortierungComponent to handle the database CRUD requests
50+
* @param dsbMannschaftComponent to resolve the Veranstaltung of a Mannschaft
51+
* @param veranstaltungComponent to read the phase of the Veranstaltung
4352
*/
4453
@Autowired
45-
public MannschaftSortierungService(final DsbMannschaftSortierungComponent maSortierungComponent) {
54+
public MannschaftSortierungService(final DsbMannschaftSortierungComponent maSortierungComponent,
55+
final DsbMannschaftComponent dsbMannschaftComponent,
56+
final VeranstaltungComponent veranstaltungComponent) {
4657
this.maSortierungComponent = maSortierungComponent;
58+
this.dsbMannschaftComponent = dsbMannschaftComponent;
59+
this.veranstaltungComponent = veranstaltungComponent;
4760
}
4861

4962

@@ -61,10 +74,27 @@ public MannschaftSortierungDTO update(@RequestBody final MannschaftSortierungDTO
6174
LOG.debug("Receive 'update' request with Mannschafts-ID '{}' and Mannschafts-Sortierung '{}'.",
6275
maSortierungDTO.getId(), maSortierungDTO.getSortierung());
6376

77+
// Bei laufender Veranstaltung darf der Tabellenplatz nicht geaendert werden.
78+
final DsbMannschaftDO existingMannschaft = dsbMannschaftComponent.findById(maSortierungDTO.getId());
79+
assertVeranstaltungNotLaufend(existingMannschaft.getVeranstaltungId());
80+
6481
final DsbMannschaftDO newDsbMannschaftDO = MannschaftSortierungDTOMapper.toDO.apply(maSortierungDTO);
6582
final long userId = UserProvider.getCurrentUserId(principal);
6683

6784
final DsbMannschaftDO updatedDsbMannschaftDO = maSortierungComponent.updateSortierung(newDsbMannschaftDO, userId);
6885
return MannschaftSortierungDTOMapper.toDTO.apply(updatedDsbMannschaftDO);
6986
}
87+
88+
/**
89+
* Stellt sicher, dass die Veranstaltung nicht in der Phase 'Laufend' ist.
90+
* Bei {@code null} (keine Veranstaltung zugeordnet) passiert nichts.
91+
*
92+
* @param veranstaltungsId id der zu pruefenden Veranstaltung (darf null sein)
93+
* @throws BusinessException wenn die Veranstaltung in der Phase 'Laufend' ist
94+
*/
95+
private void assertVeranstaltungNotLaufend(final Long veranstaltungsId) {
96+
if (veranstaltungsId != null && veranstaltungComponent.isVeranstaltungLaufend(veranstaltungsId)) {
97+
throw new BusinessException(ErrorCode.ENTITY_CONFLICT_ERROR, ERROR_MSG_VERANSTALTUNG_LAUFEND);
98+
}
99+
}
70100
}

0 commit comments

Comments
 (0)