Skip to content

Commit 49edb6e

Browse files
Merge pull request #12 from gofynd/add-routes-for-partner-extension-launch-in-fdk-extension-python-FPCO-27967
Add routes for partner extension launch
2 parents c362973 + 6c7a989 commit 49edb6e

File tree

11 files changed

+242
-15
lines changed

11 files changed

+242
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
---
8+
## [v0.6.0]
9+
### Added
10+
- Added `partner_api_routes` to support calling partners server APIs through `PartnerClient`
11+
- Added Support to launch extension Admin inside partners panel
12+
713
---
814
## [v0.5.2] - 2022-12-31
915
### Added

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ async def test_route_handler(request):
7474
except Exception as e:
7575
return response.json({"error_message": str(e)}, 500)
7676

77-
fdk_extension_client.platform_api_routes_bp.add_route(test_route_handler, "/test/routes")
78-
app.blueprint(fdk_extension_client.platform_api_routes_bp)
77+
fdk_extension_client.platform_api_routes.add_route(test_route_handler, "/test/routes")
78+
app.blueprint(fdk_extension_client.platform_api_routes)
7979
```
8080

8181
#### How to call platform apis in background tasks?
@@ -94,6 +94,26 @@ async def background_handler(request):
9494
return response.json({"error_message": str(e)}, 500)
9595
```
9696

97+
#### How to call partner apis?
98+
99+
To call partner api you need to have instance of `PartnerClient`. Instance holds methods for SDK classes. All routes registered under `partner_api_routes` blueprint will have `partner_client` under request context object which is instance of `PartnerClient`.
100+
101+
> Here `partner_api_routes` has middleware attached which allows passing such request which are called after launching extension under any company.
102+
103+
```python
104+
async def test_route_handler(request):
105+
try:
106+
partner_client = request.conn_info.ctx.partner_client
107+
data = await partner_client.lead.getTickets()
108+
return response.json({"data": data["json"]})
109+
except Exception as e:
110+
return response.json({"error_message": str(e)}, 500)
111+
112+
fdk_extension_client.partner_api_routes.add_route(test_route_handler, "/test/routes")
113+
app.blueprint(fdk_extension_client.partner_api_routes)
114+
```
115+
116+
97117

98118
#### How to register for webhook events?
99119

