initial commit

This commit is contained in:
2025-10-23 10:57:33 +07:00
commit b39d1a435d
34 changed files with 1353 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package id.luxic.mai_queue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MaiQueueApplication {
public static void main(String[] args) {
SpringApplication.run(MaiQueueApplication.class, args);
}
}

View File

@@ -0,0 +1,32 @@
package id.luxic.mai_queue.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DatasourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.build();
}
}

View File

@@ -0,0 +1,28 @@
package id.luxic.mai_queue.config;
import id.luxic.mai_queue.response.ResponseMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class ExceptionHandling {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ResponseMessage> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseMessage.of(HttpStatus.BAD_REQUEST, errors);
}
}

View File

@@ -0,0 +1,35 @@
package id.luxic.mai_queue.controller;
import id.luxic.mai_queue.model.User;
import id.luxic.mai_queue.request.auth.LoginAsGuestRequest;
import id.luxic.mai_queue.response.ResponseMessage;
import id.luxic.mai_queue.service.AuthService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<ResponseMessage> login() {
return ResponseMessage.of(HttpStatus.OK, null, "Login");
}
@PostMapping("/loginAsGuest")
public ResponseEntity<ResponseMessage> loginAsGuest(@RequestBody LoginAsGuestRequest loginAsGuestRequest) {
User user = authService.loginAsGuest(null, loginAsGuestRequest.getName());
return ResponseMessage.of(HttpStatus.OK, user, "Login as guest success");
}
}

View File

@@ -0,0 +1,55 @@
package id.luxic.mai_queue.controller;
import id.luxic.mai_queue.model.Location;
import id.luxic.mai_queue.request.location.NewLocationRequest;
import id.luxic.mai_queue.response.ResponseMessage;
import id.luxic.mai_queue.service.LocationService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/location")
@Slf4j
public class LocationController {
private final LocationService locationService;
public LocationController(LocationService locationService) {
this.locationService = locationService;
}
@GetMapping("/getAll")
public ResponseEntity<ResponseMessage> getAllLocation() {
return ResponseMessage.of(HttpStatus.OK, locationService.getAllLocation());
}
@PostMapping("/save")
public ResponseEntity<Location> saveLocation(@Valid @RequestBody NewLocationRequest newLocationRequest) {
log.info("saveLocation: {}", newLocationRequest);
return ResponseEntity.ok(locationService.saveLocation(newLocationRequest));
}
@GetMapping("/get/{id}")
public ResponseEntity<ResponseMessage> getLocationById(@PathVariable String id) {
Location location = locationService.getLocationById(id);
if (location == null) {
return ResponseMessage.of(HttpStatus.NOT_FOUND, null, "Location with id " + id + " not found");
}
return ResponseMessage.of(HttpStatus.OK, location);
}
@DeleteMapping("/delete/{id}")
public ResponseEntity<Void> deleteLocation(@PathVariable String id) {
locationService.deleteLocation(id);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,36 @@
package id.luxic.mai_queue.controller;
import id.luxic.mai_queue.request.queue.QueueInsertSoloRequest;
import id.luxic.mai_queue.response.ResponseMessage;
import id.luxic.mai_queue.service.QueueService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/queue")
public class QueueController {
private final QueueService queueService;
public QueueController(QueueService queueService) {
this.queueService = queueService;
}
@PostMapping("/insert/solo")
public ResponseEntity<ResponseMessage> insertSolo(@RequestBody QueueInsertSoloRequest request) {
return ResponseMessage.of(HttpStatus.OK, queueService.insertSingle(request.getLocationId(), request.getUserId()));
}
@PostMapping("/insert/pair")
public ResponseEntity<ResponseMessage> insertPair() {
return ResponseMessage.of(HttpStatus.OK, null, "Insert group");
}
@GetMapping("/getByLocation/{id}")
public ResponseEntity<ResponseMessage> findByLocationId(@PathVariable String id) {
return ResponseMessage.of(HttpStatus.OK, queueService.getQueueByLocation(id), "Find by location id");
}
}

View File

@@ -0,0 +1,26 @@
package id.luxic.mai_queue.controller;
import id.luxic.mai_queue.response.ResponseMessage;
import id.luxic.mai_queue.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping(value = "/getAll")
public ResponseEntity<ResponseMessage> getAllUsers() {
return ResponseMessage.of(HttpStatus.OK, userService.getAllUser());
}
}

View File

@@ -0,0 +1,45 @@
package id.luxic.mai_queue.model;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.*;
import java.sql.Timestamp;
import java.util.List;
@Entity
@Table(name = "location")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Location {
@Id
@Column(name = "id")
private String id;
@Column(name = "name")
private String name;
@Column(name = "time_zone")
private String timeZone;
@Column(name = "created_at")
private Timestamp createdAt;
@Column(name = "current")
private Integer currentQueue;
@Column(name = "need_pair")
private Integer needPair;
@OneToMany(mappedBy = "location")
@JsonManagedReference
private List<User> players;
@OneToMany(mappedBy = "location")
@JsonManagedReference
private List<Queue> queues;
}

View File

@@ -0,0 +1,39 @@
package id.luxic.mai_queue.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "queue")
@Builder
public class Queue {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "\"order\"")
private Integer order;
@ManyToOne
@JoinColumn(name = "location_id", referencedColumnName = "id")
@JsonBackReference
private Location location;
@ManyToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User player;
@Column(name = "pair_id")
private String pairId;
@Column(name = "solo")
private boolean solo;
}

