feat: public api 90min rule, security config, floorplan drag-and-drop edit mode

This commit is contained in:
DanielS
2026-08-19 00:48:23 +02:00
parent 22469f557c
commit e942f3bee7
8 changed files with 332 additions and 68 deletions

View File

@@ -0,0 +1,31 @@
package com.caspos.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
// Public reservation endpoints — no auth required
.requestMatchers("/api/v1/public/**").permitAll()
// Internal endpoints — authenticated (placeholder, extend with JWT later)
.requestMatchers("/api/v1/**").authenticated()
.anyRequest().denyAll()
)
// Disable default form login for REST API
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable);
return http.build();
}
}

View File

@@ -0,0 +1,29 @@
package com.caspos.controller;
import com.caspos.dto.CreateReservationRequest;
import com.caspos.dto.ReservationDto;
import com.caspos.service.PublicReservationService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/public/reservations")
@CrossOrigin(origins = "*")
public class PublicReservationController {
private final PublicReservationService publicReservationService;
public PublicReservationController(PublicReservationService publicReservationService) {
this.publicReservationService = publicReservationService;
}
/**
* POST /api/v1/public/reservations
* No auth required. endTime is always set to startTime + 90 min server-side.
*/
@PostMapping
public ResponseEntity<ReservationDto> createPublic(@RequestBody CreateReservationRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(publicReservationService.createPublic(request));
}
}

View File

@@ -0,0 +1,85 @@
package com.caspos.service;
import com.caspos.domain.Guest;
import com.caspos.domain.Reservation;
import com.caspos.domain.RestaurantTable;
import com.caspos.dto.CreateReservationRequest;
import com.caspos.dto.ReservationDto;
import com.caspos.repository.GuestRepository;
import com.caspos.repository.ReservationRepository;
import com.caspos.repository.TableRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.OffsetDateTime;
import java.util.List;
/**
* Public reservation service.
* Business rule: endTime is ALWAYS set server-side to startTime + 90 minutes.
* Client-supplied endTime is silently ignored.
*/
@Service
@Transactional
public class PublicReservationService {
private static final int RESERVATION_DURATION_MINUTES = 90;
private final ReservationRepository reservationRepository;
private final GuestRepository guestRepository;
private final TableRepository tableRepository;
public PublicReservationService(
ReservationRepository reservationRepository,
GuestRepository guestRepository,
TableRepository tableRepository
) {
this.reservationRepository = reservationRepository;
this.guestRepository = guestRepository;
this.tableRepository = tableRepository;
}
public ReservationDto createPublic(CreateReservationRequest req) {
// ── 1. Create guest ──
Guest guest = new Guest();
guest.setFirstName(req.firstName());
guest.setLastName(req.lastName());
guest.setPhone(req.phone());
guest.setEmail(req.email());
guest = guestRepository.save(guest);
// ── 2. Resolve tables ──
List<RestaurantTable> tables = req.tableIds() == null
? List.of()
: tableRepository.findAllById(req.tableIds());
// ── 3. Enforce 90-min rule — ignore client endTime ──
OffsetDateTime startTime = req.startTime();
OffsetDateTime endTime = startTime.plusMinutes(RESERVATION_DURATION_MINUTES);
// ── 4. Persist ──
Reservation reservation = new Reservation();
reservation.setGuest(guest);
reservation.setTables(tables);
reservation.setStartTime(startTime);
reservation.setEndTime(endTime); // always 90 min from start
reservation.setGuestCount(req.guestCount());
reservation.setSpecialRequests(req.specialRequests());
reservation.setStatus(Reservation.Status.PENDING); // Public → starts as PENDING
Reservation saved = reservationRepository.save(reservation);
// ── 5. Map to DTO ──
var guestDto = new ReservationDto.GuestDto(
guest.getId(), guest.getFirstName(), guest.getLastName(),
guest.getPhone(), guest.getEmail()
);
return new ReservationDto(
saved.getId(), guestDto,
saved.getTables().stream().map(RestaurantTable::getId).toList(),
saved.getStartTime(), saved.getEndTime(),
saved.getGuestCount(), saved.getStatus().name(),
saved.getSpecialRequests(), saved.getCreatedAt().toString()
);
}
}