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()));
}
}