fdk_extension/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
FDK Extension helper Library
33
"""
44

5-
__version__ = '0.6.0.beta2'
5+
__version__ = '0.6.0'
66

77

88
from fdk_extension.main import setup_fdk

fdk_extension/api_blueprints.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
from sanic import Blueprint
44
from sanic.blueprint_group import BlueprintGroup
55

6-
from .middleware.api_middleware import application_proxy_on_request
7-
from .middleware.api_middleware import platform_api_on_request
8-
from .middleware.session_middleware import session_middleware
6+
from .middleware.api_middleware import application_proxy_on_request, platform_api_on_request, partner_api_on_request
7+
from .middleware.session_middleware import session_middleware, partner_session_middleware
98

109

1110
class ClientBlueprintGroup(BlueprintGroup):
@@ -35,6 +34,13 @@ def chain(nested):
3534
if platform_api_on_request not in middleware_function:
3635
bp.middleware(platform_api_on_request, "request", *args, **kwargs)
3736

37+
elif self.client_type == "partner":
38+
if partner_session_middleware not in middleware_function:
39+
bp.middleware(partner_session_middleware, "request", *args, **kwargs)
40+
41+
if partner_api_on_request not in middleware_function:
42+
bp.middleware(partner_api_on_request, "request", *args, **kwargs)
43+
3844
elif self.client_type == "application":
3945
if application_proxy_on_request not in middleware_function:
4046
bp.middleware(application_proxy_on_request, "request", *args, **kwargs)
@@ -43,5 +49,6 @@ def chain(nested):
4349
def setup_proxy_routes() -> Tuple[BlueprintGroup, BlueprintGroup]:
4450
platform_api_routes = ClientBlueprintGroup(client_type="platform")
4551
application_proxy_routes = ClientBlueprintGroup(client_type="application")
52+
partner_proxy_routes = ClientBlueprintGroup(client_type="partner")
4653

47-
return platform_api_routes, application_proxy_routes
54+
return platform_api_routes, application_proxy_routes, partner_proxy_routes

fdk_extension/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
ONLINE_ACCESS_MODE = "online"
77

88
SESSION_COOKIE_NAME = "ext_session"
9+
ADMIN_SESSION_COOKIE_NAME='ext_adm_session'
910

1011
SESSION_EXPIRY_IN_SECONDS = 900
1112

fdk_extension/extension.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from fdk_client.platform.PlatformClient import PlatformClient
66
from fdk_client.platform.PlatformConfig import PlatformConfig
7+
from fdk_client.partner import PartnerClient, PartnerConfig
78
from fdk_client.application.ApplicationClient import ApplicationClient
89
from fdk_client.common.utils import get_headers_with_signature
910
from fdk_client.common.aiohttp_helper import AiohttpHelper
@@ -102,6 +103,9 @@ def verify_scopes(self, scopes: list, extension_data: dict) -> list:
102103

103104
def get_auth_callback(self) -> str:
104105
return urljoin(self.base_url, "/fp/auth")
106+
107+
def get_adm_auth_callback(self) -> str:
108+
return urljoin(self.base_url, "/adm/auth")
105109

106110
def is_online_access_mode(self) -> bool:
107111
return self.access_mode == ONLINE_ACCESS_MODE
@@ -146,6 +150,45 @@ async def get_platform_client(self, company_id, session: Session) -> PlatformCli
146150
})
147151
return platform_client
148152

153+
def get_partner_config(self, organization_id) -> PartnerConfig:
154+
if (not self.is_initialized()):
155+
raise FdkInvalidConfig("Extension not initialized due to invalid data")
156+
157+
partner_config = PartnerConfig({
158+
"organizationId": organization_id,
159+
"domain": self.cluster,
160+
"apiKey": self.api_key,
161+
"apiSecret": self.api_secret,
162+
"useAutoRenewTimer": False,
163+
"logLevel": "DEBUG"
164+
})
165+
return partner_config
166+
167+
async def get_partner_client(self, organization_id, session: Session) -> PartnerClient:
168+
if (not self.is_initialized()):
169+
raise FdkInvalidConfig("Extension not initialized due to invalid data")
170+
171+
from .session.session_storage import SessionStorage
172+
173+
partner_config = self.get_partner_config(organization_id)
174+
partner_config.oauthClient.setTokenFromSession(session)
175+
partner_config.oauthClient.token_expires_at = session.access_token_validity
176+
177+
if (session.access_token_validity and session.refresh_token):
178+
ac_nr_expired = ((session.access_token_validity - get_current_timestamp()) // 1000) <= 120
179+
if ac_nr_expired:
180+
logger.debug(f"Renewing access token for organization {organization_id} with partner config {json.dumps(safe_stringify(partner_config))}")
181+
renew_token_res = await partner_config.oauthClient.renewAccessToken(session.access_mode == OFFLINE_ACCESS_MODE)
182+
renew_token_res["access_token_validity"] = partner_config.oauthClient.token_expires_at
183+
session.update_token(renew_token_res)
184+
await SessionStorage.save_session(session)
185+
logger.debug(f"Access token renewed for organization {organization_id} with response {renew_token_res}")
186+
187+
partner_client = PartnerClient(partner_config)
188+
partner_client.setExtraHeaders({
189+
'x-ext-lib-version': f"py/{__version__}"
190+
})
191+
return partner_client
149192

150193
# Making API request to fetch extension details
151194
async def get_extension_details(self) -> dict:
@@ -184,6 +227,7 @@ def __init__(self, **client_data):
184227
self.platform_api_routes: ClientBlueprintGroup = client_data["platform_api_routes"]
185228
self.webhook_registry: WebhookRegistry = client_data["webhook_registry"]
186229
self.application_proxy_routes: ClientBlueprintGroup = client_data["application_proxy_routes"]
230+
self.partner_proxy_routes: ClientBlueprintGroup = client_data["partner_proxy_routes"]
187231
self.get_platform_client: PlatformClient = client_data["get_platform_client"]
188232
self.get_application_client: ApplicationClient = client_data["get_application_client"]
189233

fdk_extension/handlers.py

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Request handlers."""
22
from datetime import datetime, timedelta
3+
from urllib.parse import urljoin
34
import uuid
45

