Files
votianng/backend/src/main/java/de/stadtbote/votian/ApiController.java
T

73 lines
6.6 KiB
Java

package de.stadtbote.votian;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.*;
import static de.stadtbote.votian.Models.*;
@RestController
@RequestMapping("/api")
public class ApiController {
private final LegacyReadService legacy;
private final String legacyUrl;
ApiController(LegacyReadService legacy,@Value("${app.legacy-url:}") String legacyUrl) { this.legacy=legacy;this.legacyUrl=safeLegacyUrl(legacyUrl); }
static String safeLegacyUrl(String raw) {
if(raw==null || raw.isBlank())return null;
URI uri=URI.create(raw.trim());
if(!Set.of("http","https").contains(uri.getScheme()) || uri.getHost()==null || uri.getUserInfo()!=null || uri.getQuery()!=null || uri.getFragment()!=null) throw new IllegalArgumentException("LEGACY_APP_URL muss eine HTTP(S)-Basisadresse ohne Zugangsdaten, Query oder Fragment sein.");
return raw.trim().replaceAll("/+$","");
}
static LegacyUser user(Authentication authentication) {
if(authentication==null || !(authentication.getPrincipal() instanceof LegacyUser u))throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,"Bitte anmelden.");
return u;
}
@GetMapping("/health") public Map<String,String> health() { return Map.of("status","UP"); }
@GetMapping("/csrf") public Map<String,String> csrf(CsrfToken csrf) {
return Map.of("token",csrf.getToken(),"headerName",csrf.getHeaderName(),"parameterName",csrf.getParameterName());
}
@GetMapping("/system") public Map<String,Object> system(Authentication authentication) {
var connection=legacy.connectionCheck();
boolean connected=connection.connected();
LegacyUser u=authentication!=null&&authentication.getPrincipal() instanceof LegacyUser lu?lu:null;
Map<String,Object> result=new LinkedHashMap<>();
result.put("connected",connected);result.put("database","legacy-mysql");result.put("readOnly",true);result.put("schemaChanges",false);
result.put("message",connection.message());result.put("reason",connection.reason());
result.put("legacyUrl",legacyUrl);result.put("maxResults",legacy.maxResults());
result.put("capabilities",u==null?new Capabilities(false,false,false,false,false,false,false,false,false,false):u.capabilities());
result.put("unavailableModules",List.of("Rechnungserstellung und Zahlungsstatus","Änderungen an Aufträgen und Stammdaten","Historie aus separater Log-Datenbank","Archivtabellen"));
return result;
}
@GetMapping("/me") public Map<String,Object> me(Authentication authentication) {
return userDetails(authentication);
}
static Map<String,Object> userDetails(Authentication authentication) {
LegacyUser u=user(authentication);return Map.of("username",u.username(),"roles",List.of("VIEWER"),"headquartersId",u.headquartersId(),"userId",u.userId(),"capabilities",u.capabilities());
}
@GetMapping("/orders") public ResponseEntity<List<DeliveryOrder>> orders(Authentication auth,@RequestParam(required=false) String q,@RequestParam(required=false) OrderStatus status,@RequestParam(required=false) Long customerId,@RequestParam(required=false) Long courierId,@RequestParam(required=false) LocalDate from,@RequestParam(required=false) LocalDate to) {
var result=legacy.orders(user(auth),q,status,customerId,courierId,from,to);
return ResponseEntity.ok().header("X-Total-Count",Long.toString(result.total())).header("X-Result-Limit",Integer.toString(result.limit())).body(result.orders());
}
@GetMapping("/orders/{id}") public DeliveryOrder order(Authentication auth,@PathVariable long id) { return legacy.order(user(auth),id); }
@GetMapping("/customers") public List<Customer> customers(Authentication auth,@RequestParam(required=false) String q) { return legacy.customers(user(auth),q); }
@GetMapping("/couriers") public List<Courier> couriers(Authentication auth,@RequestParam(required=false) String q) { return legacy.couriers(user(auth),q); }
@GetMapping("/orders/export.csv") public ResponseEntity<byte[]> csv(Authentication auth,@RequestParam(required=false) String q,@RequestParam(required=false) OrderStatus status,@RequestParam(required=false) Long customerId,@RequestParam(required=false) Long courierId,@RequestParam(required=false) LocalDate from,@RequestParam(required=false) LocalDate to) {
LegacyUser u=user(auth);u.require(5);
var result=legacy.orders(u,q,status,customerId,courierId,from,to);
if(result.total()>result.limit())throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"Der Export umfasst mehr als "+result.limit()+" Aufträge. Bitte den Zeitraum oder die Filter eingrenzen; ein unvollständiger Export wird nicht erstellt.");
StringBuilder csv=new StringBuilder("\uFEFFAuftrag;Status;Kunde-ID;Kurier-ID;Abholung;Referenz;Netto;Kurierpreis;MwSt\n");
for(DeliveryOrder o:result.orders())csv.append(csvCell(o.number)).append(';').append(csvCell(o.legacyStatusLabel)).append(';').append(csvCell(o.customerId)).append(';').append(csvCell(o.courierId)).append(';').append(csvCell(o.pickupAt)).append(';').append(csvCell(o.reference)).append(';').append(csvCell(o.netPrice)).append(';').append(csvCell(o.courierPrice)).append(';').append(csvCell(o.vatRate)).append('\n');
return ResponseEntity.ok().contentType(new MediaType("text","csv",StandardCharsets.UTF_8)).header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=stadtbote-auftraege.csv").body(csv.toString().getBytes(StandardCharsets.UTF_8));
}
static String csvCell(Object value) { String s=value==null?"":value.toString();String trimmed=s.stripLeading();if(!trimmed.isEmpty()&&"=+-@".indexOf(trimmed.charAt(0))>=0)s="'"+s;return '"'+s.replace("\"","\"\"")+'"'; }
@GetMapping("/invoices") public Object invoices(Authentication auth) { user(auth).require(5);throw unsupported(); }
@RequestMapping(value="/**",method={RequestMethod.POST,RequestMethod.PUT,RequestMethod.PATCH,RequestMethod.DELETE}) public Object mutation() { throw unsupported(); }
static ResponseStatusException unsupported() { return new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED,"Dieser Schreib-/Abrechnungsworkflow ist noch nicht auf die vorhandene VoTiAn-Datenbank portiert. Bitte das bestehende VoTiAn verwenden. Es werden keine Ersatzdatensätze oder Tabellen angelegt."); }
}