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
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
||||||
|
@ -134,7 +135,90 @@ func (s *SourceCredential) IsDynamicClient() bool {
|
||||||
return len(s.DynamicClientRegistrationMode) > 0
|
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
|
// 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 {
|
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
|
||||||
if len(s.DynamicClientRegistrationMode) == 0 {
|
if len(s.DynamicClientRegistrationMode) == 0 {
|
||||||
return fmt.Errorf("dynamic client registration mode not set")
|
return fmt.Errorf("dynamic client registration mode not set")
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
"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-onprem/backend/pkg/models"
|
||||||
"github.com/fastenhealth/fasten-sources/clients/factory"
|
"github.com/fastenhealth/fasten-sources/clients/factory"
|
||||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateSource(c *gin.Context) {
|
func CreateReconnectSource(c *gin.Context) {
|
||||||
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
||||||
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||||
|
|
||||||
|
@ -39,91 +36,13 @@ func CreateSource(c *gin.Context) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||||
return
|
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
|
err := sourceCred.RegisterDynamicClient()
|
||||||
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
|
|
||||||
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
|
|
||||||
if err != nil {
|
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})
|
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||||
return
|
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
|
//generate a JWT token and then use it to get an access token for the dynamic client
|
||||||
err = sourceCred.RefreshDynamicClientAccessToken()
|
err = sourceCred.RefreshDynamicClientAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -133,12 +52,23 @@ func CreateSource(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
err := databaseRepo.CreateSource(c, &sourceCred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorln("An error occurred while storing source credential", err)
|
logger.Errorln("An error occurred while storing source credential", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// after creating the source, we should do a bulk import (in the background)
|
// 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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||||
return
|
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{}
|
var resp map[string]interface{}
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
|
||||||
{
|
{
|
||||||
secure.GET("/summary", handler.GetSummary)
|
secure.GET("/summary", handler.GetSummary)
|
||||||
|
|
||||||
secure.POST("/source", handler.CreateSource)
|
secure.POST("/source", handler.CreateReconnectSource)
|
||||||
secure.POST("/source/manual", handler.CreateManualSource)
|
secure.POST("/source/manual", handler.CreateManualSource)
|
||||||
secure.GET("/source", handler.ListSource)
|
secure.GET("/source", handler.ListSource)
|
||||||
secure.GET("/source/:sourceId", handler.GetSource)
|
secure.GET("/source/:sourceId", handler.GetSource)
|
||||||
|
|
|
@ -43,9 +43,8 @@
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownManual">
|
<div ngbDropdownMenu aria-labelledby="dropdownManual">
|
||||||
<button ngbDropdownItem (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
|
<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 *ngIf="modalSelectedSourceListItem.source?.source_type != 'manual'" ngbDropdownItem (click)="sourceReconnectHandler(modalSelectedSourceListItem)" type="button" class="btn btn-danger">Reconnect</button>
|
||||||
<button ngbDropdownItem type="button" class="btn disabled btn-outline-danger">Reconnect</button>
|
<button ngbDropdownItem (click)="sourceDeleteHandler()" type="button" class="btn btn-danger">Delete</button>
|
||||||
<button ngbDropdownItem (click)="sourceDeleteHandler()" type="button" class="btn btn-outline-danger">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
|
<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 {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {Location} from '@angular/common';
|
import {Location} from '@angular/common';
|
||||||
import {EventBusService} from '../../services/event-bus.service';
|
import {EventBusService} from '../../services/event-bus.service';
|
||||||
|
import {SourceState} from '../../models/fasten/source-state';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-medical-sources-connected',
|
selector: 'app-medical-sources-connected',
|
||||||
|
@ -102,7 +103,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
||||||
});
|
});
|
||||||
this.location.replaceState(urlTree.toString());
|
this.location.replaceState(urlTree.toString());
|
||||||
|
|
||||||
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState))
|
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState)) as SourceState
|
||||||
localStorage.removeItem(callbackState)
|
localStorage.removeItem(callbackState)
|
||||||
|
|
||||||
if(callbackError && !callbackCode){
|
if(callbackError && !callbackCode){
|
||||||
|
@ -147,6 +148,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
||||||
//Create FHIR Client
|
//Create FHIR Client
|
||||||
|
|
||||||
const dbSourceCredential = new Source({
|
const dbSourceCredential = new Source({
|
||||||
|
id: expectedSourceStateInfo.reconnect_source_id,
|
||||||
source_type: sourceType,
|
source_type: sourceType,
|
||||||
|
|
||||||
authorization_endpoint: sourceMetadata.authorization_endpoint,
|
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 {
|
private getDismissReason(reason: any): string {
|
||||||
if (reason === ModalDismissReasons.ESC) {
|
if (reason === ModalDismissReasons.ESC) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ export class SourceState {
|
||||||
state: string
|
state: string
|
||||||
|
|
||||||
source_type: string //used to override the source_type for sources which have a single redirect url (eg. Epic)
|
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_verifier?: string
|
||||||
code_challenge_method?: string
|
code_challenge_method?: string
|
||||||
code_challenge?: string
|
code_challenge?: string
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-sourc
|
||||||
import {Source} from '../../models/fasten/source';
|
import {Source} from '../../models/fasten/source';
|
||||||
import {MetadataSource} from '../../models/fasten/metadata-source';
|
import {MetadataSource} from '../../models/fasten/metadata-source';
|
||||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
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 {environment} from '../../../environments/environment';
|
||||||
import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs';
|
import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
@ -17,7 +17,6 @@ import {debounceTime, distinctUntilChanged, pairwise, startWith} from 'rxjs/oper
|
||||||
import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service';
|
import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service';
|
||||||
import {FormControl, FormGroup} from '@angular/forms';
|
import {FormControl, FormGroup} from '@angular/forms';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import {Location} from '@angular/common';
|
|
||||||
|
|
||||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||||
|
|
||||||
|
@ -79,9 +78,7 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private filterService: MedicalSourcesFilterService,
|
private filterService: MedicalSourcesFilterService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private urlSerializer: UrlSerializer,
|
|
||||||
private location: Location,
|
|
||||||
) {
|
) {
|
||||||
this.filterService.filterChanges.subscribe((filterInfo) => {
|
this.filterService.filterChanges.subscribe((filterInfo) => {
|
||||||
|
|
||||||
|
@ -295,22 +292,8 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
//always close the modal
|
//always close the modal
|
||||||
this.modalService.dismissAll()
|
this.modalService.dismissAll()
|
||||||
|
|
||||||
if(!codeData){
|
//redirect the browser back to this page with the code in the query string parameters
|
||||||
//if we redirected completely, no callback data will be present.
|
this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {LighthouseSourceSearch} from '../models/lighthouse/lighthouse-source-sea
|
||||||
import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
|
import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
|
||||||
import {MedicalSourcesFilter} from './medical-sources-filter.service';
|
import {MedicalSourcesFilter} from './medical-sources-filter.service';
|
||||||
import {OpenExternalLink} from '../../lib/utils/external_link';
|
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)
|
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 {
|
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> {
|
public searchLighthouseSources(filter: MedicalSourcesFilter): Observable<LighthouseSourceSearch> {
|
||||||
if(filter.searchAfter){
|
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()
|
const state = uuidV4()
|
||||||
let sourceStateInfo = new SourceState()
|
let sourceStateInfo = new SourceState()
|
||||||
sourceStateInfo.state = state
|
sourceStateInfo.state = state
|
||||||
sourceStateInfo.source_type = sourceType
|
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
|
// generate the authorization url
|
||||||
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
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