package com.lincfusion.udyamlinc.security; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; /** Issues and reads the two token flavours: officer console and enterprise app. */ @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration:86400000}") private long expirationMs; private Key signingKey() { return Keys.hmacShaKeyFor(secret.getBytes()); } /** * Bank console session — scope=BANK, pinned to one institution. * *

The institution is a signed claim rather than something the console * sends with each call. A tenancy key the caller can type is not a tenancy * key, and this is the one place that distinction is enforced for every * request that follows. */ public String generateBankToken(String username, String role, String blocks, Long institutionId, Long branchId) { Map claims = new HashMap<>(); claims.put("realm", "UDYAM"); claims.put("scope", "BANK"); claims.put("role", role); claims.put("blocks", blocks); claims.put("institutionId", institutionId); claims.put("branchId", branchId); return build(claims, username); } /** * NABARD session — scope=NABARD, deliberately carrying no institution. * *

An apex user is not a super-user with every tenancy at once; they are a * user entitled to aggregates and to no borrower row at all. The absent * claim is what {@code TenantContext} reads to mean exactly that. */ public String generateApexToken(String username, String role, Long institutionId) { Map claims = new HashMap<>(); claims.put("realm", "UDYAM"); claims.put("scope", "NABARD"); claims.put("role", role); claims.put("institutionId", institutionId); return build(claims, username); } /** * Enterprise app session — scope=ENTERPRISE, pinned to one enterprise id so * the owner can only ever read their own record. */ public String generateEnterpriseToken(String enterpriseId, String sector) { Map claims = new HashMap<>(); claims.put("realm", "UDYAM"); claims.put("scope", "ENTERPRISE"); claims.put("role", "OWNER"); claims.put("enterpriseId", enterpriseId); claims.put("sector", sector); return build(claims, enterpriseId); } private String build(Map claims, String subject) { long now = System.currentTimeMillis(); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(now)) .setExpiration(new Date(now + expirationMs)) .signWith(signingKey(), SignatureAlgorithm.HS256) .compact(); } public Claims parse(String token) { return Jwts.parserBuilder().setSigningKey(signingKey()).build().parseClaimsJws(token).getBody(); } public String subject(String token) { return parse(token).getSubject(); } public String scope(String token) { return String.valueOf(parse(token).get("scope")); } public String role(String token) { return String.valueOf(parse(token).get("role")); } public Long institutionId(String token) { return longClaim(parse(token), "institutionId"); } public Long branchId(String token) { return longClaim(parse(token), "branchId"); } public String enterpriseId(String token) { Object value = parse(token).get("enterpriseId"); return value == null ? null : String.valueOf(value); } /** JWT numerics arrive as Integer or Long depending on magnitude. */ private Long longClaim(Claims claims, String name) { Object value = claims.get(name); if (value == null) return null; if (value instanceof Number number) return number.longValue(); try { return Long.valueOf(String.valueOf(value)); } catch (NumberFormatException e) { return null; } } public boolean isValid(String token) { try { return parse(token).getExpiration().after(new Date()); } catch (Exception e) { return false; } } }