56
from sanic.blueprints import Blueprint
@@ -11,7 +12,7 @@
1112
from .constants import *
1213
from .exceptions import FdkSessionNotFoundError, FdkInvalidOAuthError
1314
from .extension import extension
14-
from .middleware.session_middleware import session_middleware
15+
from .middleware.session_middleware import session_middleware, partner_session_middleware
1516
from .session.session import Session
1617
from .session.session_storage import SessionStorage
1718
from .utilities import logger
@@ -99,7 +100,7 @@ async def auth_handler(request: Request):
99100
if not extension.is_online_access_mode():
100101
session_id = Session.generate_session_id(False, **{
101102
"cluster": extension.cluster,
102-
"company_id": company_id
103+
"id": company_id
103104
})
104105
session = await SessionStorage.get_session(session_id)
105106
if not session:
@@ -156,7 +157,7 @@ async def auto_install_handler(request: Request):
156157
platform_config = extension.get_platform_config(company_id=company_id)
157158
session_id = Session.generate_session_id(False, **{
158159
"cluster": extension.cluster,
159-
"company_id": company_id
160+
"id": company_id
160161
})
161162

162163
session = await SessionStorage.get_session(session_id=session_id)
@@ -203,7 +204,7 @@ async def uninstall_handler(request: Request):
203204
if not extension.is_online_access_mode():
204205
session_id = Session.generate_session_id(False, **{
205206
"cluster": extension.cluster,
206-
"company_id": company_id
207+
"id": company_id
207208
})
208209
await SessionStorage.delete_session(session_id=session_id)
209210

@@ -214,17 +215,137 @@ async def uninstall_handler(request: Request):
214215
logger.exception(e)
215216
return json_response({"error_message": str(e)}, 500)
216217

