diff --git a/README.md b/README.md index f08f4a4..796cc13 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,35 @@ public class ExampleOfflineAccessMode { } ``` +#### How to call partner apis? + +To call partner api you need to have instance of `PartnerClient`. Instance holds methods for SDK classes. + +extend `BasePartnerController` class to create controller which will add `PartnerClient` in request. + +```java +@RestController +@RequestMapping("/api/v1") +@Slf4j +public class PartnerController extends BasePartnerController { + + @GetMapping(value = "/orgThemes", produces = "application/json") + public ThemePartnerModels.MarketplaceThemeSchema getOrgThemes(HttpServletRequest request) { + try { + PartnerClient partnerClient = (PartnerClient) request.getAttribute("partnerClient"); + ThemePartnerModels.MarketplaceThemeSchema orgThemes = partnerClient.theme.getOrganizationThemes("published", null, null); + + return orgThemes; + + } catch (Exception e) { + System.out.println(e.getMessage()); + throw new RuntimeException(e); + } + + } +} +``` + #### How to register for Webhook Events? Webhook events can be helpful to handle tasks when certain events occur on platform. You can subscribe to such events by passing **webhook** in Extension Configuration Property diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 diff --git a/pom.xml b/pom.xml index 1ebc7ce..6795c78 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.fynd fynd-extension-java - 0.5.0-beta.1 + 0.6.0 fynd-extension-java Java Fynd Extension Library @@ -17,7 +17,7 @@ 20211205 4.3.1 1.7 - v1.0.0 + 1.3.11-beta.4 @@ -33,6 +33,7 @@ org.projectlombok lombok + 1.18.30 diff --git a/src/main/java/com/fynd/extension/controllers/BasePartnerController.java b/src/main/java/com/fynd/extension/controllers/BasePartnerController.java new file mode 100644 index 0000000..55cf4f1 --- /dev/null +++ b/src/main/java/com/fynd/extension/controllers/BasePartnerController.java @@ -0,0 +1,10 @@ +package com.fynd.extension.controllers; + +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BasePartnerController { + + + // will be used by the extension developer +} \ No newline at end of file diff --git a/src/main/java/com/fynd/extension/controllers/ExtensionADMController.java b/src/main/java/com/fynd/extension/controllers/ExtensionADMController.java new file mode 100644 index 0000000..f85cf75 --- /dev/null +++ b/src/main/java/com/fynd/extension/controllers/ExtensionADMController.java @@ -0,0 +1,201 @@ +package com.fynd.extension.controllers; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import com.fynd.extension.error.FdkInvalidOAuth; +import com.fynd.extension.error.FdkSessionNotFound; +import com.fynd.extension.middleware.AccessMode; +import com.fynd.extension.middleware.FdkConstants; +import com.fynd.extension.model.Extension; +import com.fynd.extension.model.Option; +import com.fynd.extension.model.Response; +import com.fynd.extension.session.Session; +import com.fynd.extension.session.SessionStorage; +import com.sdk.common.model.AccessTokenDto; +import com.sdk.partner.PartnerConfig; +import lombok.extern.slf4j.Slf4j; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@RestController +@RequestMapping("/adm") +@Slf4j +public class ExtensionADMController { + @Autowired + Extension ext; + + @Autowired + SessionStorage sessionStorage; + + @GetMapping(path = "/install") + public ResponseEntity install(@RequestParam(value = "organization_id") String organizationId, + HttpServletResponse response, HttpServletRequest request) { + + try { + log.info("/adm/install invoked"); + if (StringUtils.isEmpty(organizationId)) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body("Invalid organization id"); + } + + PartnerConfig partnerConfig = ext.getPartnerConfig(organizationId); + Session session = new Session(Session.generateSessionId(true, null), true); + Date sessionExpires = Date.from(Instant.now() + .plusMillis(Fields.MINUTES_LIMIT)); + if (session.isNew()) { + session.setOrganizationId(organizationId); + session.setScope(ext.getExtensionProperties().getScopes()); + session.setExpires(FdkConstants.DATE_FORMAT.get() + .format(sessionExpires)); + session.setExpiresIn(sessionExpires.getTime()); + session.setAccessMode( + AccessMode.ONLINE.getName()); // Always generate online mode token for extension launch + session.setExtensionId(ext.getExtensionProperties() + .getApiKey()); + } else { + if (!StringUtils.isEmpty(session.getExpires())) { + session.setExpires(FdkConstants.DATE_FORMAT.get() + .format(session.getExpires())); + session.setExpiresIn(sessionExpires.getTime()); + } + } + ResponseCookie resCookie = ResponseCookie.from(FdkConstants.ADMIN_SESSION_COOKIE_NAME, session.getId()) + .httpOnly(true) + .sameSite("None") + .secure(true) + .path("/") + .maxAge(Duration.between(Instant.now(), Instant.ofEpochMilli( + session.getExpiresIn()))) + .build(); + + session.setState(UUID.randomUUID() + .toString()); + String baseUrl = ext.getExtensionProperties().getBaseUrl(); + var uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl); + String authCallback = uriBuilderFactory.builder().pathSegment("adm/auth").build().toString(); + System.out.println("authCallback " + authCallback); + String redirectUrl = partnerConfig.getPartnerOauthClient() + .getAuthorizationURL(session.getScope(), authCallback, + session.getState(), + true); // Always generate online mode token for extension launch + sessionStorage.saveSession(session); + request.setAttribute("session", session); + return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT) + .header(HttpHeaders.LOCATION, redirectUrl) + .header(HttpHeaders.SET_COOKIE, resCookie.toString()) + .build(); + } catch (Exception error) { + log.error("Exception in install call ", error); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new Response(false, error.getMessage())); + } + + } + + @GetMapping(path = "/auth") + public ResponseEntity authorize(@RequestParam(value = "organization_id") String organizationId, + @RequestParam(value = "code", required = false) String code, + @RequestParam(value = "state") String state, + HttpServletRequest request, HttpServletResponse response) { + + try { + String sessionIdForOrganization = ext.getCookieValue(request.getCookies()); + if (StringUtils.isNotEmpty(sessionIdForOrganization)) { + Session fdkSession = sessionStorage.getSession(sessionIdForOrganization); + if (Objects.isNull(fdkSession)) { + throw new FdkSessionNotFound("Can not complete oauth process as session not found"); + } + if (!fdkSession.getState() + .equalsIgnoreCase(state)) { + throw new FdkInvalidOAuth("Invalid oauth call"); + } + PartnerConfig partnerConfig = ext.getPartnerConfig(fdkSession.getOrganizationId()); + partnerConfig.getPartnerOauthClient() + .verifyCallback(code); + + AccessTokenDto token = partnerConfig.getPartnerOauthClient() + .getRawToken(); + Date sessionExpires = Date.from(Instant.now() + .plusMillis(token.getExpiresIn() * 1000)); + fdkSession.setExpires(FdkConstants.DATE_FORMAT.get() + .format(sessionExpires)); + token.setAccessTokenValidity(sessionExpires.getTime()); + Session.updateToken(token, fdkSession); + sessionStorage.saveSession(fdkSession); + request.setAttribute("session", fdkSession); + // Generate separate access token for offline mode + if (!ext.isOnlineAccessMode()) { + String sid = Session.generateSessionId(false, new Option(organizationId, ext.getExtensionProperties() + .getCluster())); + Session session = sessionStorage.getSession(sid); + log.debug("Retrieving session in ExtensionController.authorize() : {}", session); + if (ObjectUtils.isEmpty(session) || (!Objects.equals(session.getExtensionId(), + ext.getExtensionProperties() + .getApiKey()))) { + session = new Session(sid, true); + } + AccessTokenDto offlineTokenRes = partnerConfig.getPartnerOauthClient() + .getOfflineAccessToken(String.join(",", ext.getExtensionProperties().getScopes()), code); + session.setOrganizationId(organizationId); + session.setScope(ext.getExtensionProperties().getScopes()); + session.setState(fdkSession.getState()); + session.setExtensionId(ext.getExtensionProperties() + .getApiKey()); + offlineTokenRes.setAccessTokenValidity(partnerConfig.getPartnerOauthClient() + .getTokenExpiresAt()); + offlineTokenRes.setAccessMode(AccessMode.OFFLINE.getName()); + Session.updateToken(offlineTokenRes, session); + log.debug("Saving session from ExtensionController.authorize() : {}", session); + sessionStorage.saveSession(session); + } else { + fdkSession.setExpires(null); + } + ResponseCookie resCookie = ResponseCookie.from(FdkConstants.ADMIN_SESSION_COOKIE_NAME, fdkSession.getId()) + .httpOnly(true) + .sameSite("None") + .secure(true) + .path("/") + .maxAge(Duration.between(Instant.now(), Instant.ofEpochMilli( + fdkSession.getExpiresIn()))) + .build(); + String baseUrl = ext.getExtensionProperties().getBaseUrl(); + var uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl); + String admLaunchCallback = uriBuilderFactory.builder().pathSegment("admin").build().toString(); + return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT) + .header(HttpHeaders.LOCATION, admLaunchCallback) + .header(HttpHeaders.SET_COOKIE, resCookie.toString()) + .build(); + } + } catch (Exception error) { + log.error("Exception in auth call ", error); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new Response(false, error.getMessage())); + } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new Response(false, "Failed due to empty Session ID")); + } + + public interface Fields { + int MINUTES_LIMIT = 900000; + String DELIMITER = "_"; + } + +} diff --git a/src/main/java/com/fynd/extension/middleware/ControllerInterceptor.java b/src/main/java/com/fynd/extension/middleware/ControllerInterceptor.java index 0cd38ee..eb5616d 100644 --- a/src/main/java/com/fynd/extension/middleware/ControllerInterceptor.java +++ b/src/main/java/com/fynd/extension/middleware/ControllerInterceptor.java @@ -2,11 +2,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fynd.extension.controllers.BaseApplicationController; +import com.fynd.extension.controllers.BasePartnerController; import com.fynd.extension.controllers.BasePlatformController; import com.fynd.extension.model.*; import com.fynd.extension.session.Session; import com.sdk.application.ApplicationClient; import com.sdk.application.ApplicationConfig; +import com.sdk.partner.PartnerClient; import com.sdk.platform.PlatformClient; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -33,6 +35,9 @@ public class ControllerInterceptor implements HandlerInterceptor { @Autowired SessionInterceptor sessionInterceptor; + @Autowired + PartnerSessionInterceptor partnerSessionInterceptor; + @Override public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) { try { @@ -78,6 +83,19 @@ public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServl } return true; } + else if(controller instanceof BasePartnerController){ + + boolean isSessionInterceptorPassed = partnerSessionInterceptor.preHandle(request, response, handler); + + log.info("[PARTNER INTERCEPTOR]"); + Session fdkSession = (Session) request.getAttribute("fdkSession"); + PartnerClient partnerClient = extension.getPartnerClient(fdkSession.getOrganizationId(), fdkSession); + + request.setAttribute("partnerClient", partnerClient); + request.setAttribute("extension", extension); + + return isSessionInterceptorPassed; + } } diff --git a/src/main/java/com/fynd/extension/middleware/FdkConstants.java b/src/main/java/com/fynd/extension/middleware/FdkConstants.java index 88d187c..bfe2f9e 100644 --- a/src/main/java/com/fynd/extension/middleware/FdkConstants.java +++ b/src/main/java/com/fynd/extension/middleware/FdkConstants.java @@ -5,6 +5,8 @@ public class FdkConstants { public static final String SESSION_COOKIE_NAME = "ext_session"; + public static final String ADMIN_SESSION_COOKIE_NAME = "ext_adm_session"; + public static final ThreadLocal DATE_FORMAT = new ThreadLocal() { @Override diff --git a/src/main/java/com/fynd/extension/middleware/PartnerSessionInterceptor.java b/src/main/java/com/fynd/extension/middleware/PartnerSessionInterceptor.java new file mode 100644 index 0000000..951ec43 --- /dev/null +++ b/src/main/java/com/fynd/extension/middleware/PartnerSessionInterceptor.java @@ -0,0 +1,50 @@ +package com.fynd.extension.middleware; + +import com.fynd.extension.session.Session; +import com.fynd.extension.session.SessionStorage; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.Arrays; +import java.util.Optional; + +import static com.fynd.extension.middleware.FdkConstants.ADMIN_SESSION_COOKIE_NAME; + +@Slf4j +@Component +public class PartnerSessionInterceptor implements HandlerInterceptor{ + + @Autowired + SessionStorage sessionStorage; + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + log.info("[PARTNER SESSION INTERCEPTOR]"); + Session fdkSession = null; + + Optional sessionCookie = Arrays.stream(request.getCookies()) + .filter(c -> c.getName().equals(ADMIN_SESSION_COOKIE_NAME)) + .findFirst(); + + if(sessionCookie.isPresent()){ + String sessionId = sessionCookie.map(Cookie::getValue).orElse(null); + fdkSession = sessionStorage.getSession(sessionId); + } + + if (ObjectUtils.isNotEmpty(fdkSession)) { + request.setAttribute("fdkSession", fdkSession); + return true; + } else { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "unauthorized"); + } + } +} diff --git a/src/main/java/com/fynd/extension/model/Extension.java b/src/main/java/com/fynd/extension/model/Extension.java index 4c84aa2..e971737 100644 --- a/src/main/java/com/fynd/extension/model/Extension.java +++ b/src/main/java/com/fynd/extension/model/Extension.java @@ -13,8 +13,11 @@ import com.sdk.common.RequestSignerInterceptor; import com.sdk.common.RetrofitServiceFactory; import com.sdk.common.model.AccessTokenDto; +import com.sdk.partner.PartnerClient; import com.sdk.platform.PlatformClient; import com.sdk.platform.PlatformConfig; +import com.sdk.partner.PartnerConfig; + import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -170,6 +173,26 @@ public String getSessionIdFromCookie(Cookie[] cookies, String companyId) { return StringUtils.EMPTY; } + public String getCookieValue(Cookie[] cookies) { + try{ + // Replace "yourDynamicCookieName" with the actual dynamic cookie name + String dynamicCookieName = FdkConstants.ADMIN_SESSION_COOKIE_NAME; + if (cookies != null) { + for (Cookie cookie : cookies) { + // Check if the cookie name matches the dynamic name + if (dynamicCookieName.equals(cookie.getName())) { + // Process the cookie value + return cookie.getValue(); + } + } + } + throw new FdkSessionNotFound("Cookie not found"); + } catch (Exception e) { + log.error("Failure in fetching Cookie : {}", e); + } + return StringUtils.EMPTY; + } + private static void verifyScopes(List scopeList, ExtensionDetailsDTO extensionDetailsDTO) { List missingScopes = scopeList.stream() .filter(val -> !extensionDetailsDTO.getScope() @@ -230,8 +253,60 @@ public PlatformConfig getPlatformConfig(String companyId) { throw new FdkInvalidExtensionConfig("Extension not initialized due to invalid data"); } return new PlatformConfig(companyId, this.extensionProperties.getApiKey(), - this.extensionProperties.getApiSecret(), this.extensionProperties.getCluster(), - false); + this.extensionProperties.getApiSecret(), this.extensionProperties.getCluster(), + false); + } + + public PartnerConfig getPartnerConfig(String organizationId) { + if (!this.isInitialized) { + throw new FdkInvalidExtensionConfig("Extension not initialized due to invalid data"); + } + return new PartnerConfig( + organizationId, + this.extensionProperties.getApiKey(), + this.extensionProperties.getApiSecret(), + this.extensionProperties.getCluster(), + false + ); + } + + public PartnerClient getPartnerClient(String organizationId, Session session){ + if (!this.isInitialized) { + throw new FdkInvalidExtensionConfig("Extension not initialized due to invalid data"); + } + + PartnerConfig partnerConfig = this.getPartnerConfig(organizationId); + + AccessTokenDto accessTokenDto = buildAccessToken(session); + + partnerConfig.getPartnerOauthClient().setToken(accessTokenDto); + partnerConfig.getPartnerOauthClient().setTokenExpiresAt(session.getAccessTokenValidity()); + + if (Objects.nonNull(session.getAccessTokenValidity()) && Objects.nonNull(session.getRefreshToken())) { + boolean acNrExpired = ((session.getAccessTokenValidity() - new Date().getTime()) / 1000) <= 120; + if (acNrExpired) { + try { + log.debug("Renewing access token for organization {} with partner config {}", organizationId, + partnerConfig); + AccessTokenDto renewTokenRes = partnerConfig.getPartnerOauthClient() + .renewAccesstoken(); + renewTokenRes.setAccessTokenValidity(partnerConfig.getPartnerOauthClient() + .getTokenExpiresAt()); + Session.updateToken(renewTokenRes, session); + SessionStorage sessionStorage = new SessionStorage(); + sessionStorage.saveSession(session, this); + log.info("Access token renewed for organization : " + organizationId); + } catch (Exception e) { + log.error("Exception occurred in renewing access token ", e); + } + } + } + + PartnerClient partnerClient = new PartnerClient(partnerConfig); + + partnerClient.setExtraHeader("x-ext-lib-version", "java/" + buildVersion); + + return partnerClient; } private AccessTokenDto buildAccessToken(Session session) { diff --git a/src/main/java/com/fynd/extension/model/Option.java b/src/main/java/com/fynd/extension/model/Option.java index 0c492bb..ecc97bc 100644 --- a/src/main/java/com/fynd/extension/model/Option.java +++ b/src/main/java/com/fynd/extension/model/Option.java @@ -9,7 +9,7 @@ @NoArgsConstructor public class Option { - private String company_id; + private String id; private String cluster; } diff --git a/src/main/java/com/fynd/extension/session/Session.java b/src/main/java/com/fynd/extension/session/Session.java index 49a972d..56004d8 100644 --- a/src/main/java/com/fynd/extension/session/Session.java +++ b/src/main/java/com/fynd/extension/session/Session.java @@ -58,6 +58,9 @@ public class Session { @JsonProperty("extension_id") private String extensionId; + @JsonProperty("organization_id") + private String organizationId; + public Session(String id, boolean isNew) { this.id = id; this.isNew = isNew; @@ -77,7 +80,7 @@ public static String generateSessionId(boolean isOnline, Option options) throws MessageDigest digest = MessageDigest.getInstance("SHA-256"); return Base64.getEncoder() .encodeToString( - digest.digest((options.getCluster() + ":" + options.getCompany_id()).getBytes())); + digest.digest((options.getCluster() + ":" + options.getId()).getBytes())); } }