Update his to implement new okapi spec

master
Bas Kloosterman 12 months ago
parent 2d6d973cd5
commit 02bedb4e7c
  1. 2
      his/app/src/App.js
  2. 16
      his/app/src/ManageServiceProvider.js
  3. 8
      his/app/src/Patients.js
  4. 22
      his/app/src/ServiceProviders.js
  5. 17
      his/app/src/index.js
  6. 2
      his/main.go
  7. 9
      his/model/db.go
  8. 32
      his/model/models.go
  9. 396
      his/openapiclient.go
  10. 340
      his/srv.go

@ -12,7 +12,7 @@ const App = () => {
<p style={{marginRight: 50}}><Link to="/">MYHIS</Link></p>
<div className="c-main-nav__main">
<NavLink to="patienten">Patienten</NavLink>
<NavLink to="connecties">Verbindingen</NavLink>
<NavLink to="dienstverleners">Dienstverleners</NavLink>
</div>
</nav>
<div className="c-main-content">

@ -27,15 +27,15 @@ export const needConsent = (x ) => {
return true
}
const ManageConnection = () => {
const ManageServiceProvider = () => {
const location = useLocation()
const {connId} = useParams()
const [loading, setLoading] = useState(false)
const [connection, setConnection] = useState(null)
const [connection, setServiceProvider] = useState(null)
const [services, setServices] = useState([])
useEffect(() => {
fetch(`/api/connections/${connId}`).then(x => x.json()).then(({connection, meta}) => {
setConnection(connection)
fetch(`/api/serviceProviders/${connId}`).then(x => x.json()).then(({connection, meta}) => {
setServiceProvider(connection)
setServices(meta)
})
}, [location])
@ -46,7 +46,7 @@ const ManageConnection = () => {
const modService = async (activate, connection, service) => {
setLoading(true)
await fetch(`/api/connections/${connection.ID}/services`, {
await fetch(`/api/serviceProviders/${connection.ID}/services`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
@ -54,8 +54,8 @@ const ManageConnection = () => {
body: JSON.stringify({active: activate, service: service.id})
})
await fetch(`/api/connections/${connId}`).then(x => x.json()).then(({connection, meta}) => {
setConnection(connection)
await fetch(`/api/serviceProviders/${connId}`).then(x => x.json()).then(({connection, meta}) => {
setServiceProvider(connection)
setServices(meta)
})
@ -128,4 +128,4 @@ const ManageConnection = () => {
);
};
export default ManageConnection;
export default ManageServiceProvider;

@ -2,7 +2,7 @@ import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams, useLocation } from "react-router-dom";
import "./Index.css";
import { policy, needConsent } from "./ManageConnection";
import { policy, needConsent } from "./ManageServiceProvider";
import format from "date-fns/format";
const hasConsent = (patient, service) => {
@ -54,11 +54,11 @@ const DropDown = ({patient, services, updateSubscription, loading, saveConsent,
}
const systems = services.reduce((acc, cur) => {
if (!acc[cur.Connection.System]) {
acc[cur.Connection.System] = []
if (!acc[cur.ServiceProvider.System]) {
acc[cur.ServiceProvider.System] = []
}
acc[cur.Connection.System].push(cur)
acc[cur.ServiceProvider.System].push(cur)
return acc
}, {})

@ -2,14 +2,14 @@ import React from "react";
import { useEffect, useState } from "react";
import { Link, Outlet, useNavigate, useParams, useLocation } from "react-router-dom";
import "./Index.css";
export const NewConnection = () => {
export const NewServiceProvider = () => {
let navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [url, setURL] = useState('')
const makeNewConn = () => {
setLoading(true)
fetch('/api/connections', {
fetch('/api/serviceProviders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -35,7 +35,7 @@ export const NewConnection = () => {
)
}
export const ActivateConnection = () => {
export const ActivateServiceProvider = () => {
let params = useParams()
const connId = params.connId
@ -45,7 +45,7 @@ export const ActivateConnection = () => {
const activateConn = () => {
setLoading(true)
fetch(`/api/connections/${connId}/activate`, {
fetch(`/api/serviceProviders/${connId}/activate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -73,16 +73,16 @@ export const ActivateConnection = () => {
const Connections = () => {
let location = useLocation();
const [connections, setConnections] = useState([])
const [serviceProviders, setConnections] = useState([])
useEffect(() => {
fetch('/api/connections').then(x => x.json()).then(x => setConnections(x) )
fetch('/api/serviceProviders').then(x => x.json()).then(x => setConnections(x) )
}, [location])
return (
<div>
<Outlet/>
<h1 className="t-page-header">Verbindingen</h1>
<Link to="/connecties/nieuw" className="c-button">Nieuwe verbinding</Link>
<Link to="/dienstverleners/nieuw" className="c-button">Nieuwe verbinding</Link>
<table className="c-table">
<thead>
<tr>
@ -95,7 +95,7 @@ const Connections = () => {
</tr>
</thead>
<tbody>
{connections.map(x => {
{serviceProviders.map(x => {
return (<tr key={x.ID}>
<td>{x.Addr}</td>
<td>{x.System ? x.System : 'Onbekend'}</td>
@ -105,13 +105,13 @@ const Connections = () => {
x.State == "pending" ? "" : (
x.Services.length ? x.Services.map((s) => {
return <span style={{marginRight: 10}} key={s.ID}>{s.Name}</span>
}) : <Link style={{marginRight: 10}} to={`/connecties/${x.ID}`}>Beschikbare diensten</Link>
}) : <Link style={{marginRight: 10}} to={`/dienstverleners/${x.ID}`}>Beschikbare diensten</Link>
)}</td>
<td>
{x.State == "pending" ? (
<Link to={`/connecties/${x.ID}/activeer`}>Afronden verbinding</Link>
<Link to={`/dienstverleners/${x.ID}/activeer`}>Afronden verbinding</Link>
) : (
x.Services.length ? <Link to={`/connecties/${x.ID}`}>Beheer</Link> : <Link to={`/connecties/${x.ID}`}>Activeer</Link>
x.Services.length ? <Link to={`/dienstverleners/${x.ID}`}>Beheer</Link> : <Link to={`/dienstverleners/${x.ID}`}>Activeer</Link>
)}
</td>
</tr>)

@ -8,8 +8,8 @@ import {
import App from "./App";
import Home from "./Home";
import Connections, { ActivateConnection, NewConnection } from "./Connections";
import ManageConnection from "./ManageConnection";
import ServiceProviders, { ActivateServiceProvider, NewServiceProvider } from "./ServiceProviders";
import ManageServiceProvider from "./ManageServiceProvider";
import Patients from "./Patients";
const container = document.getElementById("root");
@ -20,16 +20,11 @@ root.render(<BrowserRouter basename="/ui">
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="/patienten" element={<Patients />}/>
<Route path="/connecties" element={<Connections />}>
<Route path="nieuw" element={<NewConnection />} />
<Route path=":connId/activeer" element={<ActivateConnection />} />
<Route path="/dienstverleners" element={<ServiceProviders />}>
<Route path="nieuw" element={<NewServiceProvider />} />
<Route path=":connId/activeer" element={<ActivateServiceProvider />} />
</Route>
<Route path="/connecties/:connId" element={<ManageConnection />} />
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
<Route path="/dienstverleners/:connId" element={<ManageServiceProvider />} />
</Route>
</Routes>
</BrowserRouter>);

@ -9,7 +9,7 @@ import (
)
var localAddr = "0.0.0.0:8084"
var externalAddr = "http://localhost:8084"
var externalAddr = "https://localhost:8084"
func main() {
stop := make(chan os.Signal, 1)

@ -3,16 +3,19 @@ package model
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
"gorm.io/gorm/logger"
"whiteboxsystems.nl/okapidemo/sharedmodel"
)
func GetDB(location string) (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{})
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return nil, err
}
db.AutoMigrate(&Connection{})
db.AutoMigrate(&ServiceProvider{})
db.AutoMigrate(&sharedmodel.AuthConfig{})
db.AutoMigrate(&Service{})
db.AutoMigrate(&Patient{})

@ -4,8 +4,8 @@ import (
"time"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
"src.whiteboxsystems.nl/DECOZO/okapi"
"whiteboxsystems.nl/okapidemo/sharedmodel"
)
type ConnectionState string
@ -15,11 +15,11 @@ const (
ConnectionStateCompleted = ConnectionState("completed")
)
type Connection struct {
type ServiceProvider struct {
gorm.Model
Reference string
AuthConfigID uint
AuthConfig *sharedmodel.AuthConfig
AuthConfig *sharedmodel.XISAuthConfig
State ConnectionState
Addr string
Supplier string
@ -29,13 +29,13 @@ type Connection struct {
type Service struct {
gorm.Model
ConnectionID uint
Connection *Connection `json:"Connection,omitempty"`
ServiceProviderID uint
ServiceProvider *ServiceProvider `json:"ServiceProvider,omitempty"`
ServiceID string
Name string
Description string
SubscriptionPolicy openkv.SubscriptionPolicy
ConsentPolicy openkv.ConsentPolicy
SubscriptionPolicy okapi.SubscriptionPolicy
ConsentPolicy okapi.ConsentPolicy
AuthConfigID uint
AuthConfig *sharedmodel.AuthConfig
Subscriptions []Patient `gorm:"many2many:service_patients;"`
@ -43,14 +43,14 @@ type Service struct {
type Consent struct {
gorm.Model
ConnectionID uint
ServiceID string
PatientID uint `json:"-"`
Patient Patient `json:"-"`
ConsentGivenOn *time.Time
VerbalConsent bool
Brochure string
Brochureversion string
ServiceProviderID uint
ServiceID string
PatientID uint `json:"-"`
Patient Patient `json:"-"`
ConsentGivenOn *time.Time
VerbalConsent bool
Brochure string
Brochureversion string
}
type Patient struct {

@ -2,22 +2,36 @@ package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"regexp"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/types/known/structpb"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/his/model"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
"src.whiteboxsystems.nl/DECOZO/okapi"
"whiteboxsystems.nl/okapidemo/certgen"
"whiteboxsystems.nl/okapidemo/his/model"
"whiteboxsystems.nl/okapidemo/sharedmodel"
)
const CONN_PSK = "0000"
func toStruct(m map[string]interface{}) *structpb.Struct {
s, err := structpb.NewStruct(m)
func getUnauthenticatedClient(addr string) (openkv.OpenKVClient, error) {
if err != nil {
panic(err)
}
return s
}
func getUnauthenticatedClient(addr string) (okapi.OkAPIClient, error) {
opts := []grpc.DialOption{
grpc.WithInsecure(), // dont do this in any production env...
grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}), // Don't do this in production
),
}
conn, err := grpc.Dial(addr, opts...)
@ -26,23 +40,26 @@ func getUnauthenticatedClient(addr string) (openkv.OpenKVClient, error) {
return nil, err
}
// defer conn.Close()
return openkv.NewOpenKVClient(conn), nil
// defer serviceProvider.Close()
return okapi.NewOkAPIClient(conn), nil
}
func getAuthenticatedClient(addr, psk string) (openkv.OpenKVClient, error) {
func getAuthenticatedClient(serviceProvider *model.ServiceProvider, cert tls.Certificate) (okapi.OkAPIClient, error) {
opts := []grpc.DialOption{
grpc.WithPerRPCCredentials(makePSKAuth(psk, true)),
grpc.WithInsecure(), // dont do this in any production env...
grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
}),
),
}
conn, err := grpc.Dial(addr, opts...)
conn, err := grpc.Dial(serviceProvider.Addr, opts...)
if err != nil {
return nil, err
}
// defer conn.Close()
return openkv.NewOpenKVClient(conn), nil
return okapi.NewOkAPIClient(conn), nil
}
type PSKAuth struct {
@ -64,23 +81,33 @@ func makePSKAuth(psk string, insecure bool) *PSKAuth {
return &PSKAuth{psk, insecure}
}
func (srv *HISServer) register(addr string) (*model.Connection, error) {
func (srv *HISServer) register(addr string) (*model.ServiceProvider, error) {
client, err := getUnauthenticatedClient(addr)
if err != nil {
return nil, err
}
auth := &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Config: &openkv.AuthConfig_ApiTokenConfig{&openkv.APITokenConfig{Token: CONN_PSK}},
jwkBytes, err := certgen.PublicKeyToJWKJson(certgen.ExtractPublicKey(srv.clientCert.PrivateKey))
if err != nil {
return nil, err
}
auth := &okapi.XISAuthConfiguration{
Method: okapi.XISAuthMethod_mTLS,
Configuration: &okapi.XISAuthConfiguration_MtlsConfiguration{
MtlsConfiguration: &okapi.MTLSConfigurationParams{
PublicKey: string(jwkBytes),
},
},
}
resp, err := client.Register(context.Background(), &openkv.RegisterRequest{
OrganisationId: "00009999",
OrganisationIdSystem: "https://vektis.nl/agbz",
OrganisationDisplayName: "Praktijk de oude berg",
Auth: auth,
resp, err := client.Register(context.Background(), &okapi.RegisterRequest{
OrganisationIdentifier: "00009999",
OrganisationIdentifierType: "https://vektis.nl/agbz",
OrganisationDisplayName: "Praktijk de oude berg",
Auth: auth,
})
if err != nil {
@ -88,15 +115,11 @@ func (srv *HISServer) register(addr string) (*model.Connection, error) {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("%v", resp.Error.Message)
}
connection := &model.Connection{
connection := &model.ServiceProvider{
Addr: addr,
AuthConfig: &sharedmodel.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Raw: CONN_PSK,
AuthConfig: &sharedmodel.XISAuthConfig{
Method: int32(okapi.XISAuthMethod_mTLS),
Raw: string(jwkBytes),
},
State: model.ConnectionStatePending,
Reference: resp.Reference,
@ -104,8 +127,8 @@ func (srv *HISServer) register(addr string) (*model.Connection, error) {
meta, _ := srv.listMeta(connection)
connection.Supplier = meta.Supplier
connection.System = meta.System
connection.Supplier = meta.SupplierDisplayName
connection.System = meta.ProductName
if err := srv.data.Create(connection).Error; err != nil {
return nil, err
@ -114,164 +137,145 @@ func (srv *HISServer) register(addr string) (*model.Connection, error) {
return connection, nil
}
func (srv *HISServer) activate(conn *model.Connection, psk string) (*model.Connection, error) {
client, err := getAuthenticatedClient(conn.Addr, conn.AuthConfig.Raw)
func (srv *HISServer) activate(serviceProvider *model.ServiceProvider, psk string) (*model.ServiceProvider, error) {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
panic(err)
}
resp, err := client.CompleteRegistration(context.Background(), &openkv.CompleteRegistrationRequest{
Reference: conn.Reference,
RegistrationToken: psk,
_, err = client.CompleteRegistration(context.Background(), &okapi.CompleteRegistrationRequest{
Reference: serviceProvider.Reference,
AuthorizationToken: psk,
})
if err != nil {
log.Printf("Err in request: %v", err)
return nil, err
} else if !resp.Success {
log.Printf("success: %v; Err: %v", resp.Success, resp.Error)
return nil, fmt.Errorf("%v", resp.Error.Message)
}
meta, err := srv.listMeta(conn)
meta, err := srv.listMeta(serviceProvider)
if err != nil {
return conn, fmt.Errorf("Failed to retreive metadata: %v ", err)
return serviceProvider, fmt.Errorf("Failed to retreive metadata: %v ", err)
}
conn.State = model.ConnectionStateCompleted
conn.Supplier = meta.Supplier
conn.System = meta.System
serviceProvider.State = model.ConnectionStateCompleted
serviceProvider.Supplier = meta.SupplierDisplayName
serviceProvider.System = meta.ProductName
if err := srv.data.Save(conn).Error; err != nil {
if err := srv.data.Save(serviceProvider).Error; err != nil {
return nil, err
}
return conn, err
return serviceProvider, err
}
func (srv *HISServer) listMeta(serviceProvider *model.ServiceProvider) (*okapi.GetMetadataResponse, error) {
client, err := getUnauthenticatedClient(serviceProvider.Addr)
if err != nil {
return nil, err
}
resp, err := client.GetMetadata(context.Background(), &okapi.GetMetadataRequest{})
if err != nil {
log.Printf("Err in request: %v", err)
return nil, err
}
return resp, nil
}
func (srv *HISServer) listMeta(conn *model.Connection) (*openkv.GetMetadataResponse, error) {
client, err := getUnauthenticatedClient(conn.Addr)
func (srv *HISServer) listServices(serviceProvider *model.ServiceProvider) (*okapi.ListServicesResponse, error) {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
return nil, err
}
resp, err := client.GetMetadata(context.Background(), &openkv.GetMetadataRequest{})
resp, err := client.ListServices(context.Background(), &okapi.ListServicesRequest{})
if err != nil {
log.Printf("Err in request: %v", err)
return nil, err
} else if !resp.Success {
log.Printf("success: %v; Err: %v", resp.Success, resp.Error)
return nil, fmt.Errorf("%v", resp.Error.Message)
}
return resp, nil
}
func (srv *HISServer) enableService(conn *model.Connection, service string, active bool) error {
client, err := getAuthenticatedClient(conn.Addr, conn.AuthConfig.Raw)
func (srv *HISServer) enableService(serviceProvider *model.ServiceProvider, serviceId string) error {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
return err
}
moddedService := &model.Service{}
moddedServiceErr := srv.data.Where("connection_id = ? and service_id = ?", conn.ID, service).First(moddedService).Error
moddedServiceErr := srv.data.Where("service_provider_id = ? and service_id = ?", serviceProvider.ID, serviceId).First(moddedService).Error
if moddedServiceErr != nil && moddedServiceErr != gorm.ErrRecordNotFound {
return moddedServiceErr
}
if moddedServiceErr != nil && !active {
return nil
}
if moddedServiceErr == nil && active {
return nil
if moddedServiceErr == nil {
return fmt.Errorf("Service already activivated")
}
meta, _ := srv.listMeta(conn)
var serviceDefinition *openkv.ServiceDefinition
meta, _ := srv.listServices(serviceProvider)
var serviceDefinition *okapi.Service
for _, sd := range meta.Services {
if sd.Id == service {
if sd.Id == serviceId {
serviceDefinition = sd
}
}
if serviceDefinition == nil {
return fmt.Errorf("Invalid service: %v", service)
return fmt.Errorf("Invalid service: %v", serviceId)
}
var resp *openkv.ConfigServiceResponse
var resp *okapi.EnableServiceResponse
if m, _ := regexp.MatchString("wbx:*", service); m { // Whitebox
resp, err = client.ConfigService(context.Background(), &openkv.ConfigServiceRequest{
Service: service,
Enabled: active,
Fetch: &openkv.ServiceConfig{
if m, _ := regexp.MatchString("wbx:*", serviceId); m { // Whitebox
resp, err = client.EnableService(context.Background(), &okapi.EnableServiceRequest{
ServiceId: serviceId,
Fetch: &okapi.CallbackConfiguration{
Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
Config: map[string]string{
Configuration: toStruct(map[string]interface{}{
"url": externalAddr + "/external/api",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
}),
Auth: &okapi.ProtocolAuthConfiguration{
Method: sharedmodel.AuthMethodDecozoMTLS,
},
},
Push: &openkv.ServiceConfig{
Push: &okapi.CallbackConfiguration{
Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-push/http",
Config: map[string]string{
Configuration: toStruct(map[string]interface{}{
"url": externalAddr + "/external/api",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
},
},
})
} else if m, _ := regexp.MatchString("voorbeeld:*", service); m { // Kis
resp, err = client.ConfigService(context.Background(), &openkv.ConfigServiceRequest{
Service: service,
Enabled: active,
Fetch: &openkv.ServiceConfig{
Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
Config: map[string]string{
"url": externalAddr + "/external/api",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
},
},
Push: &openkv.ServiceConfig{
Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-push/http",
Config: map[string]string{
"url": externalAddr + "/external/api",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
}),
Auth: &okapi.ProtocolAuthConfiguration{
Method: sharedmodel.AuthMethodDecozoMTLS,
},
},
})
} else { // DVZA / FHIR
resp, err = client.ConfigService(context.Background(), &openkv.ConfigServiceRequest{
Service: service,
Enabled: active,
Fetch: &openkv.ServiceConfig{
resp, err = client.EnableService(context.Background(), &okapi.EnableServiceRequest{
ServiceId: serviceId,
Fetch: &okapi.CallbackConfiguration{
Protocol: "https://hl7.org/fhir",
Config: map[string]string{
Configuration: toStruct(map[string]interface{}{
"url": externalAddr + "/external/fhir/Patient",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
}),
Auth: &okapi.ProtocolAuthConfiguration{
Method: sharedmodel.AuthMethodDecozoBearerToken,
},
},
Push: &openkv.ServiceConfig{
Push: &okapi.CallbackConfiguration{
Protocol: "https://hl7.org/fhir",
Config: map[string]string{
Configuration: toStruct(map[string]interface{}{
"url": externalAddr + "/external/fhir/Patient",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
}),
Auth: &okapi.ProtocolAuthConfiguration{
Method: sharedmodel.AuthMethodDecozoBearerToken,
},
},
})
@ -280,94 +284,154 @@ func (srv *HISServer) enableService(conn *model.Connection, service string, acti
if err != nil {
log.Printf("Err in request: %v", err)
return err
} else if !resp.Success {
log.Printf("success: %v; Err: %v", resp.Success, resp.Error)
return fmt.Errorf("%v", resp.Error)
}
if !active {
subs := []model.Patient{}
srv.data.Model(moddedService).Association("Subscriptions").Find(&subs)
srv.data.Model(moddedService).Association("Subscriptions").Delete(subs)
srv.subscribePatients(conn, moddedService, false, subs)
return srv.data.Unscoped().Delete(moddedService).Error
}
authConfig := sharedmodel.NewAuthConfig(resp.Fetch.Auth)
return srv.data.Create(&model.Service{
ConnectionID: conn.ID,
ServiceProviderID: serviceProvider.ID,
ServiceID: serviceDefinition.Id,
Name: serviceDefinition.Name,
Description: serviceDefinition.Name,
SubscriptionPolicy: serviceDefinition.SubscriptionPolicy,
ConsentPolicy: serviceDefinition.ConsentPolicy,
AuthConfig: &sharedmodel.AuthConfig{
Method: resp.Fetch.Auth.Method,
Raw: resp.Fetch.Auth.GetApiTokenConfig().Token,
},
AuthConfig: authConfig,
}).Error
}
func (srv *HISServer) subscribePatients(conn *model.Connection, service *model.Service, active bool, patients []model.Patient) (*openkv.UpdateSubscriptionsResponse, error) {
client, err := getAuthenticatedClient(conn.Addr, conn.AuthConfig.Raw)
func (srv *HISServer) disableService(serviceProvider *model.ServiceProvider, serviceId string) error {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
return err
}
moddedService := &model.Service{}
moddedServiceErr := srv.data.Where("service_provider_id = ? and service_id = ?", serviceProvider.ID, serviceId).First(moddedService).Error
if moddedServiceErr != nil && moddedServiceErr != gorm.ErrRecordNotFound {
return moddedServiceErr
}
if moddedServiceErr != nil {
return fmt.Errorf("Service not active")
}
meta, _ := srv.listServices(serviceProvider)
var serviceDefinition *okapi.Service
for _, sd := range meta.Services {
if sd.Id == serviceId {
serviceDefinition = sd
}
}
if serviceDefinition == nil {
return fmt.Errorf("Invalid service: %v", serviceId)
}
_, err = client.DisableService(context.Background(), &okapi.DisableServiceRequest{
ServiceId: serviceId,
})
if err != nil {
log.Printf("Err in request: %v", err)
return err
}
subs := []model.Patient{}
srv.data.Model(moddedService).Association("Subscriptions").Find(&subs)
srv.data.Model(moddedService).Association("Subscriptions").Delete(subs)
srv.unsubscribePatients(serviceProvider, moddedService, subs)
return srv.data.Unscoped().Delete(moddedService).Error
}
func (srv *HISServer) subscribePatients(serviceProvider *model.ServiceProvider, service *model.Service, patients []model.Patient) (*okapi.CreateOrUpdatePatientRegistrationsResponse, error) {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
return nil, err
}
subs := []*openkv.SubscriptionData{}
subs := []*okapi.PatientRegistrationCreateOrUpdateData{}
for _, pat := range patients {
subs = append(subs, &openkv.SubscriptionData{
Subscribe: active,
Subject: &openkv.PatientMeta{
ExternalId: pat.ExternalId,
ExternalIdSystem: "http://fhir.nl/fhir/NamingSystem/bsn",
Name: pat.Name,
Birthdate: pat.Birthdate,
subs = append(subs, &okapi.PatientRegistrationCreateOrUpdateData{
Id: pat.ExternalId,
Subject: &okapi.PatientMeta{
Identifier: &okapi.Identifier{
Type: "http://fhir.nl/fhir/NamingSystem/bsn",
Value: pat.ExternalId,
},
Name: &okapi.Name{
Display: pat.Name,
},
Address: &okapi.Address{},
Birthdate: pat.Birthdate,
},
ProtocolMeta: map[string]string{
CallbackProtocolData: toStruct(map[string]interface{}{
"patientID": pat.PatientID,
},
}),
})
}
req := &openkv.UpdateSubscriptionsRequest{
ServiceId: service.ServiceID,
SubscriptionData: subs,
req := &okapi.CreateOrUpdatePatientRegistrationsRequest{
ServiceId: service.ServiceID,
Registrations: subs,
}
resp, err := client.UpdateSubscriptions(context.Background(), req)
resp, err := client.CreateOrUpdatePatientRegistrations(context.Background(), req)
if err != nil {
log.Printf("Err in request: %v", err)
return nil, err
} else if !resp.Success {
log.Printf("success: %v; Err: %v", resp.Success, resp.Errors)
}
return resp, nil
}
func (srv *HISServer) unsubscribePatients(serviceProvider *model.ServiceProvider, service *model.Service, patients []model.Patient) (*okapi.RemovePatientRegistrationsResponse, error) {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
return nil, err
}
subs := []string{}
for _, pat := range patients {
subs = append(subs, pat.ExternalId)
}
req := &okapi.RemovePatientRegistrationsRequest{
ServiceId: service.ServiceID,
Registrations: subs,
}
resp, err := client.RemovePatientRegistrations(context.Background(), req)
if err != nil {
log.Printf("Err in request: %v", err)
return nil, err
}
return resp, nil
}
func listSubscriptions(addr, service string) {
client, err := getAuthenticatedClient(addr, CONN_PSK)
func (srv *HISServer) listPatientRegistrations(serviceProvider *model.ServiceProvider, service *model.Service) ([]*okapi.PatientRegistrationData, error) {
client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
if err != nil {
panic(err)
return nil, err
}
req := &openkv.ListSubscriptionsRequest{
ServiceId: service,
req := &okapi.ListPatientRegistrationsRequest{
ServiceId: service.ServiceID,
}
if resp, err := client.ListSubscriptions(context.Background(), req); err != nil {
resp, err := client.ListPatientRegistrations(context.Background(), req)
if err != nil {
log.Printf("Err in request: %v", err)
return
} else {
if !resp.Success {
log.Printf("success: %v; Err: %v", resp.Success, resp.Error)
} else {
log.Printf("success: %v", resp)
}
return
return nil, err
}
return resp.PatientRegistrationData, nil
}

@ -2,7 +2,11 @@ package main
import (
"context"
"crypto"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
@ -12,17 +16,62 @@ import (
"time"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/credentials"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/his/model"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
"src.whiteboxsystems.nl/DECOZO/okapi"
"whiteboxsystems.nl/okapidemo/certgen"
"whiteboxsystems.nl/okapidemo/his/model"
"whiteboxsystems.nl/okapidemo/sharedmodel"
)
func loadCert() *tls.Certificate {
_, err := os.Stat("certs/client.crt")
if err != nil {
_, _, certPem, keyPem, err := certgen.GenCert("whitebox", "whitebox")
if err != nil {
panic(err)
}
if err != nil {
panic(err)
}
if err := ioutil.WriteFile("certs/client.crt", []byte(certPem), 0600); err != nil {
panic(err)
}
if err := ioutil.WriteFile("certs/client.key", []byte(keyPem), 0600); err != nil {
panic(err)
}
}
certificate, err := tls.LoadX509KeyPair("certs/client.crt", "certs/client.key")
if err != nil {
panic("Load client certification failed: " + err.Error())
}
return &certificate
}
func loadKeyPair() credentials.TransportCredentials {
certificate := loadCert()
tlsConfig := &tls.Config{
ClientAuth: tls.RequestClientCert,
Certificates: []tls.Certificate{*certificate},
// ClientCAs: capool,
}
return credentials.NewTLS(tlsConfig)
}
type HISServer struct {
srv *http.Server
inited bool
data *gorm.DB
stopTasks chan struct{}
srv *http.Server
inited bool
data *gorm.DB
stopTasks chan struct{}
clientCert tls.Certificate
}
func (srv *HISServer) LoadData(location string) error {
@ -43,7 +92,7 @@ func (srv *HISServer) ListenAndServe() {
srv.init()
}
log.Println("Listening on %v", srv.srv.Addr)
srv.srv.ListenAndServe()
srv.srv.ListenAndServeTLS("", "")
}
func (srv *HISServer) Shutdown(ctx context.Context) error {
@ -56,7 +105,6 @@ func (srv *HISServer) init() {
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./assets")
r.Use(srv.Authenticate)
r.GET("/", func(c *gin.Context) {
c.Redirect(301, "/ui")
})
@ -65,12 +113,12 @@ func (srv *HISServer) init() {
r.GET("/api/patients", srv.GetPatients)
r.POST("/api/patients/:id/consent", srv.UpdateConsent)
r.POST("/api/patients/:id/consent/:consentID", srv.DeleteConsent)
r.GET("/api/connections", srv.GetConnections)
r.POST("/api/connections", srv.NewConnection)
r.POST("/api/connections/:id/activate", srv.ActivateConnection)
r.POST("/api/connections/:id/services", srv.ModService)
r.GET("/api/connections/:id", srv.GetConnection)
r.DELETE("/api/patients/:id/consent/:consentID", srv.DeleteConsent)
r.GET("/api/serviceProviders", srv.GetServiceProviders)
r.POST("/api/serviceProviders", srv.NewServiceProvider)
r.POST("/api/serviceProviders/:id/activate", srv.ActivateServiceProvider)
r.POST("/api/serviceProviders/:id/services", srv.ModService)
r.GET("/api/serviceProviders/:id", srv.GetServiceProvider)
r.GET("/api/services", srv.GetServices)
r.POST("/api/services/:id/subscriptions", srv.UpdateSubscription)
@ -81,12 +129,12 @@ func (srv *HISServer) init() {
ticker := time.NewTicker(30 * time.Second)
srv.stopTasks = make(chan struct{})
srv.TaskOptOut()
srv.TaskSyncPatients()
go func() {
for {
select {
case <-ticker.C:
srv.TaskOptOut()
srv.TaskSyncPatients()
case <-srv.stopTasks:
ticker.Stop()
return
@ -99,45 +147,112 @@ func (srv *HISServer) GetIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}
func (srv *HISServer) TaskOptOut() {
func (srv *HISServer) diffPatients(a []*okapi.PatientRegistrationData, b []model.Patient) []model.Patient {
mappedPatients := map[string]bool{}
diff := []model.Patient{}
for _, p := range a {
mappedPatients[p.Id] = true
}
for _, p := range b {
if _, ok := mappedPatients[p.ExternalId]; !ok {
diff = append(diff, p)
}
}
return diff
}
func (srv *HISServer) intersectPatients(a []*okapi.PatientRegistrationData, b []model.Patient) []model.Patient {
mappedPatients := map[string]bool{}
intersect := []model.Patient{}
for _, p := range a {
mappedPatients[p.Id] = true
}
for _, p := range b {
if _, ok := mappedPatients[p.ExternalId]; ok {
intersect = append(intersect, p)
}
}
return intersect
}
func (srv *HISServer) TaskSyncPatients() {
services := []model.Service{}
srv.data.Where("subscription_policy = ?", openkv.SubscriptionPolicy_optout).Preload("Connection").Preload("Connection.AuthConfig").Preload("Subscriptions").Find(&services)
srv.data.Preload("ServiceProvider").Preload("ServiceProvider.AuthConfig").Preload("Subscriptions").Find(&services)
patients := []model.Patient{}
srv.data.Find(&patients)
mappedPatients := map[string]model.Patient{}
for _, pat := range patients {
mappedPatients[pat.ExternalId] = pat
}
for _, service := range services {
mappedSubscriptions := map[string]model.Patient{}
for _, sub := range service.Subscriptions {
mappedSubscriptions[sub.ExternalId] = sub
}
activePatients := []model.Patient{}
inactivePatients := []model.Patient{}
for _, p := range patients {
include := true
for _, sub := range service.Subscriptions {
if sub.ID == p.ID {
include = false
if _, ok := mappedSubscriptions[p.ExternalId]; ok {
if service.SubscriptionPolicy == okapi.SubscriptionPolicy_optout {
// Subscriptions means opted out, so unsubscribe
inactivePatients = append(inactivePatients, p)
} else {
activePatients = append(activePatients, p)
}
} else {
if service.SubscriptionPolicy == okapi.SubscriptionPolicy_optout {
activePatients = append(activePatients, p)
} else {
inactivePatients = append(inactivePatients, p)
}
}
if include {
activePatients = append(activePatients, p)
}
}
_, err := srv.subscribePatients(service.Connection, &service, true, activePatients)
currentPatients, err := srv.listPatientRegistrations(service.ServiceProvider, &service)
if err != nil {
log.Println("active patients for optout (%v) err: %v", service.Name, err)
} else {
log.Printf("sync patients for %v: %v patients subscribed", service.Name, len(activePatients))
log.Println("Could not retrieve current subscriptions: %v", service.Name, err)
continue
}
inactivePatients := service.Subscriptions
toSubscribe := srv.diffPatients(currentPatients, activePatients)
_, err = srv.subscribePatients(service.Connection, &service, false, inactivePatients)
if len(toSubscribe) > 0 {
_, err := srv.subscribePatients(service.ServiceProvider, &service, activePatients)
if err != nil {
log.Println("inactive patients for optout (%v) err: %v", service.Name, err)
} else {
log.Printf("sync patients for %v: %v patients unsubscribed", service.Name, len(inactivePatients))
if err != nil {
log.Println("active patients for optout (%v) err: %v", service.Name, err)
} else {
log.Printf("sync patients for %v: %v patients subscribed", service.Name, len(toSubscribe))
}
}
toUnSubscribe := srv.intersectPatients(currentPatients, inactivePatients)
if len(toUnSubscribe) > 0 {
_, err = srv.unsubscribePatients(service.ServiceProvider, &service, inactivePatients)
if err != nil {
log.Println("inactive patients for optout (%v) err: %v", service.Name, err)
} else {
log.Printf("sync patients for %v: %v patients unsubscribed", service.Name, len(toUnSubscribe))
}
}
if len(toSubscribe) == 0 && len(toUnSubscribe) == 0 {
log.Println("Patient sync, nothing to sync")
}
}
@ -148,24 +263,50 @@ func (srv *HISServer) Authenticate(c *gin.Context) {
return
}
authHeader := c.Request.Header.Get("Authorization")
log.Printf("authHeader: %v", authHeader)
raw := ""
method := ""
if len(c.Request.TLS.PeerCertificates) > 0 {
jwk, err := certgen.PublicKeyToJWK(c.Request.TLS.PeerCertificates[0].PublicKey)
if err != nil {
log.Printf("Error extracting public key JKW: %v", err)
c.Status(401)
c.Abort()
return
}
rawBytes, _ := jwk.Thumbprint(crypto.SHA256)
raw = fmt.Sprintf("%X", rawBytes)
method = sharedmodel.AuthMethodDecozoMTLS
} else {
raw = c.Request.Header.Get("Authorization")
method = sharedmodel.AuthMethodDecozoBearerToken
}
authConfig := &sharedmodel.AuthConfig{}
if err := srv.data.Where("raw = ?", authHeader).First(authConfig).Error; err != nil {
if err := srv.data.Where("raw = ? and method = ?", raw, method).First(authConfig).Error; err != nil {
c.Status(401)
c.Abort()
return
}
if strings.HasPrefix(c.Request.RequestURI, "/external/api") {
if srv.data.Where("auth_config_id = ? and service_id like ?", authConfig.ID, "wbx:%").First(&model.Service{}).Error == nil {
service := &model.Service{}
if srv.data.Where(
"auth_config_id = ? and service_id like ?", authConfig.ID, "wbx:%",
).First(service).Error == nil {
c.Set("authenticatedService", service)
return
}
}
if strings.HasPrefix(c.Request.RequestURI, "/external/fhir") {
if srv.data.Where("auth_config_id = ? and service_id like ?", authConfig.ID, "%:dvza").First(&model.Service{}).Error == nil {
service := &model.Service{}
if srv.data.Where(
"auth_config_id = ? and service_id like ?", authConfig.ID, "%:dvza",
).First(service).Error == nil {
c.Set("authenticatedService", service)
return
}
}
@ -184,8 +325,8 @@ func (srv *HISServer) GetPatients(c *gin.Context) {
c.JSON(200, patients)
}
func (srv *HISServer) GetConnections(c *gin.Context) {
connections := []model.Connection{}
func (srv *HISServer) GetServiceProviders(c *gin.Context) {
connections := []model.ServiceProvider{}
if err := srv.data.Preload("AuthConfig").Preload("Services").Find(&connections).Error; err != nil {
c.AbortWithError(500, err)
return
@ -196,7 +337,7 @@ func (srv *HISServer) GetConnections(c *gin.Context) {
func (srv *HISServer) GetServices(c *gin.Context) {
services := []model.Service{}
if err := srv.data.Preload("Connection").Preload("Subscriptions").Find(&services).Error; err != nil {
if err := srv.data.Preload("ServiceProvider").Preload("Subscriptions").Find(&services).Error; err != nil {
c.AbortWithError(500, err)
return
}
@ -204,15 +345,15 @@ func (srv *HISServer) GetServices(c *gin.Context) {
c.JSON(200, services)
}
func (srv *HISServer) GetConnection(c *gin.Context) {
func (srv *HISServer) GetServiceProvider(c *gin.Context) {
connID := c.Param("id")
connection := &model.Connection{}
connection := &model.ServiceProvider{}
if err := srv.data.Where("id = ?", connID).Preload("AuthConfig").Preload("Services").Find(connection).Error; err != nil {
c.AbortWithError(404, err)
return
}
serviceMeta, err := srv.listMeta(connection)
serviceMeta, err := srv.listServices(connection)
if err != nil {
c.AbortWithError(400, err)
@ -231,13 +372,19 @@ func (srv *HISServer) ModService(c *gin.Context) {
c.BindJSON(&payload)
connID := c.Param("id")
connection := &model.Connection{}
connection := &model.ServiceProvider{}
if err := srv.data.Where("id = ?", connID).Preload("AuthConfig").Preload("Services").Find(connection).Error; err != nil {
c.AbortWithError(404, err)
return
}
err := srv.enableService(connection, payload.Service, payload.Active)
var err error
if payload.Active {
err = srv.enableService(connection, payload.Service)
} else {
err = srv.disableService(connection, payload.Service)
}
if err != nil {
log.Println("err: %v", err)
@ -249,7 +396,7 @@ func (srv *HISServer) ModService(c *gin.Context) {
}
func (srv *HISServer) applyPolicy(sub bool, service *model.Service) bool {
if service.SubscriptionPolicy == openkv.SubscriptionPolicy_optout {
if service.SubscriptionPolicy == okapi.SubscriptionPolicy_optout {
return !sub
}
return sub
@ -291,7 +438,7 @@ func (srv *HISServer) UpdateSubscription(c *gin.Context) {
serviceID := c.Param("id")
service := &model.Service{}
if err := srv.data.Where("id = ?", serviceID).Preload("Connection").Preload("Connection.AuthConfig").Find(service).Error; err != nil {
if err := srv.data.Where("id = ?", serviceID).Preload("ServiceProvider").Preload("ServiceProvider.AuthConfig").Find(service).Error; err != nil {
c.AbortWithError(404, err)
return
}
@ -316,12 +463,38 @@ func (srv *HISServer) UpdateSubscription(c *gin.Context) {
return
}
_, err := srv.subscribePatients(service.Connection, service, srv.applyPolicy(payload.Active, service), []model.Patient{*patient})
if srv.applyPolicy(payload.Active, service) {
resp, err := srv.subscribePatients(service.ServiceProvider, service, []model.Patient{*patient})
if err != nil {
log.Println("err: %v", err)
c.AbortWithError(400, err)
return
if err != nil {
log.Println("err: %v", err)
c.AbortWithError(400, err)
return
}
for _, result := range resp.Results {
if result.Error != nil {
log.Printf("Error for subscriptions")
c.AbortWithStatusJSON(http.StatusBadRequest, map[string]string{"error": result.Error.Message})
return
}
}
} else {
resp, err := srv.unsubscribePatients(service.ServiceProvider, service, []model.Patient{*patient})
if err != nil {
log.Println("err: %v", err)
c.AbortWithError(400, err)
return
}
for _, result := range resp.Results {
if result.Error != nil {
log.Printf("Error for subscriptions")
c.AbortWithStatusJSON(http.StatusBadRequest, map[string]string{"error": result.Error.Message})
return
}
}
}
if payload.Active {
@ -333,7 +506,7 @@ func (srv *HISServer) UpdateSubscription(c *gin.Context) {
c.Status(201)
}
func (srv *HISServer) NewConnection(c *gin.Context) {
func (srv *HISServer) NewServiceProvider(c *gin.Context) {
var payload struct {
URL string `json:"url"`
}
@ -350,10 +523,10 @@ func (srv *HISServer) NewConnection(c *gin.Context) {
c.JSON(201, conn)
}
func (srv *HISServer) ActivateConnection(c *gin.Context) {
func (srv *HISServer) ActivateServiceProvider(c *gin.Context) {
connID := c.Param("id")
connection := &model.Connection{}
connection := &model.ServiceProvider{}
if err := srv.data.Preload("AuthConfig").Where("id = ?", connID).First(connection).Error; err != nil {
c.AbortWithError(404, err)
@ -379,7 +552,22 @@ func (srv *HISServer) ActivateConnection(c *gin.Context) {
func (srv *HISServer) GetPatient(c *gin.Context) {
id := c.Param("id")
patient := &model.Patient{}
if err := srv.data.Where("patient_id = ?", id).First(patient).Error; err == nil {
serviceAny, _ := c.Get("authenticatedService")
service, _ := serviceAny.(*model.Service)
subscribed := srv.data.Raw(
"select * from service_patients where service_id = ? and patient_id = ?",
service.ID,
patient.ID,
).Scan(&map[string]interface{}{}).RowsAffected > 0
if !srv.applyPolicy(subscribed, service) {
c.Status(http.StatusUnauthorized)
return
}
f, err := os.Open(path.Join("./data/patients", patient.FileBase+".edi"))
if err != nil {
c.Error(err)
@ -397,6 +585,20 @@ func (srv *HISServer) GetFHIRPatient(c *gin.Context) {
id := c.Query("id")
patient := &model.Patient{}
if err := srv.data.Where("patient_id = ?", id).First(patient).Error; err == nil {
serviceAny, _ := c.Get("authenticatedService")
service, _ := serviceAny.(*model.Service)
subscribed := srv.data.Raw(
"select * from service_patients where service_id = ? and patient_id = ?",
service.ID,
patient.ID,
).Scan(&map[string]interface{}{}).RowsAffected > 0
if !srv.applyPolicy(subscribed, service) {
c.Status(http.StatusUnauthorized)
return
}
f, err := os.Open(path.Join("./data/patients", patient.FileBase+".fhir.json"))
if err != nil {
c.Error(err)
@ -411,10 +613,20 @@ func (srv *HISServer) GetFHIRPatient(c *gin.Context) {
}
func NewServer(addr string) *HISServer {
srv := &HISServer{srv: &http.Server{
Addr: addr,
Handler: gin.Default(),
}}
cert := loadCert()
srv := &HISServer{
srv: &http.Server{
Addr: addr,
Handler: gin.Default(),
TLSConfig: &tls.Config{
ClientAuth: tls.RequestClientCert,
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return cert, nil
},
},
},
clientCert: *cert,
}
return srv
}

Loading…
Cancel
Save