View File

@@ -0,0 +1,39 @@
package id.luxic.mai_queue.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "\"user\"")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
@Id
@Column(name = "id")
private String id;
@Column(name = "name")
private String name;
@Column(name = "username", unique = true)
private String username;
@Column(name = "password")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
@Column(name = "guest")
private Boolean guest;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "location_id", referencedColumnName = "id")
@JsonBackReference
private Location location;
}

View File

@@ -0,0 +1,7 @@
package id.luxic.mai_queue.repository;
import id.luxic.mai_queue.model.Location;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LocationRepository extends JpaRepository<Location, String> {
}

View File

@@ -0,0 +1,27 @@
package id.luxic.mai_queue.repository;
import id.luxic.mai_queue.model.Queue;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface QueueRepository extends JpaRepository<Queue, Integer> {
@Query("SELECT MAX(q.order) FROM Queue q WHERE q.location.id = :locationId")
Integer findLastOrderByLocationId(@Param("locationId") String locationId);
@Query("SELECT q FROM Queue q WHERE q.location.id = :locationId ORDER BY order ASC")
List<Queue> findByLocationId(@Param("locationId") String locationId);
@Modifying
@Query("""
UPDATE Queue q
set q.order = q.order + 1
where q.location.id = :locationId and q.order > :targetOrder
""")
void reorderQueue(@Param("locationId") String locationId, Integer targetOrder);
}

View File

@@ -0,0 +1,7 @@
package id.luxic.mai_queue.repository;
import id.luxic.mai_queue.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, String> {
}

View File

@@ -0,0 +1,10 @@
package id.luxic.mai_queue.request.auth;
import lombok.Data;
@Data
public class LoginAsGuestRequest {
private String name;
}

View File

@@ -0,0 +1,15 @@
package id.luxic.mai_queue.request.location;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.antlr.v4.runtime.misc.NotNull;
@Data
public class NewLocationRequest {
@NotBlank(message = "name is required")
private String name;
@NotBlank(message = "timeZone is required")
private String timeZone;
}

View File

@@ -0,0 +1,11 @@
package id.luxic.mai_queue.request.queue;
import lombok.Data;
@Data
public class QueueInsertSoloRequest {
private String userId;
private String locationId;
}

View File

@@ -0,0 +1,23 @@
package id.luxic.mai_queue.response;
import lombok.Builder;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
@Builder
@Data
public class ResponseMessage {
private Integer status;
private String message;
private Object object;
public static ResponseEntity<ResponseMessage> of(HttpStatus status, Object object) {
return new ResponseEntity<>(ResponseMessage.builder().object(object).status(status.value()).build(), status);
}
public static ResponseEntity<ResponseMessage> of(HttpStatus status, Object object, String message) {
return new ResponseEntity<>(ResponseMessage.builder().object(object).status(status.value()).message(message).build(), status);
}
}

View File

@@ -0,0 +1,4 @@
package id.luxic.mai_queue.scheduler;
public class Cleanup {
}

View File

@@ -0,0 +1,25 @@
package id.luxic.mai_queue.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@Slf4j
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("filterChain {}", http);
http.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll());
return http.build();
}
}