218+
async def adm_install_handler(request: Request):
219+
try:
220+
organization_id = request.args.get("organization_id")
221+
partner_config = extension.get_partner_config(organization_id)
222+
223+
session = Session(Session.generate_session_id(True))
224+
session_expires = datetime.now() + timedelta(seconds=SESSION_EXPIRY_IN_SECONDS) # 15 mins
225+
226+
if session.is_new:
227+
session.organization_id = organization_id
228+
session.scope = extension.scopes
229+
session.expires = session_expires
230+
session.access_mode = ONLINE_ACCESS_MODE # Always generate online mode token for extension launch
231+
session.extension_id = extension.api_key
232+
233+
request.conn_info.ctx.fdk_session = session
234+
request.conn_info.ctx.extension = extension
235+
236+
session.state = str(uuid.uuid4())
237+
238+
auth_callback = extension.get_adm_auth_callback()
239+
240+
# start authorization flow
241+
redirect_url = partner_config.oauthClient.startAuthorization({
242+
"scope": session.scope,
243+
"redirectUri": auth_callback,
244+
"state": session.state,
245+
"access_mode": ONLINE_ACCESS_MODE # Always generate online mode token for extension launch
246+
})
247+
248+
logger.debug(f"Redirecting after partner install callback to url: {redirect_url}")
249+
250+
cookie_name = ADMIN_SESSION_COOKIE_NAME
251+
252+
next_response = redirect(redirect_url)
253+
next_response.cookies[cookie_name] = session.session_id
254+
next_response.cookies[cookie_name]["secure"] = True
255+
next_response.cookies[cookie_name]["samesite"] = "None"
256+
next_response.cookies[cookie_name]["httponly"] = True
257+
next_response.cookies[cookie_name]["expires"] = session.expires
258+
259+
await SessionStorage.save_session(session)
260+
261+
return next_response
262+
except Exception as e:
263+
logger.exception(e)
264+
return json_response({"error_message": str(e)}, 500)
265+
266+
async def adm_auth_handler(request: Request):
267+
try:
268+
if not request.conn_info.ctx.fdk_session:
269+
raise FdkSessionNotFoundError("Can not complete oauth process as session not found")
270+
271+
if request.conn_info.ctx.fdk_session.state != request.args.get("state"):
272+
raise FdkInvalidOAuthError("Invalid oauth call")
273+
274+
organization_id = request.conn_info.ctx.fdk_session.organization_id
275+
276+
partner_config = extension.get_partner_config(organization_id)
277+
await partner_config.oauthClient.verifyCallback(request.args)
278+
279+
token: dict = partner_config.oauthClient.raw_token
280+
session_expires = datetime.now() + timedelta(seconds=token["expires_in"])
281+
282+
request.conn_info.ctx.fdk_session.expires = session_expires
283+
token["access_token_validity"] = int(session_expires.timestamp()*1000)
284+
request.conn_info.ctx.fdk_session.update_token(token)
285+
286+
await SessionStorage.save_session(request.conn_info.ctx.fdk_session)
287+
288+
289+
if not extension.is_online_access_mode():
290+
session_id = Session.generate_session_id(False, **{
291+
"cluster": extension.cluster,
292+
"id": organization_id
293+
})
294+
session = await SessionStorage.get_session(session_id)
295+
if not session:
296+
session = Session(session_id=session_id)
297+
elif session.extension_id != extension.api_key:
298+
session = Session(session_id=session_id)
299+
300+
#TODO: Do we need this here again
301+
partner_config = extension.get_partner_config(organization_id)
302+
offline_token_response = await partner_config.oauthClient.getOfflineAccessToken(
303+
extension.scopes, request.args.get("code")
304+
)
305+
306+
session.organization_id = organization_id
307+
session.scope = extension.scopes
308+
session.state = request.conn_info.ctx.fdk_session.state
309+
session.extension_id = extension.api_key
310+
offline_token_response["access_token_validity"] = partner_config.oauthClient.token_expires_at
311+
offline_token_response["access_mode"] = OFFLINE_ACCESS_MODE
312+
session.update_token(offline_token_response)
313+
314+
await SessionStorage.save_session(session=session)
315+
316+
317+
redirect_url = urljoin(extension.base_url, '/admin')
318+
next_response = redirect(redirect_url)
319+
320+
cookie_name = ADMIN_SESSION_COOKIE_NAME
321+
next_response.cookies[cookie_name] = request.conn_info.ctx.fdk_session.session_id
322+
next_response.cookies[cookie_name]["secure"] = True
323+
next_response.cookies[cookie_name]["samesite"] = "None"
324+
next_response.cookies[cookie_name]["httponly"] = True
325+
next_response.cookies[cookie_name]["expires"] = session_expires
326+
327+
logger.debug(f"Redirecting after auth callback to url: {redirect_url}")
328+
329+
return next_response
330+
except Exception as e:
331+
logger.exception(e)
332+
return json_response({"error_message": str(e)}, 500)
217333

218334
def setup_routes() -> BlueprintGroup:
219335
fdk_routes_bp1 = Blueprint("fdk_routes_bp1")
220336
fdk_routes_bp2 = Blueprint("fdk_routes_bp2")
337+
fdk_routes_bp3 = Blueprint("fdk_routes_bp3")
221338

222339
fdk_routes_bp1.middleware(session_middleware, "request")
223340
fdk_routes_bp1.add_route(auth_handler, "/fp/auth", methods=["GET"])
224341
fdk_routes_bp1.add_route(auto_install_handler, "/fp/auto_install", methods=["POST"])
225342

226343
fdk_routes_bp2.add_route(install_handler, "/fp/install", methods=["GET"])
344+
fdk_routes_bp2.add_route(adm_install_handler, "/adm/install", methods=["GET"])
227345
fdk_routes_bp2.add_route(uninstall_handler, "/fp/uninstall", methods=["POST"])
228346

229-
fdk_route = Blueprint.group(fdk_routes_bp1, fdk_routes_bp2)
347+
fdk_routes_bp3.middleware(partner_session_middleware, "request")
348+
fdk_routes_bp3.add_route(adm_auth_handler, "/adm/auth", methods=["GET"])
349+
350+
fdk_route = Blueprint.group(fdk_routes_bp1, fdk_routes_bp2, fdk_routes_bp3)
230351
return fdk_route

0 commit comments

Comments
 (0)