From a96e36e409891c9a9d5871239c836271c9119e5e Mon Sep 17 00:00:00 2001 From: Sidharth Prabhu Date: Wed, 17 Jun 2026 21:02:49 +0530 Subject: [PATCH] Bill Bot 1 --- .DS_Store | Bin 8196 -> 10244 bytes backend/.DS_Store | Bin 0 -> 8196 bytes backend/src/.DS_Store | Bin 0 -> 6148 bytes backend/src/main/.DS_Store | Bin 0 -> 6148 bytes .../canteen/sales/config/SecurityConfig.java | 9 + .../sales/controller/DeviceLogController.java | 55 ++ .../sales/controller/OrderController.java | 5 + .../sales/controller/TerminalController.java | 121 ++++- .../sales/controller/UserController.java | 16 + .../sales/controller/WalletController.java | 3 + .../rit/canteen/sales/model/DeviceLog.java | 47 ++ .../com/rit/canteen/sales/model/Terminal.java | 19 + .../rit/canteen/sales/model/TerminalDTO.java | 20 +- .../sales/repository/DeviceLogRepository.java | 11 + .../sales/service/DeviceLogService.java | 30 ++ .../sales/service/DevicePairingService.java | 151 ++++++ .../src/main/resources/application.properties | 5 +- backend/terminal_api_spec.md | 327 ++++++++++++ frontend/package-lock.json | 468 +++++++++--------- frontend/src/components/LinkDeviceModal.tsx | 195 ++++++++ frontend/src/pages/Terminals.tsx | 129 ++++- ordering_site/package-lock.json | 440 ++++++++-------- .../src/components/FeedbackModal.tsx | 4 +- ordering_site/src/contexts/AuthContext.tsx | 37 +- ordering_site/src/contexts/FoodContext.tsx | 2 +- ordering_site/src/pages/CheckoutScreen.tsx | 18 +- ordering_site/src/pages/HomeScreen.tsx | 21 +- ordering_site/src/pages/ItemDetailScreen.tsx | 27 +- ordering_site/src/pages/MyOrdersScreen.tsx | 17 +- ordering_site/src/pages/StallDetailScreen.tsx | 4 +- 30 files changed, 1665 insertions(+), 516 deletions(-) create mode 100644 backend/.DS_Store create mode 100644 backend/src/.DS_Store create mode 100644 backend/src/main/.DS_Store create mode 100644 backend/src/main/java/com/rit/canteen/sales/controller/DeviceLogController.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/model/DeviceLog.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/repository/DeviceLogRepository.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/service/DeviceLogService.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java create mode 100644 backend/terminal_api_spec.md create mode 100644 frontend/src/components/LinkDeviceModal.tsx diff --git a/.DS_Store b/.DS_Store index b5a07990cce49f56f65b4c23015dcc9f58ea28fb..88b6450dba4a14b778ba4144affe4842900bfc24 100644 GIT binary patch delta 598 zcmZp1XbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~50$SAfkU^hRb*km37Lr!*vB!)zW zWQOd?I|P(iIoLV4IVRs12ovPs?UZBqfdavqRjJYG zMVTqV1&PU-dFjgbAAoKH0?x^`Vy0fo4gnCZvV%8-0d&7MgCj#QLkvS9Lj%KfhQ$o) z84fa>V|dK)mEk`lFQXWv5~DVw4x=ffIimxkBV!O_G-Co|;$&Z8RcW~Q7*ZMX7*fiD zi}G^v^U{GPGj6sLIKpf$$WQ?BKaflZD$-@hVaNxC2ZJ6%7DF+Ro#&a8pPZDFp9It- z0K}Vsc;4jC!k)^U3_%R8K&QDd_yTD?hFpdeRJDvslXXSZ*cc}QwM?6AC?c{sS7a9R z#)h>lg3Lf6AW+~260V@Q*;x3Uc{0CDpa>Hr#xy|Ej0_A0AbN6vOz-BCq5+JPi-ddV K?B>b2A}#=siGxD` delta 116 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD7Z0TH$S7`WF7&-%`(CVnJ3GO z+fD8l7iVK)11dYYd5!owW0lMQU3rvdiCHx;Keh(`KXkF@r0T<)6D!{XWn}=@7K<3-xL5yrd${S7ytkR zD@U^pn_~(e=k-WwQa$a61nmKQpiqE3#E=}()*YGw&46Y=GoTsJ4EzfW;4_;S;}_3; zIjURDfM(#oWPqO!7FLe7!m31ZbYLS=0K^vT76oO@1B8vG&{kNLC`{3(%pQm`B`U=r z(j3Pvu8wFctV)#TK%_YkH8W8e3K6rToh8(P*b;TC8PE(IWq{}IJ>Y@^9&8-S-yMiB zi!Vb7h9Lw7#hH+KZpd7;;F~Oib$DMCl(?72nD^(=M})6D+68zQa2-Zq3?>kd_wu&E zOABnm7TzLM4$np|H|QBPJ@o6LTxigL(V%}8b#~Ov#BU;N;zvt;WUUwrL;GRa#x^`W zli=Z19S_aOLIhre7LW57Ew>Vv6sPapQW(U^wQHYAQ*+Cy)2&9U(Pli%K4z6{;w0rb z<3!8+-2!8wTaz7kG4LySv-b|8i4#yi5(YuRN0pTa0WGjfhLvd{7PeVAV5E#x-W=H7 z9UZ+nWQ~oF>JUgZ09xflh7*|oLU zPjYqg5}(bBSQyT+I7k9UmsZ5=iN85R*BH*oS1?=RL-C>?#fV32a!XDWIkf0!b{>X~ z>xZ*6!;*w;<^qg@uUt%-EjeC5i|O0nB#EiCeML$pq-(qyy){W8NxxsWg}gu+_0ecf zj->7TO2vRdn1U>#lV!3>9I`I$ zn`$5NCFM^6iBV7=@m4mXA)`g?;o7J1h~V3)h}%9n{FN75w*D*MDh?BVlsI@W#0}bf z!`5l*=RfkHi)P>i8EBTi2YCK}_t*FTC%6LbS~H*-_}dJSmg(#?|2bcK{x>d&{~)%o tu43hd>s2KR6KuE&?-%Jf;?F+}QMS~Wt*|N)N3iIN06~LpGy{K>fgc41L*4)Y literal 0 HcmV?d00001 diff --git a/backend/src/.DS_Store b/backend/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2239b821a0d4f8d757ec24e67957c9dded7c341c GIT binary patch literal 6148 zcmeHK%T5$Q6ukw5^ngeMGl)@>j;qC)_Oo|$57=&2gJdq5;4 zv(^vr1N;CJS1$Dfbm`WGKj6kS-g*pA-MBD@^i8VHt^26zd-_pb06?5ZWfZ^!00W!A zR2O!?5#bl@jI?P>2O<%3%mut$3U~>}V!}ic1`-DTZwAD>n}Qm6-~&7L{yONpbs$9+ zaTr(HiR$`2|4^z2eiV<4d?jt|sg4VsMyJtjthvwljvIS%BXYcOMU0j>4@xcFD?Rq< zPSMQFa29($qoGXjD-=UkpZKi8cO2eel}P5g%3-99bkQ8z+Z!9Zo43X%M)&j9-qh4Y z-nug}xxb$_vbo#$7K>X!jqz8Cc7op}DLoon@EVoFdi?UXu7`YX)$`3XTNS0dx-a%z z>NWfN2l_I7*=%<3^3aw3tJjL=K*=j_RYP&5Gd$p-6EtYi?2WMAxiV)9A>GD03o(5W zyZ%db^`A;uq?M;frls9F#$=8Ra?*z(rEH}d`jz>OjflpDWBRgKtsL?oSm2Q#`@oSVVDLNR^d4`;SIcn zckl_mz<2lw2c(A#k{e{0Uz7G~Qfwsn`Liy>yP96ae zQ)rfgy8QJ=w1ffC*4R{t78IgV5mhSDM+~A*4n(D+-`4R>g(@A0Tp8!lD-(S}A^PA! zEEx{OR+#h&0|^5s8E98SQ|$i_@E!ic`Tt~6GD;Xo7&uo9kkqU@>tIQGZ#`3-*lR6p t>)3>dxTZpxf}P%ur6RWCA#766X9)$-*4R{t7L?>iK+=$O2?PI>fnOo^-sb=S literal 0 HcmV?d00001 diff --git a/backend/src/main/.DS_Store b/backend/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ee131149481864953f08a4e73486dd3740bac058 GIT binary patch literal 6148 zcmeHKO;6N77=8y9>4HdsUBswK<6cbIC>}e}IW6kNN}j=+%RNz>8;m=c7SBj5k6|-(==_XWn^c=6#xWIsibddSw(q2LKHV z$4nPiXBdz3a?09NPX{95bIb*->;}xmHlHxHRDo21i>&~EcQddJ4m7|y@%~yEyK^W^ zB$0&>Jcxin9lbL5PcT0Go8dO2n)*h3bZdbh#Umr%NLzcR<5H*AsdZ}`_H)*>V<)ah zmJ_b=))HfZoA{2q?0e0Uo||Dbc6{oEg5Xy?9J2n@rxn(;Se;fP!L?;T%WBz@KD56- zHg>;gj8BXn6pj6+k|v@D|>|d-x1r;RpPJL()SA$t^NWie#Kj5|d1mMY2XVi9@y}zb$#( zH`RO!NQBNAkGo-qG+?xf(S&)Nc;Q=W+gkU9-)XwRRDo21^HzX=9}FxUO@%Fq;?seZ zxB?(%uv-}V;@2P1;2nsj!j?qrK_iliD5*qUF^IZ35J|_lsm8Y?N;(j+GR~t`ChCSl z)Ww4sVmJ^}Vp^vPqzW8Ypj|d~e*QoD`~CkoDQ%?+qze333XsgKJ!@e~>TErg96xIf uESp%k@o_DQf`XMgj)lXI;vp=;FlKNE(Nx%yh&^cOkARRNtx^TfRe|4?iRZNd literal 0 HcmV?d00001 diff --git a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java index 3197ba76..108c856c 100644 --- a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java +++ b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java @@ -51,8 +51,13 @@ public class SecurityConfig { // ── PUBLIC: Terminal hardware order lookup (auth via X-API-KEY header, not JWT) ── .requestMatchers(HttpMethod.GET, "/api/terminals/orders/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/terminals/validate").permitAll() + .requestMatchers(HttpMethod.POST, "/api/terminals/pair").permitAll() .requestMatchers(HttpMethod.POST, "/api/terminals/*/verify-pin").permitAll() + // ── PUBLIC: Device log ingestion (ESP32 Bill-Bot devices, no JWT) ── + .requestMatchers(HttpMethod.POST, "/api/device-logs").permitAll() + // ── PUBLIC: Notifications read (admin frontend polls this before login guard kicks in) ── .requestMatchers(HttpMethod.GET, "/api/notifications/**").permitAll() @@ -66,10 +71,14 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/api/orders/user/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/wallet/balance/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/wallet/transactions/**").authenticated() + .requestMatchers(HttpMethod.POST, "/api/wallet/topup").authenticated() .requestMatchers(HttpMethod.POST, "/api/coupons/redeem").authenticated() .requestMatchers(HttpMethod.POST, "/api/feedback/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/feedback/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/auth/user/**").authenticated() + .requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated() // ── STAFF/MANAGER/MASTER: All other management APIs ── .requestMatchers("/api/**").hasAnyRole("MASTER", "MANAGER", "STAFF", "OPERATOR") diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/DeviceLogController.java b/backend/src/main/java/com/rit/canteen/sales/controller/DeviceLogController.java new file mode 100644 index 00000000..3aa96ce4 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/DeviceLogController.java @@ -0,0 +1,55 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.DeviceLog; +import com.rit.canteen.sales.service.DeviceLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/device-logs") +public class DeviceLogController { + + @Autowired + private DeviceLogService deviceLogService; + + /** + * POST /api/device-logs + * Accepts JSON: { "device_id": "...", "message": "..." } + * Called by ESP32 Bill-Bot devices to log events. + */ + @PostMapping + public ResponseEntity createLog(@RequestBody Map body) { + String deviceId = body.get("device_id"); + String message = body.get("message"); + + if (deviceId == null || message == null) { + return ResponseEntity.badRequest().build(); + } + + deviceLogService.saveLog(deviceId, message); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + /** + * GET /api/device-logs + * Returns all device logs (newest first) for admin dashboard. + */ + @GetMapping + public List getAllLogs() { + return deviceLogService.getAllLogs(); + } + + /** + * GET /api/device-logs/{deviceId} + * Returns logs for a specific device. + */ + @GetMapping("/{deviceId}") + public List getLogsByDevice(@PathVariable String deviceId) { + return deviceLogService.getLogsByDeviceId(deviceId); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java index ba82e716..99ab089b 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -256,6 +256,11 @@ public class OrderController { public ResponseEntity updateOrder(@PathVariable Long id, @RequestBody Order updatedOrder) { try { return orderRepository.findById(id).map(existingOrder -> { + // ── SECURITY: Verify ownership of order ── + Long tokenUserId = getTokenUserId(); + if (tokenUserId != null && !tokenUserId.equals(existingOrder.getUserId()) && !isStaff()) { + return ResponseEntity.status(403).body(Map.of("error", "Access denied")); + } BigDecimal oldAmount = existingOrder.getTotalAmount(); BigDecimal newAmount = updatedOrder.getTotalAmount(); diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java index 5751bb4e..fa435df0 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java @@ -3,6 +3,7 @@ package com.rit.canteen.sales.controller; import com.rit.canteen.sales.model.Order; import com.rit.canteen.sales.model.Terminal; import com.rit.canteen.sales.model.TerminalDTO; +import com.rit.canteen.sales.service.DevicePairingService; import com.rit.canteen.sales.service.TerminalService; import com.rit.canteen.sales.repository.TerminalRepository; import com.rit.canteen.sales.repository.OrderRepository; @@ -22,6 +23,9 @@ public class TerminalController { @Autowired private TerminalService terminalService; + @Autowired + private DevicePairingService devicePairingService; + @Autowired private TerminalRepository terminalRepository; @@ -36,7 +40,10 @@ public class TerminalController { t.getName(), t.getLocation(), "********", - "****" + "****", + t.isPaired(), + t.getDeviceId() != null ? maskDeviceId(t.getDeviceId()) : null, + t.getPairedAt() )) .toList(); } @@ -64,6 +71,10 @@ public class TerminalController { return ResponseEntity.noContent().build(); } + // ────────────────────────────────────────────────────────────── + // Order Lookup (ESP32 uses X-API-KEY) + // ────────────────────────────────────────────────────────────── + @GetMapping("/orders/{orderNumber}") public ResponseEntity getOrderForTerminal( @PathVariable String orderNumber, @@ -97,4 +108,112 @@ public class TerminalController { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found")); } } + + @GetMapping("/validate") + public ResponseEntity validateApiKey(@RequestHeader("X-API-KEY") String apiKey) { + Optional terminal = terminalRepository.findByApiKey(apiKey); + if (terminal.isEmpty()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("status", "INVALID", "message", "Invalid API Key")); + } + Terminal t = terminal.get(); + System.out.println(">>> Terminal Connected & Validated: " + t.getName() + " [ID: " + t.getId() + ", Location: " + t.getLocation() + "]"); + return ResponseEntity.ok(Map.of( + "status", "VALID", + "terminalId", t.getId(), + "name", t.getName(), + "location", t.getLocation() + )); + } + + // ────────────────────────────────────────────────────────────── + // OTP-Based Device Pairing + // ────────────────────────────────────────────────────────────── + + /** + * POST /api/terminals/pair + * Called by ESP32 during setup. The device sends its OTP and chipId. + * If pairing is already completed (admin linked it), returns the apiKey. + * Otherwise returns status "WAITING". + * + * No auth required — the device has no credentials yet. + */ + @PostMapping("/pair") + public ResponseEntity registerDeviceOtp(@RequestBody Map body) { + String otp = body.get("otp"); + String deviceId = body.get("deviceId"); + + if (otp == null || otp.isBlank() || deviceId == null || deviceId.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("message", "Both 'otp' and 'deviceId' are required")); + } + + Map result = devicePairingService.registerOtp(otp, deviceId); + return ResponseEntity.ok(result); + } + + /** + * POST /api/terminals/{id}/link-device + * Called by admin dashboard. Admin enters the OTP shown on the ESP32 screen. + * Links the device to this terminal record. + * + * Requires JWT auth (admin role). + */ + @PostMapping("/{id}/link-device") + public ResponseEntity linkDevice(@PathVariable Long id, @RequestBody Map body) { + String otp = body.get("otp"); + + if (otp == null || otp.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("message", "OTP is required")); + } + + Optional result = devicePairingService.linkDevice(id, otp); + if (result.isPresent()) { + Terminal t = result.get(); + return ResponseEntity.ok(Map.of( + "message", "Device linked successfully", + "terminalId", t.getId(), + "name", t.getName(), + "deviceId", t.getDeviceId() + )); + } else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", "Invalid or expired OTP. Please check the code on the device screen and try again.")); + } + } + + /** + * POST /api/terminals/{id}/unpair + * Called by admin dashboard to disconnect a device from a terminal. + * The device will need to re-pair on next boot. + */ + @PostMapping("/{id}/unpair") + public ResponseEntity unpairDevice(@PathVariable Long id) { + Optional result = devicePairingService.unpairDevice(id); + if (result.isPresent()) { + return ResponseEntity.ok(Map.of("message", "Device unpaired successfully")); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("message", "Terminal not found")); + } + } + + // ────────────────────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────────────────────── + + /** + * Masks a device ID for display: "Esp 32 - AABBCCDDEEFF" → "Esp 32 - AA••••••EEFF" + */ + private String maskDeviceId(String deviceId) { + if (deviceId == null || deviceId.length() < 8) return deviceId; + // Find the last part (MAC portion) of the device ID + int dashIndex = deviceId.lastIndexOf('-'); + if (dashIndex >= 0 && dashIndex + 5 < deviceId.length()) { + String prefix = deviceId.substring(0, dashIndex + 1).trim(); + String mac = deviceId.substring(dashIndex + 1).trim(); + if (mac.length() >= 6) { + return prefix + " " + mac.substring(0, 2) + "••••" + mac.substring(mac.length() - 4); + } + } + return deviceId.substring(0, 4) + "••••" + deviceId.substring(deviceId.length() - 4); + } } diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java b/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java index 9df3ee30..00dcf7ba 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java @@ -128,6 +128,11 @@ public class UserController { public ResponseEntity updateUser( @PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) { + // Extra ownership check: customers can only update their own profile; staff can update any + Long tokenUserId = getTokenUserId(); + if (tokenUserId != null && !tokenUserId.equals(id) && !isStaff()) { + return ResponseEntity.status(403).build(); + } LoginResponse.UserDto updated = userService.updateUser(id, request.getName(), request.getMobileNumber(), request.getPin()); return updated != null ? ResponseEntity.ok(updated) : ResponseEntity.notFound().build(); @@ -147,6 +152,17 @@ public class UserController { // ── Helpers ────────────────────────────────────────────────────────────── + private Long getTokenUserId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getDetails() instanceof Claims claims) { + Object uid = claims.get("userId"); + if (uid != null) { + return uid instanceof Integer ? ((Integer) uid).longValue() : (Long) uid; + } + } + return null; + } + private String getAuthenticatedMobile() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getDetails() instanceof Claims claims) { diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java index 9ae4a135..48e6f0c0 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java @@ -74,6 +74,9 @@ public class WalletController { public ResponseEntity topUp(@RequestBody Map request) { try { Long userId = Long.valueOf(request.get("userId").toString()); + if (!canAccessUser(userId)) { + return ResponseEntity.status(403).body(Map.of("error", "Access denied")); + } BigDecimal amount = new BigDecimal(request.get("amount").toString()); // ── FIX: validate amount BEFORE touching the database ── diff --git a/backend/src/main/java/com/rit/canteen/sales/model/DeviceLog.java b/backend/src/main/java/com/rit/canteen/sales/model/DeviceLog.java new file mode 100644 index 00000000..c94b1876 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/DeviceLog.java @@ -0,0 +1,47 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "device_logs") +public class DeviceLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "device_id", nullable = false) + private String deviceId; + + @Column(nullable = false, length = 2000) + private String message; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } + + public DeviceLog() {} + + public DeviceLog(String deviceId, String message) { + this.deviceId = deviceId; + this.message = message; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getDeviceId() { return deviceId; } + public void setDeviceId(String deviceId) { this.deviceId = deviceId; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java b/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java index 76d8c994..e7fc7786 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java @@ -2,6 +2,7 @@ package com.rit.canteen.sales.model; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; @Entity @Table(name = "terminals") @@ -23,6 +24,15 @@ public class Terminal { @Column(unique = true) private String apiKey; + @Column(name = "device_id") + private String deviceId; + + @Column(nullable = false, columnDefinition = "boolean default false") + private boolean paired = false; + + @Column(name = "paired_at") + private LocalDateTime pairedAt; + public Terminal() {} public Terminal(String name, String location, String pin, String apiKey) { @@ -47,4 +57,13 @@ public class Terminal { public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } + + public String getDeviceId() { return deviceId; } + public void setDeviceId(String deviceId) { this.deviceId = deviceId; } + + public boolean isPaired() { return paired; } + public void setPaired(boolean paired) { this.paired = paired; } + + public LocalDateTime getPairedAt() { return pairedAt; } + public void setPairedAt(LocalDateTime pairedAt) { this.pairedAt = pairedAt; } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java b/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java index ca4ad9e2..925494a7 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java @@ -1,20 +1,29 @@ package com.rit.canteen.sales.model; +import java.time.LocalDateTime; + public class TerminalDTO { private Long id; private String name; private String location; private String apiKey; private String pin; + private boolean paired; + private String deviceId; + private LocalDateTime pairedAt; public TerminalDTO() {} - public TerminalDTO(Long id, String name, String location, String apiKey, String pin) { + public TerminalDTO(Long id, String name, String location, String apiKey, String pin, + boolean paired, String deviceId, LocalDateTime pairedAt) { this.id = id; this.name = name; this.location = location; this.apiKey = apiKey; this.pin = pin; + this.paired = paired; + this.deviceId = deviceId; + this.pairedAt = pairedAt; } // Getters and Setters @@ -32,4 +41,13 @@ public class TerminalDTO { public String getPin() { return pin; } public void setPin(String pin) { this.pin = pin; } + + public boolean isPaired() { return paired; } + public void setPaired(boolean paired) { this.paired = paired; } + + public String getDeviceId() { return deviceId; } + public void setDeviceId(String deviceId) { this.deviceId = deviceId; } + + public LocalDateTime getPairedAt() { return pairedAt; } + public void setPairedAt(LocalDateTime pairedAt) { this.pairedAt = pairedAt; } } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/DeviceLogRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/DeviceLogRepository.java new file mode 100644 index 00000000..966bbb4a --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/DeviceLogRepository.java @@ -0,0 +1,11 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.DeviceLog; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface DeviceLogRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + List findByDeviceIdOrderByCreatedAtDesc(String deviceId); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DeviceLogService.java b/backend/src/main/java/com/rit/canteen/sales/service/DeviceLogService.java new file mode 100644 index 00000000..f36812f1 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/DeviceLogService.java @@ -0,0 +1,30 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.DeviceLog; +import com.rit.canteen.sales.repository.DeviceLogRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class DeviceLogService { + + @Autowired + private DeviceLogRepository deviceLogRepository; + + @Transactional + public DeviceLog saveLog(String deviceId, String message) { + DeviceLog log = new DeviceLog(deviceId, message); + return deviceLogRepository.save(log); + } + + public List getAllLogs() { + return deviceLogRepository.findAllByOrderByCreatedAtDesc(); + } + + public List getLogsByDeviceId(String deviceId) { + return deviceLogRepository.findByDeviceIdOrderByCreatedAtDesc(deviceId); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java b/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java new file mode 100644 index 00000000..b0b53b19 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java @@ -0,0 +1,151 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.Terminal; +import com.rit.canteen.sales.repository.TerminalRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages the in-memory OTP pairing lifecycle for ESP32 terminal devices. + * + * Flow: + * 1. Device boots, generates a 6-digit OTP, displays it on screen + * 2. Device calls registerOtp() — stores OTP + deviceId in memory (5-min TTL) + * 3. Admin sees OTP on device screen, enters it in dashboard against a terminal + * 4. Admin calls linkDevice() — matches OTP, binds device to terminal, returns apiKey + * 5. Device polls checkPairingStatus() — gets back apiKey once admin completes step 4 + */ +@Service +public class DevicePairingService { + + private static final long OTP_TTL_MS = 5 * 60 * 1000L; // 5 minutes + + @Autowired + private TerminalRepository terminalRepository; + + /** + * Represents a pending pairing request from a device. + */ + private static class PairingRequest { + final String otp; + final String deviceId; + final Instant createdAt; + // Set once admin links the device + volatile String apiKey; + volatile Long terminalId; + volatile boolean completed; + + PairingRequest(String otp, String deviceId) { + this.otp = otp; + this.deviceId = deviceId; + this.createdAt = Instant.now(); + this.completed = false; + } + + boolean isExpired() { + return Instant.now().toEpochMilli() - createdAt.toEpochMilli() > OTP_TTL_MS; + } + } + + // OTP → PairingRequest + private final ConcurrentHashMap pendingPairings = new ConcurrentHashMap<>(); + + /** + * Called by the ESP32 device. Registers (or refreshes) the device's OTP in memory. + * If the OTP was already matched by an admin, returns the assigned apiKey. + * + * @return Map with status ("WAITING" or "PAIRED") and optionally "apiKey" + */ + public Map registerOtp(String otp, String deviceId) { + PairingRequest existing = pendingPairings.get(otp); + + // If this OTP was already linked by admin, return the apiKey + if (existing != null && existing.completed && existing.deviceId.equals(deviceId)) { + // Clean up after delivering the key + pendingPairings.remove(otp); + return Map.of( + "status", "PAIRED", + "apiKey", existing.apiKey, + "terminalId", existing.terminalId + ); + } + + // Register or refresh the OTP + pendingPairings.put(otp, new PairingRequest(otp, deviceId)); + + return Map.of("status", "WAITING"); + } + + /** + * Called by the admin dashboard. Links a device's OTP to a specific terminal. + * + * @return Optional.empty() if OTP not found/expired, or the updated Terminal + */ + public Optional linkDevice(Long terminalId, String otp) { + PairingRequest request = pendingPairings.get(otp); + + if (request == null || request.isExpired()) { + pendingPairings.remove(otp); // Clean up expired + return Optional.empty(); + } + + Optional terminalOpt = terminalRepository.findById(terminalId); + if (terminalOpt.isEmpty()) { + return Optional.empty(); + } + + Terminal terminal = terminalOpt.get(); + + // Bind the device to the terminal + terminal.setDeviceId(request.deviceId); + terminal.setPaired(true); + terminal.setPairedAt(LocalDateTime.now()); + terminalRepository.save(terminal); + + // Mark the pairing as completed so the device can pick up the apiKey + request.apiKey = terminal.getApiKey(); + request.terminalId = terminal.getId(); + request.completed = true; + + System.out.println(">>> Device paired: " + request.deviceId + + " → Terminal: " + terminal.getName() + " [ID: " + terminal.getId() + "]"); + + return Optional.of(terminal); + } + + /** + * Unpairs a device from a terminal, clearing its deviceId and paired status. + */ + public Optional unpairDevice(Long terminalId) { + Optional terminalOpt = terminalRepository.findById(terminalId); + if (terminalOpt.isEmpty()) { + return Optional.empty(); + } + + Terminal terminal = terminalOpt.get(); + terminal.setDeviceId(null); + terminal.setPaired(false); + terminal.setPairedAt(null); + terminalRepository.save(terminal); + + System.out.println(">>> Device unpaired from Terminal: " + terminal.getName() + + " [ID: " + terminal.getId() + "]"); + + return Optional.of(terminal); + } + + /** + * Periodic cleanup of expired OTP entries (runs every 60 seconds). + */ + @Scheduled(fixedRate = 60000) + public void cleanupExpiredOtps() { + pendingPairings.entrySet().removeIf(entry -> entry.getValue().isExpired()); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 153f1678..66e5bf72 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,13 +1,14 @@ spring.application.name=backend server.address=0.0.0.0 +server.port=8080 # ============================================================ # DATABASE — REQUIRED environment variables # Set these in your environment or a .env file # ============================================================ spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/positeasy} -spring.datasource.username=${DB_USER:postgres} -spring.datasource.password=${DB_PASSWORD:} +spring.datasource.username=postgres +spring.datasource.password=sidharth spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=update diff --git a/backend/terminal_api_spec.md b/backend/terminal_api_spec.md new file mode 100644 index 00000000..1f1b8aa4 --- /dev/null +++ b/backend/terminal_api_spec.md @@ -0,0 +1,327 @@ +# Terminal Integration & ESP32 Firmware API Specification + +This document provides the API specifications of the Spring Boot backend for integrating terminal hardware (ESP32 controllers that scan QR codes and print bills). + +--- + +## 1. Authentication + +### For Paired Devices +Once a device is paired, all terminal-specific APIs use **API Key authentication**. +- **Header Key**: `X-API-KEY` +- **Value**: `POS-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` (Auto-generated when a terminal is created). + +### For Unpaired Devices (During Pairing) +The pairing endpoint requires no authentication — the device has no credentials yet. + +--- + +## 2. Device Onboarding (OTP-Based Pairing) + +### Flow Overview + +1. **Admin** creates a terminal in the dashboard (name, location, PIN) +2. **ESP32** boots with no stored API key → enters pairing mode +3. **ESP32** generates a 6-digit OTP and displays it on screen +4. **ESP32** POSTs the OTP + its chipId to `POST /api/terminals/pair` +5. **Admin** enters the OTP in the dashboard against the specific terminal +6. **Backend** matches the OTP, binds the device to the terminal +7. **ESP32** polls `POST /api/terminals/pair` again → receives `"PAIRED"` + API key +8. **ESP32** saves the API key to NVS flash and restarts +9. All subsequent communication uses `X-API-KEY` header — no further pairing needed + +### A. Register Device OTP (ESP32 → Backend) + +Used by the ESP32 to register its OTP and poll for pairing completion. + +* **URL**: `/api/terminals/pair` +* **Method**: `POST` +* **Auth**: None (public endpoint) +* **Headers**: + * `Content-Type`: `application/json` +* **Request Body**: +```json +{ + "otp": "482916", + "deviceId": "Esp 32 - AABBCCDDEEFF" +} +``` + +#### Response — Waiting (Admin hasn't linked yet): +```json +{ + "status": "WAITING" +} +``` + +#### Response — Paired (Admin completed linking): +```json +{ + "status": "PAIRED", + "apiKey": "POS-EF9832B743CA90B2381F0A1B2C3D4E5F", + "terminalId": 1 +} +``` + +* **Response Codes**: + * `200 OK`: Always — check `status` field for state. + * `400 BAD_REQUEST`: Missing `otp` or `deviceId`. + +--- + +### B. Link Device (Admin Dashboard → Backend) + +Used by the admin to match a device's OTP to a terminal. + +* **URL**: `/api/terminals/{id}/link-device` +* **Method**: `POST` +* **Auth**: JWT (admin role) +* **Headers**: + * `Content-Type`: `application/json` + * `Authorization`: `Bearer ` +* **Path Parameters**: + * `id` (Long): The terminal ID to link the device to. +* **Request Body**: +```json +{ + "otp": "482916" +} +``` + +#### Response (`200 OK`): +```json +{ + "message": "Device linked successfully", + "terminalId": 1, + "name": "Counter 1 Printer", + "deviceId": "Esp 32 - AABBCCDDEEFF" +} +``` + +* **Response Codes**: + * `200 OK`: Device linked successfully. + * `400 BAD_REQUEST`: Invalid or expired OTP. + +--- + +### C. Unpair Device (Admin Dashboard → Backend) + +Used by the admin to disconnect a device from a terminal. + +* **URL**: `/api/terminals/{id}/unpair` +* **Method**: `POST` +* **Auth**: JWT (admin role) + +#### Response (`200 OK`): +```json +{ + "message": "Device unpaired successfully" +} +``` + +--- + +## 3. Operational Endpoints + +### D. Get Order Details + +Used by the ESP32 to fetch the order details after scanning a QR code. + +* **URL**: `/api/terminals/orders/{orderNumber}` +* **Method**: `GET` +* **Headers**: + * `X-API-KEY`: `` +* **Path Parameters**: + * `orderNumber` (String): The order ID scanned from the QR code (e.g., `ORD-87A3B2D9`). + +#### Response Codes: +* `200 OK`: Order found and is eligible for processing. +* `401 UNAUTHORIZED`: Invalid or missing `X-API-KEY`. +* `404 NOT_FOUND`: Order number does not exist. +* `400 BAD_REQUEST`: Order has already been fulfilled (`status` is `COMPLETED`). +* `410 GONE`: Order has expired/archived. + +#### Response Body (`200 OK` JSON Schema): +```json +{ + "id": 12, + "orderNumber": "ORD-68EF73C9", + "displayOrderId": "045", + "userId": 5, + "totalAmount": 250.00, + "status": "PAID", + "paymentMethod": "RITZ_TOKEN", + "createdAt": "2026-06-17T13:30:00", + "orderType": "STORE_ORDER", + "archived": false, + "hasFeedback": false, + "items": [ + { + "id": 24, + "productId": 3, + "productName": "Masala Dosa", + "price": 60.00, + "quantity": 2, + "stallId": 1, + "stallName": "RIT Canteen" + }, + { + "id": 25, + "productId": 7, + "productName": "Cold Coffee", + "price": 40.00, + "quantity": 1, + "stallId": 2, + "stallName": "Juice Bar" + } + ] +} +``` + +--- + +### E. Validate API Key + +Used by the ESP32 to verify its API key is still valid (optional health check). + +* **URL**: `/api/terminals/validate` +* **Method**: `GET` +* **Headers**: + * `X-API-KEY`: `` + +#### Response (`200 OK`): +```json +{ + "status": "VALID", + "terminalId": 1, + "name": "Counter 1 Printer", + "location": "Main Hall" +} +``` + +--- + +### F. Device Logging + +Used by the ESP32 to submit runtime system logs to the backend. + +* **URL**: `/api/device-logs` +* **Method**: `POST` +* **Headers**: + * `Content-Type`: `application/json` +* **Request Body**: +```json +{ + "device_id": "Esp 32 - AABBCCDDEEFF", + "message": "Firmware Version: 1.2.6" +} +``` +* **Response Codes**: + * `201 CREATED`: Log successfully recorded. + * `400 BAD_REQUEST`: Missing `device_id` or `message`. + +--- + +### G. Verify Terminal PIN (Admin Tool) + +Used during admin operations to reveal the terminal's API Key. + +* **URL**: `/api/terminals/{id}/verify-pin` +* **Method**: `POST` +* **Headers**: + * `Content-Type`: `application/json` +* **Path Parameters**: + * `id` (Long): The database ID of the terminal registration. +* **Request Body**: +```json +{ + "pin": "1234" +} +``` +* **Response Body (`200 OK`):** +```json +{ + "id": 1, + "name": "Counter 1 Printer", + "location": "Main Hall", + "pin": "1234", + "apiKey": "POS-EF9832B743CA90B2381F", + "deviceId": "Esp 32 - AABBCCDDEEFF", + "paired": true, + "pairedAt": "2026-06-17T14:00:00" +} +``` + +--- + +## 4. ESP32 Firmware Integration Guide + +### Pairing Flow (`database.cpp`) + +The ESP32 now uses OTP-based pairing instead of JWT token polling: + +```cpp +void setupPairing() +{ + // 1. Generate 6-digit OTP + currentOtp = generateUniqueOTP(oldOtp); + displayStatus("OTP: " + currentOtp + "\nEnter on Admin Panel", 300); + + // 2. POST OTP + deviceId to /api/terminals/pair + JsonDocument requestDoc; + requestDoc["otp"] = currentOtp; + requestDoc["deviceId"] = getDeviceId(); + + // 3. Poll every 3 seconds — check if status == "PAIRED" + // 4. On success, extract apiKey and save to NVS + apiKey = doc["apiKey"].as(); + saveApiKey(apiKey); + ESP.restart(); +} +``` + +### Data Fetching (`database.cpp`) + +All requests use `X-API-KEY` instead of `Authorization: Bearer`: + +```cpp +String getData(String orderNumber) +{ + httpClient.addHeader("X-API-KEY", apiKey); // Not Bearer token + // ... GET /api/terminals/orders/{orderNumber} +} +``` + +### Order JSON Parsing (`bill_bot_system.cpp`) + +The new backend returns order details at the **root level** (no `"data"` wrapper): + +| Old Path | New Path | +|---|---| +| `doc["data"]["orderId"]` | `doc["displayOrderId"]` or `doc["orderNumber"]` | +| `doc["data"]["status"]` | `doc["status"]` (`"PAID"` = ready, `"COMPLETED"` = already done) | +| `doc["data"]["dateTz"]` | `doc["createdAt"]` | +| `doc["data"]["order"]` (items) | `doc["items"]` | +| `item["productInfo"]["name"]` | `item["productName"]` | +| `item["counterId"]` | `item["stallId"]` / `item["stallName"]` | +| `item["price"]` (in paise) | `item["price"]` (in rupees, no /100 needed) | + +### Device Logging (`database.cpp`) + +Logs now go to the Spring Boot backend directly (no Supabase): + +```cpp +void sendLog(String message) +{ + httpClient.begin(logUrl); // /api/device-logs + httpClient.addHeader("Content-Type", "application/json"); + // No supabaseApiKey header needed + httpClient.POST(body); +} +``` + +### QR Code Format + +The QR code payload is now the **order number directly**: +- Old: `prefix$PAYMENT_ID$suffix` → parsed with `$` delimiters +- New: `ORD-87A3B2D9` → passed directly to `getData()` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e7a7ca7..93e7c01e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -56,13 +56,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -71,9 +71,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -81,21 +81,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -112,14 +112,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -129,14 +129,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -146,9 +146,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -156,29 +156,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -188,9 +188,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -198,9 +198,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -208,9 +208,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -218,27 +218,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -257,33 +257,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -291,23 +291,23 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -317,9 +317,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -598,14 +598,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -617,9 +617,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.123.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz", - "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -663,9 +663,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz", - "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -680,9 +680,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz", - "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -697,9 +697,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz", - "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -714,9 +714,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz", - "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -731,9 +731,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz", - "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -748,13 +748,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz", - "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -765,13 +768,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz", - "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -782,13 +788,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz", - "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -799,13 +808,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz", - "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -816,13 +828,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz", - "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -833,13 +848,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz", - "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -850,9 +868,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz", - "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -867,9 +885,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz", - "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -877,52 +895,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.1", - "@emnapi/runtime": "1.9.1", - "@napi-rs/wasm-runtime": "^1.1.2" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz", - "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -937,9 +921,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz", - "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1259,9 +1243,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -1607,9 +1591,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2245,9 +2229,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", - "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", + "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -2855,10 +2839,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3313,9 +3307,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3462,9 +3456,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -3482,7 +3476,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3588,9 +3582,9 @@ } }, "node_modules/react-router": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", - "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3610,12 +3604,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", - "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", "license": "MIT", "dependencies": { - "react-router": "7.14.0" + "react-router": "7.18.0" }, "engines": { "node": ">=20.0.0" @@ -3704,14 +3698,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz", - "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.123.0", - "@rolldown/pluginutils": "1.0.0-rc.13" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3720,27 +3714,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.13", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.13", - "@rolldown/binding-darwin-x64": "1.0.0-rc.13", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.13", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", - "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -3893,14 +3887,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -4069,17 +4063,17 @@ } }, "node_modules/vite": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz", - "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.13", - "tinyglobby": "^0.2.15" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -4095,7 +4089,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/frontend/src/components/LinkDeviceModal.tsx b/frontend/src/components/LinkDeviceModal.tsx new file mode 100644 index 00000000..a59386df --- /dev/null +++ b/frontend/src/components/LinkDeviceModal.tsx @@ -0,0 +1,195 @@ +import { apiFetch } from '../api'; +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Wifi, CheckCircle2, AlertCircle, Smartphone } from 'lucide-react'; +import Numpad from './Numpad.tsx'; + +interface LinkDeviceModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + terminalId: number | null; + terminalName: string; +} + +const LinkDeviceModal: React.FC = ({ + isOpen, + onClose, + onSuccess, + terminalId, + terminalName +}) => { + const [otp, setOtp] = useState(''); + const [success, setSuccess] = useState(false); + const [linkedDeviceId, setLinkedDeviceId] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleLink = async (freshOtp?: string) => { + if (!terminalId) return; + const otpToSubmit = freshOtp || otp; + if (!otpToSubmit || otpToSubmit.length < 6) return; + + setLoading(true); + setError(null); + try { + const response = await apiFetch(`/api/terminals/${terminalId}/link-device`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ otp: otpToSubmit }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess(true); + setLinkedDeviceId(data.deviceId || null); + onSuccess(); + } else { + setError(data.message || 'Failed to link device'); + setOtp(''); + } + } catch (err) { + setError('Connection error. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setOtp(''); + setSuccess(false); + setLinkedDeviceId(null); + setError(null); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+ +
+

Link Physical Device

+ +
+ +
+ + {!success ? ( + +
+ +
+
+

{terminalName}

+

+ Enter the 6-digit OTP shown on the device screen +

+
+ + {/* Step-by-step instructions */} +
+
+ 1 +

Power on the ESP32 device and connect it to Wi-Fi

+
+
+ 2 +

A 6-digit OTP will appear on the device display

+
+
+ 3 +

Enter that OTP below to link the device

+
+
+ + {error && ( + + + {error} + + )} + + { + setOtp(v); + if (v.length === 6) { + setTimeout(() => handleLink(v), 100); + } + }} maxLength={6} /> + + +
+ ) : ( + +
+ +
+ +
+

Device Linked Successfully!

+

+ The physical device is now paired with {terminalName} +

+
+ + {linkedDeviceId && ( +
+
+ + {linkedDeviceId} +
+
+ )} + +
+ +

+ The device will automatically receive its API key and restart. It's now ready to scan QR codes and print bills. +

+
+ + +
+ )} +
+
+
+
+ ); +}; + +export default LinkDeviceModal; diff --git a/frontend/src/pages/Terminals.tsx b/frontend/src/pages/Terminals.tsx index 6ffbec9c..26c12978 100644 --- a/frontend/src/pages/Terminals.tsx +++ b/frontend/src/pages/Terminals.tsx @@ -1,4 +1,4 @@ -import { apiFetch } from '../api'; +import { apiFetch } from '../api'; import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { @@ -10,16 +10,25 @@ import { Search, ExternalLink, Info, - Key + Key, + Wifi, + WifiOff, + LinkIcon, + Unlink, + Smartphone } from 'lucide-react'; import AddTerminalModal from '../components/AddTerminalModal.tsx'; import PinVerificationModal from '../components/PinVerificationModal.tsx'; +import LinkDeviceModal from '../components/LinkDeviceModal.tsx'; interface Terminal { id: number; name: string; location: string; apiKey: string; + paired: boolean; + deviceId: string | null; + pairedAt: string | null; } const Terminals = () => { @@ -28,6 +37,7 @@ const Terminals = () => { const [searchQuery, setSearchQuery] = useState(''); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isPinModalOpen, setIsPinModalOpen] = useState(false); + const [isLinkModalOpen, setIsLinkModalOpen] = useState(false); const [selectedTerminal, setSelectedTerminal] = useState(null); const fetchTerminals = async () => { @@ -61,11 +71,26 @@ const Terminals = () => { } }; + const handleUnpair = async (id: number, e: React.MouseEvent) => { + e.stopPropagation(); + if (window.confirm('Unpair this device? It will need to be re-paired on next boot.')) { + try { + const response = await apiFetch(`/api/terminals/${id}/unpair`, { method: 'POST' }); + if (response.ok) fetchTerminals(); + } catch (error) { + console.error('Failed to unpair device:', error); + } + } + }; + + const pairedCount = terminals.filter(t => t.paired).length; + const filteredTerminals = terminals.filter(t => { const query = (searchQuery || '').toLowerCase(); return (t.name || '').toLowerCase().includes(query) || (t.location || '').toLowerCase().includes(query); }); + return (
{/* Header Section */} @@ -94,26 +119,28 @@ const Terminals = () => {
-

Active Devices

+

Registered Devices

{terminals.length}

-
- +
0 ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}`}> +
-

System Status

-

Secure & Online

+

Paired Devices

+

+ {pairedCount} / {terminals.length} +

- Terminals are used at physical counters to scan order QRs and print bills. + Power on a device and enter its 6-digit OTP here to pair it with a terminal.

@@ -159,15 +186,30 @@ const Terminals = () => {
-
+
- +
+ {terminal.paired && ( + + )} + +
@@ -178,16 +220,48 @@ const Terminals = () => {
+ {/* Device ID for paired terminals */} + {terminal.paired && terminal.deviceId && ( +
+ + {terminal.deviceId} +
+ )} +
-
- Device Ready -
-
- - View API Key - + {terminal.paired ? ( + <> +
+ Paired + + ) : ( + <> +
+ Unpaired + + )}
+ + {terminal.paired ? ( +
+ + View API Key + +
+ ) : ( + + )}
@@ -226,6 +300,17 @@ const Terminals = () => { terminalName={selectedTerminal?.name || ''} /> + { + setIsLinkModalOpen(false); + setSelectedTerminal(null); + }} + onSuccess={fetchTerminals} + terminalId={selectedTerminal?.id || null} + terminalName={selectedTerminal?.name || ''} + /> +