| 1 | package com.nikola.web3ednevnikbackend.controller;
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 | import com.nikola.web3ednevnikbackend.config.security.CustomUserDetails;
|
|---|
| 5 | import com.nikola.web3ednevnikbackend.dto.users.BlagajnikResponseDTO;
|
|---|
| 6 | import com.nikola.web3ednevnikbackend.dto.users.RoditelResponseDTO;
|
|---|
| 7 | import com.nikola.web3ednevnikbackend.service.users.BlagajnikService;
|
|---|
| 8 | import com.nikola.web3ednevnikbackend.service.users.RoditelService;
|
|---|
| 9 |
|
|---|
| 10 | import org.springframework.http.ResponseEntity;
|
|---|
| 11 | import org.springframework.security.core.Authentication;
|
|---|
| 12 | import org.springframework.web.bind.annotation.*;
|
|---|
| 13 | import org.web3j.crypto.*;
|
|---|
| 14 | import org.web3j.utils.Numeric;
|
|---|
| 15 |
|
|---|
| 16 | import java.math.BigInteger;
|
|---|
| 17 | import java.nio.charset.StandardCharsets;
|
|---|
| 18 | import java.util.*;
|
|---|
| 19 | import java.util.concurrent.ConcurrentHashMap;
|
|---|
| 20 |
|
|---|
| 21 | @RestController
|
|---|
| 22 | @RequestMapping("/metamuskAuth")
|
|---|
| 23 | public class MetamuskController {
|
|---|
| 24 | private final Map<String, String> nonces = new ConcurrentHashMap<>();
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 | private final RoditelService roditelService;
|
|---|
| 28 | private final BlagajnikService blagajnikService;
|
|---|
| 29 |
|
|---|
| 30 | public MetamuskController(RoditelService roditelService, BlagajnikService blagajnikService) {
|
|---|
| 31 | this.roditelService = roditelService;
|
|---|
| 32 | this.blagajnikService = blagajnikService;
|
|---|
| 33 | }
|
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 | @GetMapping("/nonce")
|
|---|
| 37 | public Map<String, String> getNonce(@RequestParam String address) {
|
|---|
| 38 | String normalized = address.toLowerCase();
|
|---|
| 39 |
|
|---|
| 40 | String nonce = UUID.randomUUID().toString();
|
|---|
| 41 | nonces.putIfAbsent(normalized, nonce);
|
|---|
| 42 |
|
|---|
| 43 | return Map.of("nonce", nonce);
|
|---|
| 44 | }
|
|---|
| 45 |
|
|---|
| 46 | //TODO: @PostMapping("/unlink-wallet") //logika samo za roditel (roditel moze da pravi unlink)
|
|---|
| 47 | @PostMapping("/unlink-wallet")
|
|---|
| 48 | public ResponseEntity<?> unlinkWallet(Authentication authentication) {
|
|---|
| 49 |
|
|---|
| 50 | if (authentication == null || !authentication.isAuthenticated()) {
|
|---|
| 51 | return ResponseEntity.status(401)
|
|---|
| 52 | .body(Map.of("message", "Unauthorized"));
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | CustomUserDetails userDetails =
|
|---|
| 56 | (CustomUserDetails) authentication.getPrincipal();
|
|---|
| 57 |
|
|---|
| 58 | UUID roditelId = userDetails.getId();
|
|---|
| 59 |
|
|---|
| 60 | RoditelResponseDTO roditel = roditelService.getRoditelById(roditelId);
|
|---|
| 61 |
|
|---|
| 62 | if (roditel.getMetamusk_adresa() == null) {
|
|---|
| 63 | return ResponseEntity.badRequest()
|
|---|
| 64 | .body(Map.of("message", "No wallet linked"));
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | roditelService.unlinkMetamuskAddress(roditelId);
|
|---|
| 68 |
|
|---|
| 69 | return ResponseEntity.ok(Map.of("message", "Wallet unlinked successfully"));
|
|---|
| 70 | }
|
|---|
| 71 |
|
|---|
| 72 |
|
|---|
| 73 |
|
|---|
| 74 | @PostMapping("/link-wallet-roditel") //logika samo za roditel (roditel moze da go menuva wallet)
|
|---|
| 75 | public ResponseEntity<?> linkWalletParent(
|
|---|
| 76 | @RequestBody Map<String, String> payload,
|
|---|
| 77 | Authentication authentication
|
|---|
| 78 | ) {
|
|---|
| 79 |
|
|---|
| 80 | if (authentication == null || !authentication.isAuthenticated()) {
|
|---|
| 81 | return ResponseEntity.status(401)
|
|---|
| 82 | .body(Map.of("message", "Unauthorized"));
|
|---|
| 83 | }
|
|---|
| 84 |
|
|---|
| 85 | String address = payload.get("address");
|
|---|
| 86 | String signature = payload.get("signature");
|
|---|
| 87 |
|
|---|
| 88 | if (address == null || signature == null) {
|
|---|
| 89 | return ResponseEntity.badRequest()
|
|---|
| 90 | .body(Map.of("message", "Missing data"));
|
|---|
| 91 | }
|
|---|
| 92 |
|
|---|
| 93 | address = address.trim();
|
|---|
| 94 | if (!WalletUtils.isValidAddress(address)) {
|
|---|
| 95 | return ResponseEntity.badRequest()
|
|---|
| 96 | .body(Map.of("message", "Invalid Ethereum address"));
|
|---|
| 97 | }
|
|---|
| 98 | String normalized = address.toLowerCase();
|
|---|
| 99 |
|
|---|
| 100 | String nonce = nonces.get(normalized);
|
|---|
| 101 |
|
|---|
| 102 | if (nonce == null) {
|
|---|
| 103 | return ResponseEntity.badRequest()
|
|---|
| 104 | .body(Map.of("message", "Nonce not found"));
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | boolean valid = verifySignature(address, signature, nonce);
|
|---|
| 108 |
|
|---|
| 109 | if (!valid) {
|
|---|
| 110 | return ResponseEntity.status(401)
|
|---|
| 111 | .body(Map.of("message", "Invalid signature"));
|
|---|
| 112 | }
|
|---|
| 113 |
|
|---|
| 114 | nonces.remove(normalized);
|
|---|
| 115 |
|
|---|
| 116 | CustomUserDetails userDetails =
|
|---|
| 117 | (CustomUserDetails) authentication.getPrincipal();
|
|---|
| 118 |
|
|---|
| 119 | UUID roditelId = userDetails.getId();
|
|---|
| 120 |
|
|---|
| 121 | RoditelResponseDTO roditel = roditelService.getRoditelById(roditelId);
|
|---|
| 122 |
|
|---|
| 123 |
|
|---|
| 124 | roditelService.updateMetamuskAddress(roditelId, normalized);
|
|---|
| 125 | System.out.println("Linked wallet attempt for roditel id " + roditelId + " with address" + normalized);
|
|---|
| 126 |
|
|---|
| 127 |
|
|---|
| 128 | return ResponseEntity.ok(Map.of("message", "Wallet linked successfully"));
|
|---|
| 129 | }
|
|---|
| 130 |
|
|---|
| 131 |
|
|---|
| 132 | public boolean verifySignature(String expectedAddress, String signatureHex, String message) {
|
|---|
| 133 | try {
|
|---|
| 134 | byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
|
|---|
| 135 | byte[] sigBytes = Numeric.hexStringToByteArray(signatureHex);
|
|---|
| 136 |
|
|---|
| 137 | if (sigBytes.length != 65) {
|
|---|
| 138 | return false;
|
|---|
| 139 | }
|
|---|
| 140 |
|
|---|
| 141 | byte v = sigBytes[64];
|
|---|
| 142 | if (v < 27) {
|
|---|
| 143 | v += 27;
|
|---|
| 144 | }
|
|---|
| 145 |
|
|---|
| 146 | Sign.SignatureData sigData = new Sign.SignatureData(
|
|---|
| 147 | v,
|
|---|
| 148 | Arrays.copyOfRange(sigBytes, 0, 32),
|
|---|
| 149 | Arrays.copyOfRange(sigBytes, 32, 64)
|
|---|
| 150 | );
|
|---|
| 151 |
|
|---|
| 152 |
|
|---|
| 153 | BigInteger publicKey = Sign.signedPrefixedMessageToKey(messageBytes, sigData);
|
|---|
| 154 |
|
|---|
| 155 | String recoveredAddress = "0x" + Keys.getAddress(publicKey);
|
|---|
| 156 |
|
|---|
| 157 | System.out.println("Recovered: " + recoveredAddress);
|
|---|
| 158 |
|
|---|
| 159 | return recoveredAddress.equalsIgnoreCase(expectedAddress);
|
|---|
| 160 |
|
|---|
| 161 | } catch (Exception e) {
|
|---|
| 162 | e.printStackTrace();
|
|---|
| 163 | return false;
|
|---|
| 164 | }
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 |
|
|---|
| 168 | } |
|---|