move registration code into the sourceCredential.
Added ability to Reconnect/Update source make sure re-connect function is disabled for manual sources.
This commit is contained in:
parent
c590663537
commit
a6edb24aa0
|
@ -1,6 +1,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
||||
|
@ -134,7 +135,90 @@ func (s *SourceCredential) IsDynamicClient() bool {
|
|||
return len(s.DynamicClientRegistrationMode) > 0
|
||||
}
|
||||
|
||||
// This method will generate a new keypair, register a new dynamic client with the provider
|
||||
// it will set the following fields:
|
||||
// - DynamicClientJWKS
|
||||
// - DynamicClientId
|
||||
func (s *SourceCredential) RegisterDynamicClient() error {
|
||||
|
||||
//this source requires dynamic client registration
|
||||
// see https://fhir.epic.com/Documentation?docId=Oauth2§ion=Standalone-Oauth2-OfflineAccess-0
|
||||
|
||||
// Generate a public-private key pair
|
||||
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
|
||||
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while generating device-specific keypair for dynamic client: %w", err)
|
||||
}
|
||||
|
||||
//store in sourceCredential
|
||||
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while serializing keypair for dynamic client: %w", err)
|
||||
}
|
||||
s.DynamicClientJWKS = []map[string]string{
|
||||
serializedKeypair,
|
||||
}
|
||||
|
||||
//generate dynamic client registration request
|
||||
payload := ClientRegistrationRequest{
|
||||
SoftwareId: s.ClientId,
|
||||
Jwks: ClientRegistrationRequestJwks{
|
||||
Keys: []ClientRegistrationRequestJwksKey{
|
||||
{
|
||||
KeyType: "RSA",
|
||||
KeyId: serializedKeypair["kid"],
|
||||
Modulus: serializedKeypair["n"],
|
||||
PublicExponent: serializedKeypair["e"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while marshalling dynamic client registration request: %w", err)
|
||||
}
|
||||
|
||||
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
|
||||
req, err := http.NewRequest(http.MethodPost, s.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while generating dynamic client registration request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken))
|
||||
|
||||
registrationResponse, err := http.DefaultClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while sending dynamic client registration request: %w", err)
|
||||
}
|
||||
defer registrationResponse.Body.Close()
|
||||
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
|
||||
b, err := io.ReadAll(registrationResponse.Body)
|
||||
if err == nil {
|
||||
log.Printf("Error Response body: %s", string(b))
|
||||
}
|
||||
return fmt.Errorf("an error occurred while reading dynamic client registration response, status code was not 200: %d", registrationResponse.StatusCode)
|
||||
|
||||
}
|
||||
|
||||
//read response
|
||||
var registrationResponseBytes ClientRegistrationResponse
|
||||
err = json.NewDecoder(registrationResponse.Body).Decode(®istrationResponseBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while parsing dynamic client registration response: %w", err)
|
||||
}
|
||||
|
||||
//store the dynamic client id
|
||||
s.DynamicClientId = registrationResponseBytes.ClientId
|
||||
return nil
|
||||
}
|
||||
|
||||
// this will set/update the AccessToken and Expiry using the dynamic client credentials
|
||||
// it will set the following fields:
|
||||
// - AccessToken
|
||||
// - ExpiresAt
|
||||
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
|
||||
if len(s.DynamicClientRegistrationMode) == 0 {
|
||||
return fmt.Errorf("dynamic client registration mode not set")
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||
"github.com/fastenhealth/fasten-sources/clients/factory"
|
||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CreateSource(c *gin.Context) {
|
||||
func CreateReconnectSource(c *gin.Context) {
|
||||
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
||||
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||
|
||||
|
@ -39,91 +36,13 @@ func CreateSource(c *gin.Context) {
|
|||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
//this source requires dynamic client registration
|
||||
// see https://fhir.epic.com/Documentation?docId=Oauth2§ion=Standalone-Oauth2-OfflineAccess-0
|
||||
|
||||
// Generate a public-private key pair
|
||||
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
|
||||
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
|
||||
err := sourceCred.RegisterDynamicClient()
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while generating device-specific keypair for dynamic client", err)
|
||||
logger.Errorln("An error occurred while registering dynamic client", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
//store in sourceCredential
|
||||
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while serializing keypair for dynamic client", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
sourceCred.DynamicClientJWKS = []map[string]string{
|
||||
serializedKeypair,
|
||||
}
|
||||
|
||||
//generate dynamic client registration request
|
||||
payload := models.ClientRegistrationRequest{
|
||||
SoftwareId: sourceCred.ClientId,
|
||||
Jwks: models.ClientRegistrationRequestJwks{
|
||||
Keys: []models.ClientRegistrationRequestJwksKey{
|
||||
{
|
||||
KeyType: "RSA",
|
||||
KeyId: serializedKeypair["kid"],
|
||||
Modulus: serializedKeypair["n"],
|
||||
PublicExponent: serializedKeypair["e"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while marshalling dynamic client registration request", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
|
||||
req, err := http.NewRequest(http.MethodPost, sourceCred.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while generating dynamic client registration request", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sourceCred.AccessToken))
|
||||
|
||||
registrationResponse, err := http.DefaultClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while sending dynamic client registration request", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
defer registrationResponse.Body.Close()
|
||||
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
|
||||
logger.Errorln("An error occurred while reading dynamic client registration response, status code was not 200", registrationResponse.StatusCode)
|
||||
b, err := io.ReadAll(registrationResponse.Body)
|
||||
if err == nil {
|
||||
logger.Printf("Error Response body: %s", string(b))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//read response
|
||||
var registrationResponseBytes models.ClientRegistrationResponse
|
||||
err = json.NewDecoder(registrationResponse.Body).Decode(®istrationResponseBytes)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while parsing dynamic client registration response", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//store the dynamic client id
|
||||
sourceCred.DynamicClientId = registrationResponseBytes.ClientId
|
||||
|
||||
//generate a JWT token and then use it to get an access token for the dynamic client
|
||||
err = sourceCred.RefreshDynamicClientAccessToken()
|
||||
if err != nil {
|
||||
|
@ -133,11 +52,22 @@ func CreateSource(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
err := databaseRepo.CreateSource(c, &sourceCred)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while storing source credential", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
if sourceCred.ID != uuid.Nil {
|
||||
//reconnect
|
||||
err := databaseRepo.UpdateSource(c, &sourceCred)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while reconnecting source credential", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
//create source for the first time
|
||||
err := databaseRepo.CreateSource(c, &sourceCred)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while storing source credential", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// after creating the source, we should do a bulk import (in the background)
|
||||
|
|
|
@ -53,16 +53,6 @@ func UnsafeRequestSource(c *gin.Context) {
|
|||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
//TODO: if source has been updated, we should save the access/refresh token.
|
||||
//if updatedSource != nil {
|
||||
// logger.Warnf("TODO: source credential has been updated, we should store it in the database: %v", updatedSource)
|
||||
// // err := databaseRepo.CreateSource(c, updatedSource)
|
||||
// // if err != nil {
|
||||
// // logger.Errorf("An error occurred while updating source credential %v", err)
|
||||
// // c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
// // return
|
||||
// // }
|
||||
//}
|
||||
|
||||
var resp map[string]interface{}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
|
|||
{
|
||||
secure.GET("/summary", handler.GetSummary)
|
||||
|
||||
secure.POST("/source", handler.CreateSource)
|
||||
secure.POST("/source", handler.CreateReconnectSource)
|
||||
secure.POST("/source/manual", handler.CreateManualSource)
|
||||
secure.GET("/source", handler.ListSource)
|
||||
secure.GET("/source/:sourceId", handler.GetSource)
|
||||
|
|
|
@ -43,9 +43,8 @@
|
|||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownManual">
|
||||
<button ngbDropdownItem (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
|
||||
<!-- <button ngbDropdownItem (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>-->
|
||||
<button ngbDropdownItem type="button" class="btn disabled btn-outline-danger">Reconnect</button>
|
||||
<button ngbDropdownItem (click)="sourceDeleteHandler()" type="button" class="btn btn-outline-danger">Delete</button>
|
||||
<button *ngIf="modalSelectedSourceListItem.source?.source_type != 'manual'" ngbDropdownItem (click)="sourceReconnectHandler(modalSelectedSourceListItem)" type="button" class="btn btn-danger">Reconnect</button>
|
||||
<button ngbDropdownItem (click)="sourceDeleteHandler()" type="button" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
|
||||
|
|
|
@ -11,6 +11,7 @@ import {ToastService} from '../../services/toast.service';
|
|||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {Location} from '@angular/common';
|
||||
import {EventBusService} from '../../services/event-bus.service';
|
||||
import {SourceState} from '../../models/fasten/source-state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-medical-sources-connected',
|
||||
|
@ -102,7 +103,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
|||
});
|
||||
this.location.replaceState(urlTree.toString());
|
||||
|
||||
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState))
|
||||
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState)) as SourceState
|
||||
localStorage.removeItem(callbackState)
|
||||
|
||||
if(callbackError && !callbackCode){
|
||||
|
@ -147,6 +148,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
|||
//Create FHIR Client
|
||||
|
||||
const dbSourceCredential = new Source({
|
||||
id: expectedSourceStateInfo.reconnect_source_id,
|
||||
source_type: sourceType,
|
||||
|
||||
authorization_endpoint: sourceMetadata.authorization_endpoint,
|
||||
|
@ -364,6 +366,29 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
//this is similar to the connectHandler in the MedicalSourcesComponent
|
||||
public sourceReconnectHandler(selectedSourceListItem: SourceListItem){
|
||||
|
||||
let sourceType = selectedSourceListItem.metadata.source_type
|
||||
this.lighthouseApi.getLighthouseSource(sourceType)
|
||||
.then(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||
console.log(sourceMetadata);
|
||||
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata, selectedSourceListItem.source.id)
|
||||
|
||||
console.log('authorize url:', authorizationUrl.toString());
|
||||
// redirect to lighthouse with uri's (or open a new window in desktop mode)
|
||||
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri).subscribe((codeData) => {
|
||||
//Note: this code will only run in Desktop mode (with popups)
|
||||
//in non-desktop environments, the user is redirected in the same window, and this code is never executed.
|
||||
|
||||
//always close the modal
|
||||
this.modalService.dismissAll()
|
||||
|
||||
//redirect the browser back to this page with the code in the query string parameters
|
||||
this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private getDismissReason(reason: any): string {
|
||||
if (reason === ModalDismissReasons.ESC) {
|
||||
|
|
|
@ -2,6 +2,8 @@ export class SourceState {
|
|||
state: string
|
||||
|
||||
source_type: string //used to override the source_type for sources which have a single redirect url (eg. Epic)
|
||||
reconnect_source_id?: string //used to reconnect a source
|
||||
|
||||
code_verifier?: string
|
||||
code_challenge_method?: string
|
||||
code_challenge?: string
|
||||
|
|
|
@ -5,7 +5,7 @@ import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-sourc
|
|||
import {Source} from '../../models/fasten/source';
|
||||
import {MetadataSource} from '../../models/fasten/metadata-source';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ActivatedRoute, Router, UrlSerializer} from '@angular/router';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs';
|
||||
import {
|
||||
|
@ -17,7 +17,6 @@ import {debounceTime, distinctUntilChanged, pairwise, startWith} from 'rxjs/oper
|
|||
import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service';
|
||||
import {FormControl, FormGroup} from '@angular/forms';
|
||||
import * as _ from 'lodash';
|
||||
import {Location} from '@angular/common';
|
||||
|
||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||
|
||||
|
@ -79,9 +78,7 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
private activatedRoute: ActivatedRoute,
|
||||
private filterService: MedicalSourcesFilterService,
|
||||
private modalService: NgbModal,
|
||||
private router: Router,
|
||||
private urlSerializer: UrlSerializer,
|
||||
private location: Location,
|
||||
|
||||
) {
|
||||
this.filterService.filterChanges.subscribe((filterInfo) => {
|
||||
|
||||
|
@ -295,22 +292,8 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
//always close the modal
|
||||
this.modalService.dismissAll()
|
||||
|
||||
if(!codeData){
|
||||
//if we redirected completely, no callback data will be present.
|
||||
return
|
||||
}
|
||||
|
||||
//User was shown a popup, which was closed, and data was returned using events
|
||||
//redirect to callback page with code
|
||||
|
||||
let urlTree = this.router.createUrlTree(
|
||||
['/sources/callback/' + sourceType],
|
||||
{ queryParams: codeData, }
|
||||
);
|
||||
|
||||
let absUrl = this.location.prepareExternalUrl(this.urlSerializer.serialize(urlTree))
|
||||
console.log(absUrl);
|
||||
window.location.replace(absUrl)
|
||||
//redirect the browser back to this page with the code in the query string parameters
|
||||
this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import {LighthouseSourceSearch} from '../models/lighthouse/lighthouse-source-sea
|
|||
import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
|
||||
import {MedicalSourcesFilter} from './medical-sources-filter.service';
|
||||
import {OpenExternalLink} from '../../lib/utils/external_link';
|
||||
import {Router, UrlSerializer} from '@angular/router';
|
||||
import {Location} from '@angular/common';
|
||||
|
||||
export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||
|
||||
|
@ -21,8 +23,12 @@ export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 12
|
|||
})
|
||||
export class LighthouseService {
|
||||
|
||||
constructor(@Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient) {
|
||||
}
|
||||
constructor(
|
||||
@Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient,
|
||||
private router: Router,
|
||||
private urlSerializer: UrlSerializer,
|
||||
private location: Location,
|
||||
) {}
|
||||
|
||||
public searchLighthouseSources(filter: MedicalSourcesFilter): Observable<LighthouseSourceSearch> {
|
||||
if(filter.searchAfter){
|
||||
|
@ -87,11 +93,15 @@ export class LighthouseService {
|
|||
}
|
||||
|
||||
|
||||
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> {
|
||||
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata, reconnectSourceId?: string): Promise<URL> {
|
||||
const state = uuidV4()
|
||||
let sourceStateInfo = new SourceState()
|
||||
sourceStateInfo.state = state
|
||||
sourceStateInfo.source_type = sourceType
|
||||
if(reconnectSourceId){
|
||||
//if the source already exists, and we want to re-connect it (because of an expiration), we need to pass the existing source id
|
||||
sourceStateInfo.reconnect_source_id = reconnectSourceId
|
||||
}
|
||||
|
||||
// generate the authorization url
|
||||
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
||||
|
@ -283,6 +293,28 @@ export class LighthouseService {
|
|||
)
|
||||
}
|
||||
|
||||
//after waiting for the desktop code, we need to redirect to the callback page with the code in the query params
|
||||
// (which is what would have happened if we were in a browser and we were redirected as usual)
|
||||
redirectWithDesktopCode(sourceType: string, codeData: any){
|
||||
|
||||
if(!codeData){
|
||||
//if we redirected completely, no callback data will be present.
|
||||
return
|
||||
}
|
||||
|
||||
//User was shown a popup, which was closed, and data was returned using events
|
||||
//redirect to callback page with code
|
||||
|
||||
let urlTree = this.router.createUrlTree(
|
||||
['/sources/callback/' + sourceType],
|
||||
{ queryParams: codeData, }
|
||||
);
|
||||
|
||||
let absUrl = this.location.prepareExternalUrl(this.urlSerializer.serialize(urlTree))
|
||||
console.log(absUrl);
|
||||
window.location.replace(absUrl)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue