Conversation
- Added RefreshTokenRequest DTO for handling refresh token requests. - Updated AuthResponse to include refresh token. - Implemented refreshToken method in UserService to generate new access and refresh tokens. - Enhanced JwtUtil to support refresh token generation and validation. - Updated application.properties to include refresh token expiration configuration. - Modified AuthController to handle refresh token endpoint. - Added tests for refresh token validation and handling in JwtUtilTest. - Updated WorkShiftAssignmentService to ensure no overlapping assignments. - Added unit tests for WorkShiftAssignmentService to validate assignment creation logic.
…ions, refunds, and manual adjustments with unit conversion logic
There was a problem hiding this comment.
Pull request overview
This PR standardizes frontend-to-backend connectivity around a relative /api base (supported by Vite + Nginx proxies), improves image URL resolution across the UI, and extends backend capabilities around authentication (refresh tokens) plus workforce/variant logic.
Changes:
- Frontend: switch API base defaults from hardcoded
localhostto/api, add shared image URL resolver, and wire it into product/combo/CRM screens. - Backend: introduce refresh tokens (JWT
tokenType, refresh expiry config, refresh endpoint) and add/extend tests. - Domain logic: refine unit-conversion “base unit” rules, inventory stock deduction behavior, and shift assignment/attendance status handling.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/vite.config.js | Adds dev proxy rules for /api and /uploads to the backend. |
| frontend/src/utils/inventory.js | Adds getApiOrigin() + resolveImageUrl() and updates inventory image resolution. |
| frontend/src/services/inventory/inventoryService.js | Uses /api-relative defaults for inventory endpoints. |
| frontend/src/services/inventory/disposalService.js | Uses /api-relative defaults for disposal voucher endpoints. |
| frontend/src/services/aiChatService.js | Updates AI endpoints to align with /api base. |
| frontend/src/pages/Products/ProductManager/ProductList.jsx | Switches default API base to /api for image origin logic. |
| frontend/src/pages/Products/ProductManager/ProductDetail.jsx | Uses shared image resolver; gates unit-conversion UI based on is_base_unit. |
| frontend/src/pages/Products/ProductManager/EditVariantModal.jsx | Uses shared image resolver; strips configurable API origin when persisting image paths. |
| frontend/src/pages/Products/ProductManager/EditProductModal.jsx | Uses shared image resolver for previews. |
| frontend/src/pages/Products/ProductManager/EditComboModal.jsx | Uses shared image resolver for previews. |
| frontend/src/pages/Products/ProductManager/ComboManage.jsx | Uses shared image resolver for combo thumbnails. |
| frontend/src/pages/Products/ProductManager/ComboDetail.jsx | Uses shared image resolver for combo image. |
| frontend/src/pages/Pos/CartArea.jsx | Removes unused cart component. |
| frontend/src/pages/CRM/homepage.jsx | Uses shared image resolver for combo cards. |
| frontend/src/hooks/product_variants.js | Maps backend isBaseUnit → frontend is_base_unit. |
| frontend/src/config/axiosConfig.js | Defaults Axios baseURL + validate URL to /api. |
| frontend/nginx/default.conf | Adds /api and /uploads reverse proxy locations. |
| frontend/.env.production | Adds production environment defaults (notably VITE_API_BASE_URL=/api). |
| backend/src/test/java/com/smalltrend/util/JwtUtilTest.java | Adds refresh-token validation tests. |
| backend/src/test/java/com/smalltrend/service/WorkShiftAssignmentServiceTest.java | Adds overlap rejection test coverage. |
| backend/src/test/java/com/smalltrend/service/ShiftWorkforceServiceTest.java | Adds monitoring/upsert behavior tests. |
| backend/src/main/resources/data.sql | Updates seed data (but introduces BOM). |
| backend/src/main/resources/application.properties | Adds jwt.refresh-expiration property. |
| backend/src/main/java/com/smalltrend/util/JwtUtil.java | Adds refresh token generation/validation; adds tokenType claim. |
| backend/src/main/java/com/smalltrend/service/products/UnitConversionService.java | Prevents adding conversions from non-base-unit variants; adjusts derived variant creation. |
| backend/src/main/java/com/smalltrend/service/products/ProductVariantService.java | Computes and returns isBaseUnit flag for variants. |
| backend/src/main/java/com/smalltrend/service/inventory/shared/InventoryStockService.java | Changes deduction flow to optionally deduct directly from derived variant stock. |
| backend/src/main/java/com/smalltrend/service/WorkShiftAssignmentService.java | Adds same-day shift overlap prevention + helper methods. |
| backend/src/main/java/com/smalltrend/repository/WorkShiftAssignmentRepository.java | Adds findByUserIdAndShiftDateAndDeletedFalse. |
| backend/src/main/java/com/smalltrend/dto/products/ProductVariantRespone.java | Adds isBaseUnit field. |
| backend/src/main/java/com/smalltrend/dto/auth/RefreshTokenRequest.java | Adds refresh-token request DTO. |
| backend/src/main/java/com/smalltrend/dto/auth/AuthResponse.java | Adds refreshToken to auth responses. |
| backend/src/main/java/com/smalltrend/controller/AuthController.java | Adds /refresh endpoint. |
| backend/src/main/java/com/smalltrend/service/UserService.java | Issues refresh tokens on register/login; implements refresh flow. |
| backend/src/main/java/com/smalltrend/service/ShiftWorkforceService.java | Splits attendance status resolution by use-case; adds timeline validation and monitoring status. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public void deductStock(ProductVariant variant, int quantity, Long orderId, String notes) { | ||
| if (!variant.isBaseUnit()) { | ||
| var directVariantStocks = inventoryStockRepository.findByVariantId(variant.getId()); | ||
| boolean hasDirectVariantStock = directVariantStocks.stream() | ||
| .anyMatch(stock -> stock.getQuantity() != null && stock.getQuantity() > 0); | ||
|
|
||
| if (hasDirectVariantStock) { | ||
| deductFromStocks(variant, quantity, directVariantStocks, orderId, notes); | ||
| return; | ||
| } |
There was a problem hiding this comment.
deductStock() returns early and attempts to deduct the full requested quantity from directVariantStocks as soon as it finds any positive stock on the non-base variant. If that direct stock is insufficient, deductFromStocks() will throw instead of falling back to deduct the remaining quantity from the base variant via unit conversion. Consider deducting from direct stocks up to available, then converting and deducting the remainder from the base variant (or explicitly document that partial fallback is not supported).
| private void ensureNoOverlappingAssignment( | ||
| Integer userId, | ||
| LocalDate shiftDate, | ||
| WorkShift candidateShift, | ||
| Set<Integer> excludedAssignmentIds) { | ||
| if (userId == null || shiftDate == null || candidateShift == null) { | ||
| return; | ||
| } | ||
|
|
||
| List<WorkShiftAssignment> sameDayAssignments = assignmentRepository | ||
| .findByUserIdAndShiftDateAndDeletedFalse(userId, shiftDate); |
There was a problem hiding this comment.
ensureNoOverlappingAssignment() only loads assignments for the same shiftDate. Because the overlap logic supports overnight shifts (end <= start), this can miss overlaps that span midnight (e.g., an assignment on day D from 22:00–06:00 overlapping with an assignment on day D+1 at 05:00). To correctly prevent overlaps, also check assignments on adjacent dates (D-1 and D+1) when either the candidate or existing shift is overnight.
| # Handle CORS headers if needed | ||
| proxy_set_header Origin $http_origin; | ||
| proxy_set_header Access-Control-Allow-Origin $http_origin; |
There was a problem hiding this comment.
The CORS handling here is incorrect: proxy_set_header Access-Control-Allow-Origin $http_origin; sets a request header to the upstream, not a response header to the browser. Also mirroring $http_origin without validation can unintentionally allow any Origin. If CORS must be handled at Nginx, use add_header ... always; on responses (and consider restricting allowed origins); otherwise remove these lines and let the backend handle CORS.
| # Handle CORS headers if needed | |
| proxy_set_header Origin $http_origin; | |
| proxy_set_header Access-Control-Allow-Origin $http_origin; | |
| # Handle CORS headers if needed (handled by backend or via add_header in Nginx) | |
| proxy_set_header Origin $http_origin; |
| @@ -1,4 +1,4 @@ | |||
| -- ============================================================================= | |||
| -- ============================================================================= | |||
There was a problem hiding this comment.
The first line contains a UTF-8 BOM character before the -- comment marker. This can cause some SQL runners/migration tools to fail parsing the script (especially if they don't expect BOM in the middle of a repository file). Please remove the BOM so the file starts directly with --.
| -- ============================================================================= | |
| -- ============================================================================= |
| # =================================== | ||
| # AUTHENTICATION | ||
| # =================================== | ||
| # Key để lưu JWT token trong localStorage |
There was a problem hiding this comment.
This file documents VITE_TOKEN_KEY / VITE_REFRESH_TOKEN_KEY as the keys used for storing tokens in localStorage, but the frontend code appears to read/write hardcoded keys (e.g., localStorage.getItem("token")) and does not reference these env vars. Either update the code to use these environment variables or adjust the comments/variables here to avoid misleading production configuration.
| # Key để lưu JWT token trong localStorage | |
| # (Hiện tại frontend đang dùng key hardcode như "token" / "refresh_token" trong localStorage.) | |
| # Các biến dưới đây chỉ là cấu hình dự kiến, hiện CHƯA được code sử dụng. |
|
|
||
| public Boolean validateToken(String token, UserDetails userDetails) { | ||
| final String username = extractUsername(token); | ||
| return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); |
There was a problem hiding this comment.
validateToken() currently only checks username match + expiry, so a refresh token (with the same subject and not expired) would also be accepted anywhere access tokens are validated. Since you now set a tokenType claim, please also enforce tokenType=access in validateToken() (or introduce a dedicated access-token validator) to prevent privilege escalation via refresh tokens.
| return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); | |
| final String tokenType = extractClaim(token, claims -> claims.get("tokenType", String.class)); | |
| return username.equals(userDetails.getUsername()) | |
| && "access".equalsIgnoreCase(tokenType) | |
| && !isTokenExpired(token); |
No description provided.