View File

@@ -0,0 +1,30 @@
package id.luxic.mai_queue.service;
import id.luxic.mai_queue.model.User;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
private final UserService userService;
public AuthService(UserService userService) {
this.userService = userService;
}
public User loginAsGuest(String id, String name) {
if (id != null) {
User user = userService.getUserById(id);
if (user != null) {
return user;
}
}
return userService.createGuestUser(name);
}
}

View File

@@ -0,0 +1,46 @@
package id.luxic.mai_queue.service;
import id.luxic.mai_queue.model.Location;
import id.luxic.mai_queue.repository.LocationRepository;
import id.luxic.mai_queue.request.location.NewLocationRequest;
import id.luxic.mai_queue.tools.IdGenerator;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
@Service
public class LocationService {
private final LocationRepository locationRepository;
public LocationService(LocationRepository locationRepository) {
this.locationRepository = locationRepository;
}
public List<Location> getAllLocation() {
return locationRepository.findAll();
}
public Location getLocationById(String id) {
return locationRepository.findById(id).orElse(null);
}
public Location saveLocation(NewLocationRequest newLocationRequest) {
Location location = Location.builder()
.id(IdGenerator.generateUUID())
.name(newLocationRequest.getName())
.timeZone(newLocationRequest.getTimeZone())
.createdAt(new Timestamp(System.currentTimeMillis()))
.build();
return locationRepository.save(location);
}
public void deleteLocation(String id) {
locationRepository.deleteById(id);
}
}

View File

@@ -0,0 +1,66 @@
package id.luxic.mai_queue.service;
import id.luxic.mai_queue.model.Location;
import id.luxic.mai_queue.model.Queue;
import id.luxic.mai_queue.model.User;
import id.luxic.mai_queue.repository.LocationRepository;
import id.luxic.mai_queue.repository.QueueRepository;
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class QueueService {
private final QueueRepository queueRepository;
private final LocationRepository locationRepository;
public QueueService(QueueRepository queueRepository, LocationRepository locationRepository) {
this.queueRepository = queueRepository;
this.locationRepository = locationRepository;
}
@Transactional
public Queue insertSingle(String locationId, String userId) {
Integer lastOrder = queueRepository.findLastOrderByLocationId(locationId);
Integer nextOrder = lastOrder == null ? 1 : lastOrder + 1;
Location location = locationRepository.findById(locationId).orElse(null);
if (location == null) {
throw new IllegalArgumentException("Location not found");
}
Queue queue = queueRepository.save(Queue.builder()
.location(location)
.player(User.builder().id(userId).build())
.order(nextOrder)
.pairId(null)
.build());
// Auto Reorder (Pending)
// if (location.getNeedPair() == null) {
// location.setNeedPair(nextOrder);
// } else {
// reorderQueue(locationId, queue, location.getNeedPair());
// location.setNeedPair(null);
// }
//
// locationRepository.save(location);
return queue;
}
public List<Queue> getQueueByLocation(String locationId) {
return queueRepository.findByLocationId(locationId);
}
public void reorderQueue(String locationId, Queue source, Integer targetOrder) {
// Source must be higher order than target
queueRepository.reorderQueue(locationId, targetOrder);
source.setOrder(targetOrder+1);
queueRepository.save(source);
}
}

View File

@@ -0,0 +1,35 @@
package id.luxic.mai_queue.service;
import id.luxic.mai_queue.model.User;
import id.luxic.mai_queue.repository.UserRepository;
import id.luxic.mai_queue.tools.IdGenerator;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUser() {
return userRepository.findAll();
}
public User saveUser(User user) {
return userRepository.save(user);
}
public User createGuestUser(String name) {
return userRepository.save(User.builder().id(IdGenerator.generateUUID()).name(name).guest(true).build());
}
public User getUserById(String id) {
return userRepository.findById(id).orElse(null);
}
}

View File

@@ -0,0 +1,11 @@
package id.luxic.mai_queue.tools;
import java.util.UUID;
public class IdGenerator {
public static String generateUUID() {
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,7 @@
spring.application.name=mai-queue
spring.datasource.url=jdbc:postgresql://localhost:5432/mai-queue
spring.datasource.username=postgres
spring.datasource.password=password
server.port=8010