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 health() { return Map.of("status","UP"); } @GetMapping("/csrf") public Map csrf(CsrfToken csrf) { return Map.of("token",csrf.getToken(),"headerName",csrf.getHeaderName(),"parameterName",csrf.getParameterName()); } @GetMapping("/system") public Map system(Authentication authentication) { var connection=legacy.connectionCheck(); boolean connected=connection.connected(); LegacyUser u=authentication!=null&&authentication.getPrincipal() instanceof LegacyUser lu?lu:null; Map 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 me(Authentication authentication) { return userDetails(authentication); } static Map 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> 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 customers(Authentication auth,@RequestParam(required=false) String q) { return legacy.customers(user(auth),q); } @GetMapping("/couriers") public List couriers(Authentication auth,@RequestParam(required=false) String q) { return legacy.couriers(user(auth),q); } @GetMapping("/orders/export.csv") public ResponseEntity 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."); } }