sync: migrate erp-mvp to Gitea (2026-08-10)

This commit is contained in:
konturai-ops
2026-08-10 15:26:55 +00:00
commit 1b792f02ae
449 changed files with 17419 additions and 0 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,14 @@
package com.example.erpmvp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class ErpMvpApplication {
public static void main(String[] args) {
SpringApplication.run(ErpMvpApplication.class, args);
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
package com.example.erpmvp.common.api;
import java.util.List;
public record ApiError(
String code,
String message,
List<FieldValidationError> details
) {
public static ApiError of(String code, String message) {
return new ApiError(code, message, List.of());
}
public static ApiError of(String code, String message, List<FieldValidationError> details) {
return new ApiError(code, message, details == null ? List.of() : details);
}
}
@@ -0,0 +1,20 @@
package com.example.erpmvp.common.api;
import java.time.Instant;
public record ApiResponse<T>(
boolean success,
T data,
ApiError error,
Instant timestamp
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null, Instant.now());
}
public static <T> ApiResponse<T> error(ApiError error) {
return new ApiResponse<>(false, null, error, Instant.now());
}
}
@@ -0,0 +1,7 @@
package com.example.erpmvp.common.api;
public record FieldValidationError(
String field,
String message
) {
}
@@ -0,0 +1,57 @@
package com.example.erpmvp.common.audit;
import java.time.Instant;
import java.util.UUID;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@Column(nullable = false, updatable = false)
private UUID id;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@PrePersist
protected void onCreate() {
Instant now = Instant.now();
if (id == null) {
id = UUID.randomUUID();
}
if (createdAt == null) {
createdAt = now;
}
updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
}
public UUID getId() {
return id;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
}
@@ -0,0 +1,13 @@
package com.example.erpmvp.common.error;
public class BadRequestException extends BusinessException {
public BadRequestException(String message) {
super("BAD_REQUEST", message);
}
public BadRequestException(String errorCode, String message) {
super(errorCode, message);
}
}
@@ -0,0 +1,16 @@
package com.example.erpmvp.common.error;
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
@@ -0,0 +1,13 @@
package com.example.erpmvp.common.error;
public class ConflictException extends BusinessException {
public ConflictException(String message) {
super("CONFLICT", message);
}
public ConflictException(String errorCode, String message) {
super(errorCode, message);
}
}
@@ -0,0 +1,26 @@
package com.example.erpmvp.common.error;
import java.util.Set;
import jakarta.validation.ConstraintViolationException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DebugErrorController {
// Dev-only endpoint for manual error format checks. Remove or disable it in production.
@GetMapping("/api/debug/error")
public void triggerError(@RequestParam(defaultValue = "generic") String type) {
switch (type) {
case "validation" -> throw new ConstraintViolationException("Debug validation error", Set.of());
case "not-found" -> throw new NotFoundException("DEBUG_NOT_FOUND", "Debug resource was not found");
case "business" -> throw new BusinessException("DEBUG_BUSINESS_ERROR", "Debug business rule failed");
case "generic" -> throw new RuntimeException("Debug generic error");
default -> throw new BadRequestException("UNSUPPORTED_DEBUG_ERROR_TYPE", "Unsupported debug error type: " + type);
}
}
}
@@ -0,0 +1,119 @@
package com.example.erpmvp.common.error;
import java.util.List;
import com.example.erpmvp.common.api.ApiError;
import com.example.erpmvp.common.api.ApiResponse;
import com.example.erpmvp.common.api.FieldValidationError;
import jakarta.validation.ConstraintViolationException;
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.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException exception) {
List<FieldValidationError> details = exception.getBindingResult()
.getFieldErrors()
.stream()
.map(this::toFieldValidationError)
.toList();
return buildErrorResponse(
HttpStatus.BAD_REQUEST,
"VALIDATION_ERROR",
"Validation failed",
details
);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException exception) {
List<FieldValidationError> details = exception.getConstraintViolations()
.stream()
.map(violation -> new FieldValidationError(
violation.getPropertyPath().toString(),
violation.getMessage()
))
.toList();
return buildErrorResponse(
HttpStatus.BAD_REQUEST,
"VALIDATION_ERROR",
exception.getMessage(),
details
);
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNotFound(NotFoundException exception) {
return buildErrorResponse(HttpStatus.NOT_FOUND, exception.getErrorCode(), exception.getMessage());
}
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ApiResponse<Void>> handleUnauthorized(UnauthorizedException exception) {
return buildErrorResponse(HttpStatus.UNAUTHORIZED, exception.getErrorCode(), exception.getMessage());
}
@ExceptionHandler(ConflictException.class)
public ResponseEntity<ApiResponse<Void>> handleConflict(ConflictException exception) {
return buildErrorResponse(HttpStatus.CONFLICT, exception.getErrorCode(), exception.getMessage());
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ApiResponse<Void>> handleBadRequest(BadRequestException exception) {
return buildErrorResponse(HttpStatus.BAD_REQUEST, exception.getErrorCode(), exception.getMessage());
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException exception) {
return buildErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, exception.getErrorCode(), exception.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException exception) {
return buildErrorResponse(HttpStatus.BAD_REQUEST, "BAD_REQUEST", exception.getMessage());
}
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNoResourceFound(NoResourceFoundException exception) {
return buildErrorResponse(HttpStatus.NOT_FOUND, "NOT_FOUND", "Resource not found");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception exception) {
return buildErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
"INTERNAL_SERVER_ERROR",
"Unexpected internal server error"
);
}
private FieldValidationError toFieldValidationError(FieldError fieldError) {
return new FieldValidationError(
fieldError.getField(),
fieldError.getDefaultMessage()
);
}
private ResponseEntity<ApiResponse<Void>> buildErrorResponse(HttpStatus status, String code, String message) {
return buildErrorResponse(status, code, message, List.of());
}
private ResponseEntity<ApiResponse<Void>> buildErrorResponse(
HttpStatus status,
String code,
String message,
List<FieldValidationError> details
) {
ApiError error = ApiError.of(code, message, details);
return ResponseEntity.status(status).body(ApiResponse.error(error));
}
}
@@ -0,0 +1,13 @@
package com.example.erpmvp.common.error;
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super("NOT_FOUND", message);
}
public NotFoundException(String errorCode, String message) {
super(errorCode, message);
}
}
@@ -0,0 +1,13 @@
package com.example.erpmvp.common.error;
public class UnauthorizedException extends BusinessException {
public UnauthorizedException(String message) {
super("UNAUTHORIZED", message);
}
public UnauthorizedException(String errorCode, String message) {
super(errorCode, message);
}
}
@@ -0,0 +1,38 @@
package com.example.erpmvp.common.pagination;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
public class PageRequestDto {
@Min(0)
private int page = 0;
@Min(1)
@Max(100)
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
}
@@ -0,0 +1,29 @@
package com.example.erpmvp.common.pagination;
import java.util.List;
import org.springframework.data.domain.Page;
public record PageResponseDto<T>(
List<T> items,
int page,
int size,
long totalElements,
int totalPages,
boolean hasNext,
boolean hasPrevious
) {
public static <T> PageResponseDto<T> from(Page<T> page) {
return new PageResponseDto<>(
page.getContent(),
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.hasNext(),
page.hasPrevious()
);
}
}
Binary file not shown.
@@ -0,0 +1,34 @@
package com.example.erpmvp.config;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer(
@Value("${app.cors.allowed-origins}") String allowedOrigins
) {
String[] origins = Arrays.stream(allowedOrigins.split(","))
.map(String::trim)
.filter(origin -> !origin.isBlank())
.toArray(String[]::new);
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(origins)
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Content-Disposition");
}
};
}
}
@@ -0,0 +1,29 @@
package com.example.erpmvp.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI erpMvpOpenApi() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")))
.addSecurityItem(new SecurityRequirement().addList("bearer-jwt"))
.info(new Info()
.title("ERP MVP API")
.version("v1")
.description("API documentation for ERP MVP backend"));
}
}
@@ -0,0 +1,19 @@
package com.example.erpmvp.config.demo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app.demo-data")
public class DemoDataProperties {
private boolean enabled;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
@@ -0,0 +1,644 @@
package com.example.erpmvp.config.demo;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import com.example.erpmvp.modules.auth.domain.Role;
import com.example.erpmvp.modules.auth.domain.User;
import com.example.erpmvp.modules.auth.repository.UserRepository;
import com.example.erpmvp.modules.catalog.domain.Customer;
import com.example.erpmvp.modules.catalog.domain.Product;
import com.example.erpmvp.modules.catalog.domain.Supplier;
import com.example.erpmvp.modules.catalog.domain.Warehouse;
import com.example.erpmvp.modules.catalog.repository.CustomerRepository;
import com.example.erpmvp.modules.catalog.repository.ProductRepository;
import com.example.erpmvp.modules.catalog.repository.SupplierRepository;
import com.example.erpmvp.modules.catalog.repository.WarehouseRepository;
import com.example.erpmvp.modules.documents.domain.DocumentType;
import com.example.erpmvp.modules.documents.service.DocumentService;
import com.example.erpmvp.modules.procurement.domain.PurchaseOrder;
import com.example.erpmvp.modules.procurement.domain.PurchaseOrderStatus;
import com.example.erpmvp.modules.procurement.dto.ChangePurchaseOrderStatusRequest;
import com.example.erpmvp.modules.procurement.dto.CreatePurchaseOrderItemRequest;
import com.example.erpmvp.modules.procurement.dto.CreatePurchaseOrderRequest;
import com.example.erpmvp.modules.procurement.repository.PurchaseOrderRepository;
import com.example.erpmvp.modules.procurement.service.PurchaseOrderService;
import com.example.erpmvp.modules.sales.domain.CustomerOrder;
import com.example.erpmvp.modules.sales.domain.CustomerOrderStatus;
import com.example.erpmvp.modules.sales.dto.ChangeCustomerOrderStatusRequest;
import com.example.erpmvp.modules.sales.dto.CreateCustomerOrderItemRequest;
import com.example.erpmvp.modules.sales.dto.CreateCustomerOrderRequest;
import com.example.erpmvp.modules.sales.repository.CustomerOrderRepository;
import com.example.erpmvp.modules.sales.service.CustomerOrderService;
import com.example.erpmvp.modules.warehouse.domain.StockMovementSourceType;
import com.example.erpmvp.modules.warehouse.domain.StockMovementType;
import com.example.erpmvp.modules.warehouse.dto.ManualStockAdjustmentRequest;
import com.example.erpmvp.modules.warehouse.repository.StockMovementRepository;
import com.example.erpmvp.modules.warehouse.service.WarehouseStockService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@ConditionalOnProperty(prefix = "app.demo-data", name = "enabled", havingValue = "true")
public class DemoDataSeeder implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(DemoDataSeeder.class);
private static final String PO_1_MARKER = "DEMO-SEED-PO-1";
private static final String PO_2_MARKER = "DEMO-SEED-PO-2";
private static final String PO_3_MARKER = "DEMO-SEED-PO-3";
private static final String SO_1_MARKER = "DEMO-SEED-SO-1";
private static final String SO_2_MARKER = "DEMO-SEED-SO-2";
private static final String SO_3_MARKER = "DEMO-SEED-SO-3";
private static final String SO_4_MARKER = "DEMO-SEED-SO-4";
private final DemoDataProperties properties;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final ProductRepository productRepository;
private final SupplierRepository supplierRepository;
private final CustomerRepository customerRepository;
private final WarehouseRepository warehouseRepository;
private final PurchaseOrderRepository purchaseOrderRepository;
private final CustomerOrderRepository customerOrderRepository;
private final StockMovementRepository stockMovementRepository;
private final PurchaseOrderService purchaseOrderService;
private final CustomerOrderService customerOrderService;
private final WarehouseStockService warehouseStockService;
private final DocumentService documentService;
public DemoDataSeeder(
DemoDataProperties properties,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
ProductRepository productRepository,
SupplierRepository supplierRepository,
CustomerRepository customerRepository,
WarehouseRepository warehouseRepository,
PurchaseOrderRepository purchaseOrderRepository,
CustomerOrderRepository customerOrderRepository,
StockMovementRepository stockMovementRepository,
PurchaseOrderService purchaseOrderService,
CustomerOrderService customerOrderService,
WarehouseStockService warehouseStockService,
DocumentService documentService
) {
this.properties = properties;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.productRepository = productRepository;
this.supplierRepository = supplierRepository;
this.customerRepository = customerRepository;
this.warehouseRepository = warehouseRepository;
this.purchaseOrderRepository = purchaseOrderRepository;
this.customerOrderRepository = customerOrderRepository;
this.stockMovementRepository = stockMovementRepository;
this.purchaseOrderService = purchaseOrderService;
this.customerOrderService = customerOrderService;
this.warehouseStockService = warehouseStockService;
this.documentService = documentService;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
if (!properties.isEnabled()) {
return;
}
log.info("Demo data seed is enabled. Seeding demo ERP data.");
DemoUsers users = seedUsers();
DemoCatalog catalog = seedCatalog();
seedProcurement(catalog, users.admin());
DemoSales sales = seedSales(catalog, users.admin());
seedManualStockAdjustments(catalog, users.warehouse());
seedDocuments(sales, users.admin());
log.info("Demo data seed completed.");
}
private DemoUsers seedUsers() {
User admin = findOrCreateUser(
"admin@erp.local",
"admin12345",
"System Administrator",
Role.ADMIN
);
User manager = findOrCreateUser(
"manager@erp.local",
"manager12345",
"Demo Manager",
Role.MANAGER
);
User warehouse = findOrCreateUser(
"warehouse@erp.local",
"warehouse12345",
"Demo Warehouse Operator",
Role.WAREHOUSE
);
User finance = findOrCreateUser(
"finance@erp.local",
"finance12345",
"Demo Finance Specialist",
Role.FINANCE
);
return new DemoUsers(admin, manager, warehouse, finance);
}
private User findOrCreateUser(String email, String password, String fullName, Role role) {
String normalizedEmail = User.normalizeEmail(email);
return userRepository.findByEmail(normalizedEmail)
.orElseGet(() -> userRepository.save(new User(
normalizedEmail,
fullName,
passwordEncoder.encode(password),
role
)));
}
private DemoCatalog seedCatalog() {
Product apple = upsertProduct("SKU-FRESH-001", "Apple Golden", "Fresh Fruits", "kg");
Product banana = upsertProduct("SKU-FRESH-002", "Banana Premium", "Fresh Fruits", "kg");
Product tomato = upsertProduct("SKU-FRESH-003", "Tomato Local", "Vegetables", "kg");
Product cucumber = upsertProduct("SKU-FRESH-004", "Cucumber Fresh", "Vegetables", "kg");
Product rice = upsertProduct("SKU-DRY-001", "Rice 5kg", "Grocery", "pcs");
Product sugar = upsertProduct("SKU-DRY-002", "Sugar 1kg", "Grocery", "pcs");
Product juice = upsertProduct("SKU-DRINK-001", "Apple Juice 1L", "Drinks", "pcs");
Product water = upsertProduct("SKU-DRINK-002", "Water 0.5L", "Drinks", "pcs");
Supplier freshImport = upsertSupplier("Fresh Import Kazakhstan", "111111111111");
Supplier almatyAgro = upsertSupplier("Almaty Agro Supply", "222222222222");
Supplier groceryTrade = upsertSupplier("Grocery Trade LLP", "333333333333");
Customer miniMarket = upsertCustomer("Mini Market Alatau", "444444444444");
Customer greenStore = upsertCustomer("Green Store B2B", "555555555555");
Customer cityFood = upsertCustomer("City Food Retail", "666666666666");
Warehouse mainWarehouse = upsertWarehouse("WH-ALM-01", "Almaty Main Warehouse");
Warehouse freshWarehouse = upsertWarehouse("WH-ALM-02", "Almaty Fresh Warehouse");
return new DemoCatalog(
apple,
banana,
tomato,
cucumber,
rice,
sugar,
juice,
water,
freshImport,
almatyAgro,
groceryTrade,
miniMarket,
greenStore,
cityFood,
mainWarehouse,
freshWarehouse
);
}
private Product upsertProduct(String sku, String name, String category, String unit) {
String normalizedSku = Product.normalizeCode(sku);
Product product = productRepository.findBySku(normalizedSku)
.orElseGet(() -> new Product(normalizedSku, name, category, unit, null, "Demo product"));
product.update(name, category, unit, product.getBarcode(), "Demo product", true);
return productRepository.save(product);
}
private Supplier upsertSupplier(String companyName, String bin) {
Supplier supplier = supplierRepository.findByBin(bin)
.or(() -> supplierRepository.findByCompanyName(companyName))
.orElseGet(() -> new Supplier(
companyName,
bin,
"Demo contact",
"+7 700 000 0000",
null,
"Almaty, Kazakhstan"
));
supplier.update(
companyName,
bin,
supplier.getContactName() == null ? "Demo contact" : supplier.getContactName(),
supplier.getPhone() == null ? "+7 700 000 0000" : supplier.getPhone(),
supplier.getEmail(),
supplier.getAddress() == null ? "Almaty, Kazakhstan" : supplier.getAddress(),
true
);
return supplierRepository.save(supplier);
}
private Customer upsertCustomer(String companyName, String bin) {
Customer customer = customerRepository.findByBin(bin)
.or(() -> customerRepository.findByCompanyName(companyName))
.orElseGet(() -> new Customer(
companyName,
bin,
"Demo contact",
"+7 701 000 0000",
null,
"Almaty, Kazakhstan"
));
customer.update(
companyName,
bin,
customer.getContactName() == null ? "Demo contact" : customer.getContactName(),
customer.getPhone() == null ? "+7 701 000 0000" : customer.getPhone(),
customer.getEmail(),
customer.getAddress() == null ? "Almaty, Kazakhstan" : customer.getAddress(),
true
);
return customerRepository.save(customer);
}
private Warehouse upsertWarehouse(String code, String name) {
String normalizedCode = Warehouse.normalizeCode(code);
Warehouse warehouse = warehouseRepository.findByCode(normalizedCode)
.orElseGet(() -> new Warehouse(normalizedCode, name, "Almaty, Kazakhstan"));
warehouse.update(name, warehouse.getAddress() == null ? "Almaty, Kazakhstan" : warehouse.getAddress(), true);
return warehouseRepository.save(warehouse);
}
private void seedProcurement(DemoCatalog catalog, User admin) {
seedPurchaseOrder(
PO_1_MARKER,
catalog.freshImport(),
catalog.freshWarehouse(),
LocalDate.now().plusDays(5),
List.of(
poItem(catalog.apple(), "120", "450"),
poItem(catalog.banana(), "90", "520")
),
PurchaseOrderStatus.RECEIVED,
admin
);
seedPurchaseOrder(
PO_2_MARKER,
catalog.groceryTrade(),
catalog.mainWarehouse(),
LocalDate.now().plusDays(10),
List.of(
poItem(catalog.rice(), "40", "1800"),
poItem(catalog.sugar(), "100", "390")
),
PurchaseOrderStatus.ORDERED,
admin
);
seedPurchaseOrder(
PO_3_MARKER,
catalog.almatyAgro(),
catalog.freshWarehouse(),
LocalDate.now().plusDays(7),
List.of(
poItem(catalog.tomato(), "70", "380"),
poItem(catalog.cucumber(), "60", "350")
),
PurchaseOrderStatus.DRAFT,
admin
);
}
private UUID seedPurchaseOrder(
String marker,
Supplier supplier,
Warehouse warehouse,
LocalDate expectedDeliveryDate,
List<CreatePurchaseOrderItemRequest> items,
PurchaseOrderStatus targetStatus,
User admin
) {
PurchaseOrder existing = purchaseOrderRepository.findFirstByNotesContaining(marker).orElse(null);
UUID purchaseOrderId = existing == null
? purchaseOrderService.create(new CreatePurchaseOrderRequest(
supplier.getId(),
warehouse.getId(),
expectedDeliveryDate,
marker + " demo purchase order",
items
), admin).id()
: existing.getId();
advancePurchaseOrder(purchaseOrderId, targetStatus, admin, marker);
return purchaseOrderId;
}
private CreatePurchaseOrderItemRequest poItem(Product product, String quantity, String unitPrice) {
return new CreatePurchaseOrderItemRequest(
product.getId(),
new BigDecimal(quantity),
new BigDecimal(unitPrice)
);
}
private void advancePurchaseOrder(UUID purchaseOrderId, PurchaseOrderStatus targetStatus, User admin, String marker) {
PurchaseOrderStatus current = purchaseOrderRepository.findById(purchaseOrderId)
.orElseThrow()
.getStatus();
if (current == targetStatus) {
return;
}
if (targetStatus == PurchaseOrderStatus.CANCELLED) {
purchaseOrderService.changeStatus(
purchaseOrderId,
new ChangePurchaseOrderStatusRequest(PurchaseOrderStatus.CANCELLED, marker + " cancelled"),
admin
);
return;
}
if (current == PurchaseOrderStatus.DRAFT && targetAtLeast(targetStatus, PurchaseOrderStatus.APPROVED)) {
purchaseOrderService.changeStatus(
purchaseOrderId,
new ChangePurchaseOrderStatusRequest(PurchaseOrderStatus.APPROVED, marker + " approved"),
admin
);
current = PurchaseOrderStatus.APPROVED;
}
if (current == PurchaseOrderStatus.APPROVED && targetAtLeast(targetStatus, PurchaseOrderStatus.ORDERED)) {
purchaseOrderService.changeStatus(
purchaseOrderId,
new ChangePurchaseOrderStatusRequest(PurchaseOrderStatus.ORDERED, marker + " ordered"),
admin
);
current = PurchaseOrderStatus.ORDERED;
}
if (current == PurchaseOrderStatus.ORDERED && targetStatus == PurchaseOrderStatus.RECEIVED) {
purchaseOrderService.changeStatus(
purchaseOrderId,
new ChangePurchaseOrderStatusRequest(PurchaseOrderStatus.RECEIVED, marker + " received"),
admin
);
}
}
private boolean targetAtLeast(PurchaseOrderStatus target, PurchaseOrderStatus checkpoint) {
return purchaseStatusRank(target) >= purchaseStatusRank(checkpoint);
}
private int purchaseStatusRank(PurchaseOrderStatus status) {
return switch (status) {
case DRAFT -> 0;
case APPROVED -> 1;
case ORDERED -> 2;
case RECEIVED -> 3;
case CANCELLED -> -1;
};
}
private DemoSales seedSales(DemoCatalog catalog, User admin) {
UUID shippedOrderId = seedCustomerOrder(
SO_1_MARKER,
catalog.miniMarket(),
catalog.freshWarehouse(),
LocalDate.now().plusDays(3),
List.of(
soItem(catalog.apple(), "20", "650"),
soItem(catalog.banana(), "15", "720")
),
CustomerOrderStatus.SHIPPED,
admin
);
UUID confirmedOrderId = seedCustomerOrder(
SO_2_MARKER,
catalog.greenStore(),
catalog.mainWarehouse(),
LocalDate.now().plusDays(8),
List.of(
soItem(catalog.rice(), "10", "2400"),
soItem(catalog.sugar(), "25", "550")
),
CustomerOrderStatus.CONFIRMED,
admin
);
seedCustomerOrder(
SO_3_MARKER,
catalog.cityFood(),
catalog.freshWarehouse(),
LocalDate.now().plusDays(4),
List.of(soItem(catalog.tomato(), "10", "590")),
CustomerOrderStatus.NEW,
admin
);
seedCustomerOrder(
SO_4_MARKER,
catalog.miniMarket(),
catalog.freshWarehouse(),
LocalDate.now().plusDays(6),
List.of(soItem(catalog.cucumber(), "8", "540")),
CustomerOrderStatus.CANCELLED,
admin
);
return new DemoSales(shippedOrderId, confirmedOrderId);
}
private UUID seedCustomerOrder(
String marker,
Customer customer,
Warehouse warehouse,
LocalDate requestedDeliveryDate,
List<CreateCustomerOrderItemRequest> items,
CustomerOrderStatus targetStatus,
User admin
) {
CustomerOrder existing = customerOrderRepository.findFirstByNotesContaining(marker).orElse(null);
UUID customerOrderId = existing == null
? customerOrderService.create(new CreateCustomerOrderRequest(
customer.getId(),
warehouse.getId(),
requestedDeliveryDate,
marker + " demo customer order",
items
), admin).id()
: existing.getId();
advanceCustomerOrder(customerOrderId, targetStatus, admin, marker);
return customerOrderId;
}
private CreateCustomerOrderItemRequest soItem(Product product, String quantity, String unitPrice) {
return new CreateCustomerOrderItemRequest(
product.getId(),
new BigDecimal(quantity),
new BigDecimal(unitPrice)
);
}
private void advanceCustomerOrder(UUID customerOrderId, CustomerOrderStatus targetStatus, User admin, String marker) {
CustomerOrderStatus current = customerOrderRepository.findById(customerOrderId)
.orElseThrow()
.getStatus();
if (current == targetStatus) {
return;
}
if (targetStatus == CustomerOrderStatus.CANCELLED) {
if (current != CustomerOrderStatus.SHIPPED && current != CustomerOrderStatus.CLOSED) {
customerOrderService.changeStatus(
customerOrderId,
new ChangeCustomerOrderStatusRequest(CustomerOrderStatus.CANCELLED, marker + " cancelled"),
admin
);
}
return;
}
if (current == CustomerOrderStatus.NEW && targetAtLeast(targetStatus, CustomerOrderStatus.CONFIRMED)) {
customerOrderService.changeStatus(
customerOrderId,
new ChangeCustomerOrderStatusRequest(CustomerOrderStatus.CONFIRMED, marker + " confirmed"),
admin
);
current = CustomerOrderStatus.CONFIRMED;
}
if (current == CustomerOrderStatus.CONFIRMED && targetAtLeast(targetStatus, CustomerOrderStatus.IN_PROGRESS)) {
customerOrderService.changeStatus(
customerOrderId,
new ChangeCustomerOrderStatusRequest(CustomerOrderStatus.IN_PROGRESS, marker + " in progress"),
admin
);
current = CustomerOrderStatus.IN_PROGRESS;
}
if (current == CustomerOrderStatus.IN_PROGRESS && targetAtLeast(targetStatus, CustomerOrderStatus.SHIPPED)) {
customerOrderService.changeStatus(
customerOrderId,
new ChangeCustomerOrderStatusRequest(CustomerOrderStatus.SHIPPED, marker + " shipped"),
admin
);
current = CustomerOrderStatus.SHIPPED;
}
if (current == CustomerOrderStatus.SHIPPED && targetStatus == CustomerOrderStatus.CLOSED) {
customerOrderService.changeStatus(
customerOrderId,
new ChangeCustomerOrderStatusRequest(CustomerOrderStatus.CLOSED, marker + " closed"),
admin
);
}
}
private boolean targetAtLeast(CustomerOrderStatus target, CustomerOrderStatus checkpoint) {
return customerStatusRank(target) >= customerStatusRank(checkpoint);
}
private int customerStatusRank(CustomerOrderStatus status) {
return switch (status) {
case NEW -> 0;
case CONFIRMED -> 1;
case IN_PROGRESS -> 2;
case SHIPPED -> 3;
case CLOSED -> 4;
case CANCELLED -> -1;
};
}
private void seedManualStockAdjustments(DemoCatalog catalog, User warehouseUser) {
seedManualAdjustment(
catalog.mainWarehouse(),
catalog.water(),
StockMovementType.ADJUSTMENT_IN,
"8",
"DEMO opening low stock",
warehouseUser
);
seedManualAdjustment(
catalog.mainWarehouse(),
catalog.juice(),
StockMovementType.ADJUSTMENT_IN,
"5",
"DEMO opening low stock",
warehouseUser
);
}
private void seedManualAdjustment(
Warehouse warehouse,
Product product,
StockMovementType type,
String quantity,
String comment,
User warehouseUser
) {
if (stockMovementRepository.existsBySourceTypeAndWarehouse_IdAndProduct_IdAndComment(
StockMovementSourceType.MANUAL_ADJUSTMENT,
warehouse.getId(),
product.getId(),
comment
)) {
return;
}
warehouseStockService.manualAdjustment(new ManualStockAdjustmentRequest(
warehouse.getId(),
product.getId(),
type,
new BigDecimal(quantity),
comment
), warehouseUser);
}
private void seedDocuments(DemoSales sales, User admin) {
CustomerOrder shippedOrder = customerOrderRepository.findById(sales.shippedOrderId()).orElse(null);
if (shippedOrder != null
&& (shippedOrder.getStatus() == CustomerOrderStatus.SHIPPED
|| shippedOrder.getStatus() == CustomerOrderStatus.CLOSED)) {
generateCustomerOrderDocument(shippedOrder.getId(), DocumentType.INVOICE, admin);
generateCustomerOrderDocument(shippedOrder.getId(), DocumentType.CONTRACT, admin);
generateCustomerOrderDocument(shippedOrder.getId(), DocumentType.DELIVERY_NOTE, admin);
}
CustomerOrder confirmedOrder = customerOrderRepository.findById(sales.confirmedOrderId()).orElse(null);
if (confirmedOrder != null && confirmedOrder.getStatus() != CustomerOrderStatus.CANCELLED) {
generateCustomerOrderDocument(confirmedOrder.getId(), DocumentType.INVOICE, admin);
generateCustomerOrderDocument(confirmedOrder.getId(), DocumentType.CONTRACT, admin);
}
}
private void generateCustomerOrderDocument(UUID customerOrderId, DocumentType documentType, User admin) {
documentService.generateForCustomerOrder(customerOrderId, documentType, admin);
}
private record DemoUsers(User admin, User manager, User warehouse, User finance) {
}
private record DemoCatalog(
Product apple,
Product banana,
Product tomato,
Product cucumber,
Product rice,
Product sugar,
Product juice,
Product water,
Supplier freshImport,
Supplier almatyAgro,
Supplier groceryTrade,
Customer miniMarket,
Customer greenStore,
Customer cityFood,
Warehouse mainWarehouse,
Warehouse freshWarehouse
) {
}
private record DemoSales(UUID shippedOrderId, UUID confirmedOrderId) {
}
}
@@ -0,0 +1,40 @@
package com.example.erpmvp.health;
import com.example.erpmvp.common.api.ApiResponse;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class HealthController {
private final JdbcTemplate jdbcTemplate;
public HealthController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@GetMapping("/health")
public ApiResponse<HealthResponse> health() {
String databaseStatus = checkDatabase();
String serviceStatus = "UP".equals(databaseStatus) ? "UP" : "DOWN";
return ApiResponse.success(new HealthResponse(serviceStatus, "erp-backend", "v1", databaseStatus));
}
private String checkDatabase() {
try {
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
return Integer.valueOf(1).equals(result) ? "UP" : "DOWN";
} catch (Exception exception) {
return "DOWN";
}
}
public record HealthResponse(String status, String service, String version, String database) {
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,54 @@
package com.example.erpmvp.modules.auth.controller;
import com.example.erpmvp.common.api.ApiResponse;
import com.example.erpmvp.modules.auth.dto.LoginRequest;
import com.example.erpmvp.modules.auth.dto.LoginResponse;
import com.example.erpmvp.modules.auth.dto.LogoutResponse;
import com.example.erpmvp.modules.auth.dto.MeResponse;
import com.example.erpmvp.modules.auth.dto.ProtectedTestResponse;
import com.example.erpmvp.modules.auth.security.AuthUserDetails;
import com.example.erpmvp.modules.auth.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
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/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
return ApiResponse.success(authService.login(request));
}
@GetMapping("/me")
public ApiResponse<MeResponse> me(@AuthenticationPrincipal AuthUserDetails principal) {
return ApiResponse.success(MeResponse.from(principal.getUser()));
}
@PostMapping("/logout")
public ApiResponse<LogoutResponse> logout() {
return ApiResponse.success(new LogoutResponse("Logged out successfully"));
}
@GetMapping("/protected-test")
public ApiResponse<ProtectedTestResponse> protectedTest(@AuthenticationPrincipal AuthUserDetails principal) {
return ApiResponse.success(new ProtectedTestResponse(
"You are authenticated",
principal.getUser().getEmail(),
principal.getUser().getRole()
));
}
}
@@ -0,0 +1,9 @@
package com.example.erpmvp.modules.auth.domain;
public enum Role {
ADMIN,
MANAGER,
WAREHOUSE,
FINANCE
}
@@ -0,0 +1,68 @@
package com.example.erpmvp.modules.auth.domain;
import java.util.Locale;
import com.example.erpmvp.common.audit.BaseEntity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
@Entity
@Table(name = "app_users")
public class User extends BaseEntity {
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(name = "full_name", nullable = false, length = 255)
private String fullName;
@Column(name = "password_hash", nullable = false, length = 255)
private String passwordHash;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 50)
private Role role;
@Column(nullable = false)
private boolean active = true;
protected User() {
}
public User(String email, String fullName, String passwordHash, Role role) {
this.email = normalizeEmail(email);
this.fullName = fullName;
this.passwordHash = passwordHash;
this.role = role;
this.active = true;
}
public String getEmail() {
return email;
}
public String getFullName() {
return fullName;
}
@JsonIgnore
public String getPasswordHash() {
return passwordHash;
}
public Role getRole() {
return role;
}
public boolean isActive() {
return active;
}
public static String normalizeEmail(String email) {
return email == null ? null : email.trim().toLowerCase(Locale.ROOT);
}
}
@@ -0,0 +1,19 @@
package com.example.erpmvp.modules.auth.dto;
import java.util.UUID;
import com.example.erpmvp.modules.auth.domain.Role;
import com.example.erpmvp.modules.auth.domain.User;
public record AuthUserResponse(
UUID id,
String email,
String fullName,
Role role
) {
public static AuthUserResponse from(User user) {
return new AuthUserResponse(user.getId(), user.getEmail(), user.getFullName(), user.getRole());
}
}
@@ -0,0 +1,11 @@
package com.example.erpmvp.modules.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record LoginRequest(
@NotBlank @Email String email,
@NotBlank String password
) {
}
@@ -0,0 +1,10 @@
package com.example.erpmvp.modules.auth.dto;
public record LoginResponse(
String accessToken,
String tokenType,
long expiresInMinutes,
AuthUserResponse user
) {
}
@@ -0,0 +1,5 @@
package com.example.erpmvp.modules.auth.dto;
public record LogoutResponse(String message) {
}
@@ -0,0 +1,26 @@
package com.example.erpmvp.modules.auth.dto;
import java.util.UUID;
import com.example.erpmvp.modules.auth.domain.Role;
import com.example.erpmvp.modules.auth.domain.User;
public record MeResponse(
UUID id,
String email,
String fullName,
Role role,
boolean active
) {
public static MeResponse from(User user) {
return new MeResponse(
user.getId(),
user.getEmail(),
user.getFullName(),
user.getRole(),
user.isActive()
);
}
}
@@ -0,0 +1,11 @@
package com.example.erpmvp.modules.auth.dto;
import com.example.erpmvp.modules.auth.domain.Role;
public record ProtectedTestResponse(
String message,
String email,
Role role
) {
}
@@ -0,0 +1,14 @@
package com.example.erpmvp.modules.auth.repository;
import java.util.Optional;
import java.util.UUID;
import com.example.erpmvp.modules.auth.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByEmail(String email);
}
@@ -0,0 +1,44 @@
package com.example.erpmvp.modules.auth.security;
import java.util.Collection;
import java.util.List;
import com.example.erpmvp.modules.auth.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AuthUserDetails implements UserDetails {
private final User user;
public AuthUserDetails(User user) {
this.user = user;
}
public User getUser() {
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
}
@Override
public String getPassword() {
return user.getPasswordHash();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isEnabled() {
return user.isActive();
}
}
@@ -0,0 +1,29 @@
package com.example.erpmvp.modules.auth.security;
import com.example.erpmvp.modules.auth.domain.User;
import com.example.erpmvp.modules.auth.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AuthUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AuthUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) {
String email = User.normalizeEmail(username);
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new AuthUserDetails(user);
}
}
@@ -0,0 +1,39 @@
package com.example.erpmvp.modules.auth.security;
import java.io.IOException;
import com.example.erpmvp.common.api.ApiError;
import com.example.erpmvp.common.api.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
public JwtAccessDeniedHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException, ServletException {
ApiResponse<Void> body = ApiResponse.error(ApiError.of("ACCESS_DENIED", "Access is denied"));
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
objectMapper.writeValue(response.getOutputStream(), body);
}
}
@@ -0,0 +1,39 @@
package com.example.erpmvp.modules.auth.security;
import java.io.IOException;
import com.example.erpmvp.common.api.ApiError;
import com.example.erpmvp.common.api.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException, ServletException {
ApiResponse<Void> body = ApiResponse.error(ApiError.of("UNAUTHORIZED", "Authentication is required"));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
objectMapper.writeValue(response.getOutputStream(), body);
}
}
@@ -0,0 +1,67 @@
package com.example.erpmvp.modules.auth.security;
import java.io.IOException;
import com.example.erpmvp.modules.auth.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final JwtService jwtService;
private final AuthUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, AuthUserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
String token = authorizationHeader.substring(BEARER_PREFIX.length());
try {
if (SecurityContextHolder.getContext().getAuthentication() == null && jwtService.validateToken(token)) {
String email = jwtService.extractEmail(token);
AuthUserDetails userDetails = (AuthUserDetails) userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (UsernameNotFoundException exception) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,56 @@
package com.example.erpmvp.modules.auth.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter,
JwtAuthenticationEntryPoint authenticationEntryPoint,
JwtAccessDeniedHandler accessDeniedHandler
) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/auth/login",
"/api/health",
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs",
"/v3/api-docs/**",
"/api/debug/error",
"/api/debug/error/**"
).permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@@ -0,0 +1,55 @@
package com.example.erpmvp.modules.auth.service;
import com.example.erpmvp.modules.auth.domain.Role;
import com.example.erpmvp.modules.auth.domain.User;
import com.example.erpmvp.modules.auth.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class AuthDataInitializer implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(AuthDataInitializer.class);
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final String adminEmail;
private final String adminPassword;
public AuthDataInitializer(
UserRepository userRepository,
PasswordEncoder passwordEncoder,
@Value("${app.seed.admin.email}") String adminEmail,
@Value("${app.seed.admin.password}") String adminPassword
) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.adminEmail = adminEmail;
this.adminPassword = adminPassword;
}
@Override
@Transactional
public void run(String... args) {
if (userRepository.count() > 0) {
return;
}
User admin = new User(
adminEmail,
"System Administrator",
passwordEncoder.encode(adminPassword),
Role.ADMIN
);
userRepository.save(admin);
log.info("Seeded default admin user: {}", admin.getEmail());
}
}
@@ -0,0 +1,47 @@
package com.example.erpmvp.modules.auth.service;
import com.example.erpmvp.common.error.UnauthorizedException;
import com.example.erpmvp.modules.auth.domain.User;
import com.example.erpmvp.modules.auth.dto.AuthUserResponse;
import com.example.erpmvp.modules.auth.dto.LoginRequest;
import com.example.erpmvp.modules.auth.dto.LoginResponse;
import com.example.erpmvp.modules.auth.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
}
@Transactional(readOnly = true)
public LoginResponse login(LoginRequest request) {
String email = User.normalizeEmail(request.email());
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UnauthorizedException("INVALID_CREDENTIALS", "Invalid email or password"));
if (!user.isActive() || !passwordEncoder.matches(request.password(), user.getPasswordHash())) {
throw new UnauthorizedException("INVALID_CREDENTIALS", "Invalid email or password");
}
String token = jwtService.generateToken(user);
return new LoginResponse(
token,
"Bearer",
jwtService.getExpirationMinutes(),
AuthUserResponse.from(user)
);
}
}

Some files were not shown because too many files have changed in this diff Show More