Initial commit

master
Bas Kloosterman 2 years ago
commit 3e81a54556
  1. 1
      .gitignore
  2. 6
      dvzaservice/app/.babelrc
  3. 32
      dvzaservice/app/dist/main.js
  4. 11060
      dvzaservice/app/package-lock.json
  5. 30
      dvzaservice/app/package.json
  6. 22
      dvzaservice/app/src/App.js
  7. 53
      dvzaservice/app/src/Connection.js
  8. 37
      dvzaservice/app/src/Connections.js
  9. 10
      dvzaservice/app/src/Home.js
  10. 63
      dvzaservice/app/src/Index.css
  11. 22
      dvzaservice/app/src/Patient.js
  12. 33
      dvzaservice/app/src/Registrations.js
  13. 34
      dvzaservice/app/src/index.js
  14. 26
      dvzaservice/app/webpack.config.js
  15. 67
      dvzaservice/main.go
  16. 46
      dvzaservice/model/db.go
  17. 408
      dvzaservice/openapisrv.go
  18. 208
      dvzaservice/srv.go
  19. 13
      dvzaservice/templates/index.html
  20. 37
      go.mod
  21. 209
      go.sum
  22. 6
      his/app/.babelrc
  23. 32
      his/app/dist/main.js
  24. 11014
      his/app/package-lock.json
  25. 30
      his/app/package.json
  26. 22
      his/app/src/App.js
  27. 117
      his/app/src/Connections.js
  28. 10
      his/app/src/Home.js
  29. 180
      his/app/src/Index.css
  30. 130
      his/app/src/ManageConnection.js
  31. 287
      his/app/src/Patients.js
  32. 35
      his/app/src/index.js
  33. 26
      his/app/webpack.config.js
  34. 191
      his/data/patients/cbries.edi
  35. 130
      his/data/patients/cbries.fhir.json
  36. 293
      his/data/patients/jkorts.edi
  37. 126
      his/data/patients/jkorts.fhir.json
  38. 53
      his/main.go
  39. 48
      his/model/db.go
  40. 65
      his/model/models.go
  41. 350
      his/openapiclient.go
  42. 420
      his/srv.go
  43. 13
      his/templates/index.html
  44. 2361
      openkv/apispec.pb.go
  45. 175
      openkv/apispec.proto
  46. 285
      openkv/apispec_grpc.pb.go
  47. 7
      openkv/errors.go
  48. 36
      sharedmodel/auth.go
  49. 15
      sharedmodel/connection.go
  50. 61
      sharedmodel/model.go
  51. 42
      sharedmodel/registration.go
  52. 79
      sharedmodel/service.go
  53. 33
      sharedmodel/subscription.go
  54. 6
      whiteboxservice/app/.babelrc
  55. 32
      whiteboxservice/app/dist/main.js
  56. 10996
      whiteboxservice/app/package-lock.json
  57. 29
      whiteboxservice/app/package.json
  58. 22
      whiteboxservice/app/src/App.js
  59. 53
      whiteboxservice/app/src/Connection.js
  60. 37
      whiteboxservice/app/src/Connections.js
  61. 10
      whiteboxservice/app/src/Home.js
  62. 63
      whiteboxservice/app/src/Index.css
  63. 20
      whiteboxservice/app/src/Patient.js
  64. 33
      whiteboxservice/app/src/Registrations.js
  65. 34
      whiteboxservice/app/src/index.js
  66. 26
      whiteboxservice/app/webpack.config.js
  67. BIN
      whiteboxservice/bin/ediviewer
  68. 1
      whiteboxservice/bin/medeur.json
  69. 442
      whiteboxservice/bin/template.html
  70. 67
      whiteboxservice/main.go
  71. 71
      whiteboxservice/model/db.go
  72. 416
      whiteboxservice/openapisrv.go
  73. 277
      whiteboxservice/srv.go
  74. 13
      whiteboxservice/templates/index.html

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
]
}

@ -0,0 +1,32 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {
eval("throw new Error(\"Module parse failed: Unexpected token (8:12)\\nYou may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders\\n| const root = createRoot(container);\\n| \\n> root.render(<App tab='home' />);\");\n\n//# sourceURL=webpack://app/./src/index.js?");
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module doesn't tell about it's top-level declarations so it can't be inlined
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/index.js"]();
/******/
/******/ })()
;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"style-loader": "^3.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-json-pretty": "^2.2.0",
"react-router-dom": "^6.3.0"
}
}

@ -0,0 +1,22 @@
import React from "react";
import {
Link,
Outlet,
} from "react-router-dom";
import "./Index.css";
const App = () => {
return (
<div>
<nav className="c-main-nav">
<p style={{padding: 5, marginRight: 50}}>ACME DVZA</p>
<Link to="connecties">Connecties</Link>
<Link to="registraties">Registraties</Link>
</nav>
<div className="c-main-content">
<Outlet/>
</div>
</div>
)
};
export default App;

@ -0,0 +1,53 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import "./Index.css";
const Subscriptions = ({service}) => {
if (!service) {
return null
}
return (<table className="c-table">
<thead>
<tr>
<th>Naam</th>
<th>Bsn</th>
<th>Geboortedatum</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
{service.Subscriptions.map(x => {
return (<tr key={x.ID}>
<td>{x.SubjectName}</td>
<td>{x.SubjectExternalId}</td>
<td>{x.SubjectBirthdate}</td>
<td><Link to={`/connecties/${service.ConnectionID}/${service.ID}/${x.ID}`}>Bekijk dossier</Link></td>
</tr>)
})}
</tbody>
</table>
)
}
const Connection = () => {
let params = useParams();
const [connection, setConnection] = useState(null)
const [service, setService] = useState(null)
useEffect(() => {
fetch(`/api/connections/${params.connId}`).then(x => x.json()).then(x => setConnection(x) )
}, [])
useEffect(() => {
fetch(`/api/connections/${params.connId}/${params.serviceId}`).then(x => x.json()).then(x => setService(x) )
}, [])
console.log('connection', connection)
console.log('service', service)
return (
<div>
{(connection && service) ? (<h2>{connection.OrganisationDisplayName} ({connection.OrganisationId}) | {service.Service.Name}</h2>) : null}
{<Subscriptions service={service}/>}
</div>
);
};
export default Connection;

@ -0,0 +1,37 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import "./Index.css";
const App = () => {
const [connections, setConnections] = useState([])
useEffect(() => {
fetch('/api/connections').then(x => x.json()).then(x => setConnections(x) )
}, [])
return (
<div>
<table className="c-table">
<thead>
<tr>
<th>AGB</th>
<th>Naam</th>
<th>Geactiveerde diensten</th>
</tr>
</thead>
<tbody>
{connections.map(x => {
return (<tr key={x.ID}>
<td>{x.OrganisationId}</td>
<td>{x.OrganisationDisplayName}</td>
<td>{x.Services.length ? x.Services.map((s) => {
return <span key={s.Service.ID} style={{marginRight: 10}}><Link to={`/connecties/${x.ID}/${s.ID}`} >{s.Service.Name}</Link></span>
}) : '-'}</td>
</tr>)
})}
</tbody>
</table>
</div>
);
};
export default App;

@ -0,0 +1,10 @@
import React from "react";
import "./Index.css";
const App = () => {
return (
<div></div>
);
};
export default App;

@ -0,0 +1,63 @@
body {
font-family: helvetica;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
h2 {
margin-bottom: 35px;
}
.c-main-nav {
padding: 15px;
display: flex;
width: 100%;
margin-bottom: 50px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
}
.c-main-nav a {display: block; padding: 5px; color: #137ad4; text-decoration: none}
.c-table {
width: 100%;
}
.c-table table a {color: #137ad4; text-decoration: none}
.c-main-content {
padding: 25px;
width: 90%;
max-width: 1024;
margin: auto;
}
.c-table th, td {
text-align: left;
padding: 10px;
}
.c-table th {
background-color: rgba(0,0,0,0.1);
}
.c-button {
padding: 8px 17px;
font-size: 95%;
color: white;
background-color: #137ad4;
border: 0;
outline: 0;
cursor: pointer;
margin-bottom: 10px;
display: inline-block;
text-decoration: none;
}
.c-modal {
position: fixed;
top: 50%;
left: 50%;
}

@ -0,0 +1,22 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
var JSONPretty = require('react-json-pretty');
import JSONPrettyMon from 'react-json-pretty/themes/monikai.css';
import "./Index.css";
const Patient = () => {
let params = useParams();
const [patient, setPatient] = useState(null)
useEffect(() => {
fetch(`/api/connections/${params.connId}/${params.serviceId}/${params.patientId}`).then(x => x.text()).then(x => setPatient(JSON.parse(x)) )
}, [])
return (
<div>
{patient ? <JSONPretty id="json-pretty" theme={JSONPrettyMon} data={patient}></JSONPretty> : null}
</div>
);
};
export default Patient;

@ -0,0 +1,33 @@
import React from "react";
import { useEffect, useState } from "react";
import "./Index.css";
const App = () => {
const [registrations, setRegistrations] = useState([])
useEffect(() => {
fetch('/api/registrations').then(x => x.json()).then(x => setRegistrations(x) )
}, [])
console.log('registrations', registrations)
return (
<div>
<table className="c-table">
<tr>
<th>AGB</th>
<th>Naam</th>
<th>Referentie</th>
<th>Tijdelijk wachtwoord</th>
</tr>
{registrations.map(x => {
return (<tr key={x.ID}>
<td>{x.OrganisationId}</td>
<td>{x.OrganisationDisplayName}</td>
<td>{x.Reference}</td>
<td>{x.PSK}</td>
</tr>)
})}
</table>
</div>
);
};
export default App;

@ -0,0 +1,34 @@
import React from "react";
import { createRoot } from "react-dom/client";
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
import App from "./App";
import Home from "./Home";
import Registrations from "./Registrations";
import Connections from "./Connections";
import Connection from "./Connection";
import Patient from "./Patient";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<BrowserRouter basename="/ui">
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="/registraties" element={<Registrations />}/>
<Route path="/connecties" element={<Connections />}/>
<Route path="/connecties/:connId/:serviceId" element={<Connection />}/>
<Route path="/connecties/:connId/:serviceId/:patientId" element={<Patient />}/>
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
</Route>
</Routes>
</BrowserRouter>);

@ -0,0 +1,26 @@
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "../assets/js"),
filename: "index.js",
clean: true,
},
devtool: "source-map",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
}
};

@ -0,0 +1,67 @@
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"sync"
"google.golang.org/grpc"
"whiteboxsystems.nl/openkvpoc/openkv"
)
var srvaddr = "localhost:9999"
var patientIf = "localhost:9095"
func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
wg := &sync.WaitGroup{}
openapisrv := NewServer()
openapisrv.LoadData("./data/data.db")
opts := []grpc.ServerOption{
// grpc.UnaryInterceptor(openapisrv.EnsureValidModule),
}
grpcServer := grpc.NewServer(opts...)
go func() {
lis, err := net.Listen("tcp", srvaddr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
openkv.RegisterOpenKVServer(grpcServer, openapisrv)
log.Printf("RPC Listening on %v", srvaddr)
wg.Add(1)
grpcServer.Serve(lis)
}()
srv := NewUIServer(patientIf)
srv.data = openapisrv.data
go func() {
wg.Add(1)
srv.ListenAndServe()
}()
<-stop
go func() {
grpcServer.GracefulStop()
wg.Done()
log.Println("Shutdown RPC server")
}()
go func() {
log.Println("Shutdown UI server...")
srv.Shutdown(context.Background())
wg.Done()
log.Println("UI Server shutdown...")
}()
wg.Wait()
}

@ -0,0 +1,46 @@
package model
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
func GetDB(location string) (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{})
if err != nil {
return nil, err
}
// Migrate the schema
db.AutoMigrate(&sharedmodel.Registration{})
db.AutoMigrate(&sharedmodel.Connection{})
db.AutoMigrate(&sharedmodel.Service{})
db.AutoMigrate(&sharedmodel.AuthConfig{})
db.AutoMigrate(&sharedmodel.ProtocolConfig{})
db.AutoMigrate(&sharedmodel.ServiceConfig{})
db.AutoMigrate(&sharedmodel.Subscription{})
var cnt int64
db.Model(&sharedmodel.Service{}).Count(&cnt)
if cnt == 0 {
db.Create(&sharedmodel.Service{
Name: "MedMij DVZA",
Description: "MedMij compliant PGO koppeling",
SubscriptionPolicy: openkv.SubscriptionPolicy_optout,
ConsentPolicy: openkv.ConsentPolicy_presumed,
ServiceID: "acme:dvza",
FetchProtocols: sharedmodel.ProtocolArray{
{
"https://hl7.nl/fhir",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
PushProtocols: sharedmodel.ProtocolArray{},
})
}
return db, nil
}

@ -0,0 +1,408 @@
package main
import (
"context"
"fmt"
"log"
"github.com/gofrs/uuid"
"google.golang.org/grpc/metadata"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/dvzaservice/model"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
var errNotAuthorized = fmt.Errorf("Not Authorized")
var errInvalidService = fmt.Errorf("Invalid service")
var errActiveServiceConfig = fmt.Errorf("Service not activated")
type OpenKVServer struct {
openkv.UnimplementedOpenKVServer
data *gorm.DB
}
func (srv *OpenKVServer) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func requireConnection(db *gorm.DB, ctx context.Context) (*sharedmodel.Connection, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Printf("No metadata")
return nil, errNotAuthorized
}
connection := &sharedmodel.Connection{}
if a, ok := md["authorization"]; !ok {
log.Printf("No token provided")
return nil, errNotAuthorized
} else {
if err := db.Preload("AuthMethod").Raw(`
SELECT conn.*
FROM connections conn
JOIN auth_configs a on conn.auth_config_id = a.id WHERE a.method = ? and a.raw = ?
`, openkv.AuthMethod_APIToken, a[0]).Scan(connection).Error; err != nil {
log.Printf("Invalid token; err: %v;", err)
return nil, errNotAuthorized
}
}
return connection, nil
}
func requireService(db *gorm.DB, conn *sharedmodel.Connection, serviceID string) (*sharedmodel.ServiceConfig, error) {
service := &sharedmodel.Service{}
if err := db.Where("service_id = ?", serviceID).First(service).Error; err != nil {
return nil, errInvalidService
}
srvConfig := &sharedmodel.ServiceConfig{}
if err := db.Where("connection_id = ? and enabled = ? and service_id = ?", conn.ID, true, service.ID).First(srvConfig).Error; err != nil {
return nil, errActiveServiceConfig
}
return srvConfig, nil
}
func (srv *OpenKVServer) GetMetadata(
ctx context.Context, in *openkv.GetMetadataRequest,
) (*openkv.GetMetadataResponse, error) {
log.Printf("Got metadata request")
services := []*openkv.ServiceDefinition{}
presentServices := []sharedmodel.Service{}
srv.data.Find(&presentServices)
for _, service := range presentServices {
services = append(services, &openkv.ServiceDefinition{
Name: service.Name,
Description: service.Description,
Id: service.ServiceID,
SubscriptionPolicy: service.SubscriptionPolicy,
ConsentPolicy: service.ConsentPolicy,
FetchProtocols: service.GetFetchProtocols(),
PushProtocols: service.GetPushProtocols(),
})
}
resp := &openkv.GetMetadataResponse{
Supplier: "ACME inc.",
System: "DVZA",
Services: services,
Success: true,
}
return resp, nil
}
func (srv *OpenKVServer) Register(
ctx context.Context, in *openkv.RegisterRequest,
) (*openkv.RegisterResponse, error) {
ref, _ := uuid.NewV4()
psk, _ := uuid.NewV4()
reg := &sharedmodel.Registration{
Reference: ref.String(),
OrganisationId: in.OrganisationId,
OrganisationIdSystem: in.OrganisationIdSystem,
OrganisationDisplayName: in.OrganisationDisplayName,
PSK: psk.String()[0:6],
Status: sharedmodel.RegistrationStatusPending,
}
reg.SetAuthConfig(in.Auth)
srv.data.Create(reg)
resp := &openkv.RegisterResponse{
Reference: ref.String(),
Success: true,
}
log.Printf("Got registration request from %v; ref: %v; PSK: %v", reg.OrganisationDisplayName, reg.Reference, reg.PSK)
return resp, nil
}
func (srv *OpenKVServer) CompleteRegistration(
ctx context.Context, in *openkv.CompleteRegistrationRequest,
) (*openkv.CompleteRegistrationResponse, error) {
registration := &sharedmodel.Registration{}
if err := srv.data.Preload("AuthConfig").Where("reference = ? and status = ?", in.Reference, sharedmodel.RegistrationStatusPending).First(registration).Error; err != nil {
log.Printf("Invalid ref")
return nil, errNotAuthorized
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Printf("No metadata")
return nil, errNotAuthorized
}
// The keys within metadata.MD are normalized to lowercase.
// See: https://godoc.org/google.golang.org/grpc/metadata#New
if a, ok := md["authorization"]; !ok {
log.Printf("No token provided")
return nil, errNotAuthorized
} else {
if a[0] != registration.AuthConfig.Raw {
log.Printf("Invalid token; eXpected: %v; got: %v", registration.AuthConfig.Raw, a[0])
return nil, errNotAuthorized
}
}
resp := &openkv.CompleteRegistrationResponse{}
if in.RegistrationToken != registration.PSK {
resp.Error = &openkv.Error{
Code: 1,
Message: "Invalid PSK",
}
return resp, nil
}
conn := &sharedmodel.Connection{
OrganisationId: registration.OrganisationId,
OrganisationIdSystem: registration.OrganisationIdSystem,
OrganisationDisplayName: registration.OrganisationDisplayName,
AuthConfig: registration.AuthConfig.Clone(),
}
srv.data.Create(conn)
registration.Status = sharedmodel.RegistrationStatusCompleted
srv.data.Save(registration)
resp.Success = true
return resp, nil
}
func (srv *OpenKVServer) ConfigService(
ctx context.Context, in *openkv.ConfigServiceRequest,
) (*openkv.ConfigServiceResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
service := &sharedmodel.Service{}
if err := srv.data.Where("service_id = ?", in.Service).First(service).Error; err != nil {
return nil, fmt.Errorf("Invalid service: %v", service.ServiceID)
}
cnf := &sharedmodel.ServiceConfig{}
if err := srv.data.Where("connection_id = ? and service_id = ?", conn.ID, service.ID).First(cnf); err != nil {
cnf.ConnectionID = conn.ID
cnf.ServiceID = service.ID
}
log.Printf("Update service config %v for conn: %v", cnf.Service.Name, conn.ID)
cnf.Enabled = in.Enabled
// TODO actually init authdata
cnf.PushProtocol = &sharedmodel.ProtocolConfig{
Protocol: in.Push.Protocol,
AuthConfig: sharedmodel.NewAuthConfig(in.Push.Auth),
}
cnf.PushProtocol.SetConfig(in.Push.Config)
cnf.PushProtocol.AuthConfig.Raw = "2222"
cnf.FetchProtocol = &sharedmodel.ProtocolConfig{
Protocol: in.Fetch.Protocol,
AuthConfig: sharedmodel.NewAuthConfig(in.Fetch.Auth),
}
cnf.FetchProtocol.SetConfig(in.Fetch.Config)
cnf.FetchProtocol.AuthConfig.Raw = "2222"
if cnf.ID == 0 {
if err := srv.data.Create(cnf).Error; err != nil {
return nil, err
}
} else {
if err := srv.data.Save(cnf).Error; err != nil {
return nil, err
}
}
// If disabled unsubscribe all subscriptions
if !cnf.Enabled {
srv.data.Unscoped().Where("service_config_id = ?", cnf.ID).Delete(&sharedmodel.Subscription{})
}
resp := &openkv.ConfigServiceResponse{
Success: true,
Service: in.Service,
Enabled: in.Enabled,
Fetch: &openkv.ServiceConfig{
Protocol: "https://hl7.nl/fhir",
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Config: &openkv.AuthConfig_ApiTokenConfig{&openkv.APITokenConfig{Token: "2222"}},
},
},
}
return resp, nil
}
func (srv *OpenKVServer) UpdateSubscriptions(
ctx context.Context, in *openkv.UpdateSubscriptionsRequest,
) (*openkv.UpdateSubscriptionsResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
serviceConfig, err := requireService(srv.data, conn, in.ServiceId)
if err != nil {
return nil, err
}
subscriptionErrors := []*openkv.SubscriptionError{}
if err := srv.data.Transaction(func(tx *gorm.DB) error {
for idx, sd := range in.SubscriptionData {
subscription := &sharedmodel.Subscription{}
err := srv.data.Where(
"subject_external_id = ? and subject_external_id_system = ? and service_config_id = ?",
sd.Subject.ExternalId,
sd.Subject.ExternalIdSystem,
serviceConfig.ID,
).First(subscription).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
} else if err != nil && sd.Subscribe {
sub := &sharedmodel.Subscription{
SubjectExternalId: sd.Subject.ExternalId,
SubjectExternalIdSystem: sd.Subject.ExternalIdSystem,
SubjectName: sd.Subject.Name,
SubjectBirthdate: sd.Subject.Birthdate,
ServiceConfigID: serviceConfig.ID,
}
// TODO check if it is valid metadata for the specified protocol
sub.SetProtocolMeta(sd.ProtocolMeta)
if err := srv.data.Create(sub).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be persisted; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err),
},
})
}
log.Printf("add subscription: %v", sd.Subject.ExternalId)
continue
} else if err != nil && !sd.Subscribe {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrCodeAlreadySubscribed,
Message: fmt.Sprintf("Subject with id: %v (%v) already unsubscribed", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem),
},
})
continue
} else if !sd.Subscribe {
if err := srv.data.Unscoped().Delete(subscription).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be removed; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err)},
})
}
log.Printf("delete subscription: %v", sd.Subject.ExternalId)
continue
}
subscription.SubjectExternalId = sd.Subject.ExternalId
subscription.SubjectExternalIdSystem = sd.Subject.ExternalIdSystem
subscription.SubjectName = sd.Subject.Name
subscription.SubjectBirthdate = sd.Subject.Birthdate
subscription.SetProtocolMeta(sd.ProtocolMeta)
if err := srv.data.Save(subscription).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be updated; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err)},
})
continue
}
log.Printf("update subscription: %v", sd.Subject.ExternalId)
}
return nil
}); err != nil {
return nil, err
}
resp := &openkv.UpdateSubscriptionsResponse{
Success: true,
Errors: subscriptionErrors,
}
return resp, nil
}
func (srv *OpenKVServer) ListSubscriptions(
ctx context.Context, in *openkv.ListSubscriptionsRequest,
) (*openkv.ListSubscriptionsResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
serviceConfig, err := requireService(srv.data, conn, in.ServiceId)
if err != nil {
return nil, err
}
subscriptions := []*sharedmodel.Subscription{}
srv.data.Where("service_config_id = ?", serviceConfig.ID).Find(&subscriptions)
subs := []*openkv.SubscriptionData{}
for _, s := range subscriptions {
meta := map[string]string{}
s.GetProtocolMeta(&meta)
subs = append(subs, &openkv.SubscriptionData{
Subject: &openkv.PatientMeta{
ExternalId: s.SubjectExternalId,
ExternalIdSystem: s.SubjectExternalIdSystem,
Name: s.SubjectName,
Birthdate: s.SubjectBirthdate,
},
ProtocolMeta: meta,
})
}
resp := &openkv.ListSubscriptionsResponse{
Success: true,
SubscriptionData: subs,
}
return resp, nil
}
func NewServer() *OpenKVServer {
return &OpenKVServer{}
}

@ -0,0 +1,208 @@
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/dvzaservice/model"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
type UIService struct {
srv *http.Server
inited bool
data *gorm.DB
}
func (srv *UIService) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func (srv *UIService) Addr() string {
if srv.srv == nil {
return ""
}
return srv.srv.Addr
}
func (srv *UIService) ListenAndServe() {
if !srv.inited {
srv.init()
}
log.Println("Listening on %v", srv.srv.Addr)
srv.srv.ListenAndServe()
}
func (srv *UIService) Shutdown(ctx context.Context) error {
return srv.srv.Shutdown(ctx)
}
func (srv *UIService) init() {
r := srv.srv.Handler.(*gin.Engine)
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./assets")
r.Use(srv.Authenticate)
r.GET("/", func(c *gin.Context) {
c.Redirect(301, "/ui")
})
r.GET("/ui", srv.GetIndex)
r.GET("/ui/*page", srv.GetIndex)
r.GET("/api/connections", srv.GetConnections)
r.GET("/api/connections/:connID", srv.GetConnection)
r.GET("/api/connections/:connID/:serviceID", srv.GetSubscriptions)
r.GET("/api/connections/:connID/:serviceID/:patientID", srv.GetPatient)
r.GET("/api/registrations", srv.GetRegistrations)
// r.GET("/api/systems/:sysid/patients", srv.GetPatients)
// r.GET("/api/systems/:sysid/patients/:patid", srv.GetPatient)
srv.inited = true
}
func (srv *UIService) GetIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}
func (srv *UIService) GetConnection(c *gin.Context) {
connID := c.Param("connID")
connection := &sharedmodel.Connection{}
srv.data.Where("id = ?", connID).Find(&connection)
c.JSON(200, connection)
}
func (srv *UIService) GetSubscriptions(c *gin.Context) {
connID := c.Param("connID")
serviceID := c.Param("serviceID")
serviceConfig := &sharedmodel.ServiceConfig{}
srv.data.Preload("Service").Preload("Subscriptions").Where("connection_id = ? and id = ?", connID, serviceID).Find(&serviceConfig)
c.JSON(200, serviceConfig)
}
func (srv *UIService) GetPatient(c *gin.Context) {
connID := c.Param("connID")
serviceID := c.Param("serviceID")
patientID := c.Param("patientID")
patient := &sharedmodel.Subscription{}
serviceConfig := &sharedmodel.ServiceConfig{}
srv.data.Preload("FetchProtocol").Preload("FetchProtocol.AuthConfig").Preload("Service").Where("connection_id = ? and id = ?", connID, serviceID).Find(&serviceConfig)
srv.data.Where("service_config_id = ? and id = ?", serviceID, patientID).Find(&patient)
protoconfig := map[string]string{}
protometa := map[string]string{}
err := serviceConfig.FetchProtocol.UnmarshalConfig(&protoconfig)
log.Println(err, protoconfig)
err = patient.GetProtocolMeta(&protometa)
log.Println(err, protometa)
url := fmt.Sprintf("%v?id=%v", protoconfig["url"], protometa["patientID"])
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", serviceConfig.FetchProtocol.AuthConfig.Raw)
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.AbortWithError(500, err)
return
}
io.Copy(c.Writer, resp.Body)
}
func (srv *UIService) Authenticate(c *gin.Context) {
// authHeader := c.Request.Header.Get("Authorization")
// log.Printf("authHeader: %v", authHeader)
// if authHeader != "1111" {
// c.Status(401)
// c.Abort()
// }
}
func (srv *UIService) GetConnections(c *gin.Context) {
connections := []*sharedmodel.Connection{}
srv.data.Preload("Services").Preload("Services.Service").Find(&connections)
c.JSON(200, connections)
}
func (srv *UIService) GetRegistrations(c *gin.Context) {
registrations := []*sharedmodel.Registration{}
srv.data.Where("status = ?", sharedmodel.RegistrationStatusPending).Find(&registrations)
c.JSON(200, registrations)
}
// func (srv *UIService) GetSystems(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
// func (srv *UIService) GetPatients(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
// func (srv *UIService) GetPatient(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
func NewUIServer(addr string) *UIService {
srv := &UIService{srv: &http.Server{
Addr: addr,
Handler: gin.Default(),
}}
return srv
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ACME - DVZA</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/js/index.js"></script>
</body>
</html>

@ -0,0 +1,37 @@
module whiteboxsystems.nl/openkvpoc
go 1.18
require (
github.com/gin-gonic/gin v1.8.1
github.com/gofrs/uuid v4.2.0+incompatible
google.golang.org/grpc v1.48.0
google.golang.org/protobuf v1.28.0
gorm.io/driver/sqlite v1.3.6
gorm.io/gorm v1.23.8
)
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

209
go.sum

@ -0,0 +1,209 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
]
}

@ -0,0 +1,32 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {
eval("throw new Error(\"Module parse failed: Unexpected token (8:12)\\nYou may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders\\n| const root = createRoot(container);\\n| \\n> root.render(<App tab='home' />);\");\n\n//# sourceURL=webpack://app/./src/index.js?");
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module doesn't tell about it's top-level declarations so it can't be inlined
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/index.js"]();
/******/
/******/ })()
;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"style-loader": "^3.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"dependencies": {
"date-fns": "^2.29.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0"
}
}

@ -0,0 +1,22 @@
import React from "react";
import {
Link,
Outlet,
} from "react-router-dom";
import "./Index.css";
const App = () => {
return (
<div>
<nav className="c-main-nav">
<p style={{padding: 5, marginRight: 50}}>MYHIS</p>
<Link to="patienten">Patienten</Link>
<Link to="connecties">Connecties</Link>
</nav>
<div className="c-main-content">
<Outlet/>
</div>
</div>
);
};
export default App;

@ -0,0 +1,117 @@
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 = () => {
let navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [url, setURL] = useState('')
const makeNewConn = () => {
setLoading(true)
fetch('/api/connections', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({url: url})
}).then(x => x.json()).then(x => {
setLoading(false)
navigate('..', {replace: true})
})
}
return (
<div className="c-modal-overlay">
<div className="c-modal">
<div className="c-form-row">
<input className="c-input" value={url} onInput={e => setURL(e.target.value)}type="text" placeholder="url"/>
</div>
<div className="c-modal-buttons">
<button className="c-button" onClick={makeNewConn} disabled={loading}>Verbind</button>
</div>
</div>
</div>
)
}
export const ActivateConnection = () => {
let params = useParams()
const connId = params.connId
let navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [psk, setPSK] = useState('')
const activateConn = () => {
setLoading(true)
fetch(`/api/connections/${connId}/activate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({psk: psk})
}).then(x => x.json()).then(x => {
setLoading(false)
navigate('..', {replace: true})
})
}
return (
<div className="c-modal-overlay">
<div className="c-modal">
<div className="c-form-row">
<input className="c-input" value={psk} onInput={e => setPSK(e.target.value)}type="text" placeholder="tijdelijk wachtwoord"/>
</div>
<div className="c-modal-buttons">
<button className="c-button" onClick={activateConn} disabled={loading}>Activeer</button>
</div>
</div>
</div>
)
}
const Connections = () => {
let location = useLocation();
const [connections, setConnections] = useState([])
useEffect(() => {
fetch('/api/connections').then(x => x.json()).then(x => setConnections(x) )
}, [location])
return (
<div>
<Outlet/>
<Link to="/connecties/nieuw" className="c-button">Nieuwe connectie</Link>
<table className="c-table">
<thead>
<tr>
<th>Adres</th>
<th>Systeem</th>
<th>Leverancier</th>
<th>Status</th>
<th>Actieve diensten</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
{connections.map(x => {
return (<tr key={x.ID}>
<td>{x.Addr}</td>
<td>{x.System ? x.System : 'Onbekend'}</td>
<td>{x.Supplier ? x.Supplier : 'Onbekend'}</td>
<td>{x.State == "pending" ? 'in afwachting' : 'actief'}</td>
<td>{x.Services.length ? x.Services.map((s) => {
return <span style={{marginRight: 10}} key={s.ID}>{s.Name}</span>
}) : '-'}</td>
<td>
{x.State == "pending" ? <Link to={`/connecties/${x.ID}/activeer`}>Activeer</Link> : <Link to={`/connecties/${x.ID}`}>Beheer</Link>}
</td>
</tr>)
})}
</tbody>
</table>
</div>
);
};
export default Connections;

@ -0,0 +1,10 @@
import React from "react";
import "./Index.css";
const App = () => {
return (
<div></div>
);
};
export default App;

@ -0,0 +1,180 @@
body {
font-family: helvetica;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
h2 {
margin-bottom: 35px;
}
.c-main-nav {
padding: 15px;
display: flex;
width: 100%;
margin-bottom: 50px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
}
.c-main-nav a {display: block; padding: 5px; color: #137ad4; text-decoration: none}
.c-table {
width: 100%;
}
.c-table table a {color: #137ad4; text-decoration: none}
.c-main-content {
padding: 25px;
width: 90%;
max-width: 1024;
margin: auto;
}
.c-table th, td {
text-align: left;
padding: 10px;
}
.c-table th {
background-color: rgba(0,0,0,0.1);
}
.c-button {
padding: 8px 17px;
font-size: 95%;
color: white;
background-color: #137ad4;
border: 0;
outline: 0;
cursor: pointer;
margin-bottom: 10px;
display: inline-block;
text-decoration: none;
}
.c-button--sm {
padding: 4px 8px;
font-size: 80%;
margin: 0;
}
.c-button:disabled {
opacity: 0.4;
cursor: default;
}
.c-modal {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
background-color: white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
padding: 25px 25px 0 25px;
width: 95%;
max-width: 350px;
}
.c-modal-buttons {
padding-top: 25px;
display: flex;
justify-content: flex-end;
}
.c-modal-buttons button {
margin-left: 8px;
}
.c-modal__close {
position: absolute;
top: 0px;
right: 7px;
font-size: 110%;
cursor: pointer;
}
.c-modal-overlay {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.3);
}
.c-form-row {
margin-bottom: 10px;
}
.c-input {
padding: 5px;
width: 100%;
border: 0;
border-bottom: 1px solid rgba(0,0,0,0.5);
display: block;
outline: none;
font-size: 95%;
}
input:disabled {
opacity: 0.5;
}
.c-input:focus {
border-bottom: 1px solid rgba(0,0,0,1);
}
.c-dropdown {
position: relative;
}
.c-dropdown__option {
white-space: nowrap;
padding: 10px;
}
.c-dropdown__system {
position: relative;
}
.c-dropdown__ul {
display: none;
position: absolute;
top: 0;
right: 0;
background-color: white;
transform: translateX(100%);
display: none;
z-index: 999;
box-shadow: 2px 2px 35px rgb(0 0 0 / 10%);
border: 1px solid rgba(0,0,0,0.1);
}
.c-dropdown__options {
display: none;
z-index: 999;
font-size: 85%;
background: white;
top: -15px;
position: absolute;
box-shadow: 2px 2px 35px rgb(0 0 0 / 10%);
left: -12px;
border: 1px solid rgba(0,0,0,0.1);
}
.c-dropdown__system-option {
white-space: nowrap;
padding: 10px;
display: block;
}
.c-dropdown--open .c-dropdown__options {
display: block;
}
.c-dropdown__system:hover .c-dropdown__ul {
display: block;
}

@ -0,0 +1,130 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, Outlet, useNavigate, useParams, useLocation } from "react-router-dom";
import "./Index.css";
export const policy = (x ) => {
if (x.subscriptionPolicy == 1 || x.SubscriptionPolicy == 1) {
return 'opt-in'
}
if (x.subscriptionPolicy == 2 || x.SubscriptionPolicy == 2) {
return 'opt-out'
}
return 'onbekend'
}
export const needConsent = (x ) => {
if (x.consentPolicy == 1 || x.ConsentPolicy == 1) {
return true
}
if (x.consentPolicy == 2 || x.ConsentPolicy == 2) {
return false
}
return true
}
const ManageConnection = () => {
const location = useLocation()
const {connId} = useParams()
const [loading, setLoading] = useState(false)
const [connection, setConnection] = useState(null)
const [services, setServices] = useState([])
useEffect(() => {
fetch(`/api/connections/${connId}`).then(x => x.json()).then(({connection, meta}) => {
setConnection(connection)
setServices(meta)
})
}, [location])
const isActive = (connection, service) => {
return connection.Services.map(x => x.ServiceID).includes(service.id)
}
const modService = async (activate, connection, service) => {
setLoading(true)
await fetch(`/api/connections/${connection.ID}/services`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({active: activate, service: service.id})
})
await fetch(`/api/connections/${connId}`).then(x => x.json()).then(({connection, meta}) => {
setConnection(connection)
setServices(meta)
})
setLoading(false)
}
return (
<div>
<Outlet/>
<table className="c-table">
<thead>
<tr>
<th>Adres</th>
<th>Leverancier</th>
</tr>
</thead>
<tbody>
<tr>
<td>{connection && connection.Addr}</td>
<td>{connection && (connection.Supplier ? connection.Supplier : 'Onbekend')}</td>
</tr>
</tbody>
</table>
<table className="c-table">
<thead>
<tr>
<th>Naam</th>
<th>Omschrijving</th>
<th>Aanmeldpolicy</th>
<th>Toestemmingspolicy</th>
<th>Status</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
{
services.map((x) => {
return (<tr key={x.id}>
<td>{x.name}</td>
<td>{x.description}</td>
<td>{policy(x)}</td>
<td>{needConsent(x) ? 'expliciet' : 'verondersteld'}</td>
<td>{isActive(connection, x) ? 'actief' : 'inactief'}</td>
<td>{
isActive(connection, x) ? (
<button
onClick={e => modService(false, connection, x)}
disabled={loading}
className="c-button c-button--sm"
>
Deactiveer
</button>
) : (
<button
onClick={e => modService(true, connection, x)}
disabled={loading}
className="c-button c-button--sm"
>
Activeer
</button>
)
}</td>
</tr>)
})
}
</tbody>
</table>
</div>
);
};
export default ManageConnection;

@ -0,0 +1,287 @@
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 format from "date-fns/format";
import formatRelativeWithOptions from "date-fns/esm/fp/formatRelativeWithOptions/index";
const hasConsent = (patient, service) => {
let consent = patient.Consent.filter(x => x.ServiceID == service.ServiceID)[0]
if (!consent) {
return false
}
consent.ConsentGivenOn = format(new Date(consent.ConsentGivenOn), "yyyy-MM-dd")
return consent
}
const ConsentLink = ({patient, service, setConsentId}) => {
const consent = hasConsent(patient, service)
return (
<span>
(<a href="#" onClick={e => {
e.stopPropagation()
e.preventDefault()
setConsentId([patient, service])
}}>{ consent ? `toestemming gegeven op: ${format(new Date(consent.ConsentGivenOn), 'dd/MM/yyyy')}` : "toestemming vereist"}</a>)
</span>
)
}
const DropDown = ({patient, services, updateSubscription, loading, saveConsent, removeConsent}) => {
const [open, _setOpen] = useState(false)
const [consentId, setConsentId] = useState(false)
const close = () => {
_setOpen(false)
window.removeEventListener('click', close)
}
const setOpen = (x) => {
_setOpen(x)
window.addEventListener('click', close)
}
const isSubscribed = (patient, service) => {
const present = service.Subscriptions.map(x => x.ID).indexOf(patient.ID) != -1
return policy(service) == 'opt-out' ? !present : present
}
const applyPolicy = (checked, service) => {
return policy(service) == 'opt-out' ? !checked : checked
}
const systems = services.reduce((acc, cur) => {
if (!acc[cur.Connection.System]) {
acc[cur.Connection.System] = []
}
acc[cur.Connection.System].push(cur)
return acc
}, {})
return (
<div className={["c-dropdown", open ? "c-dropdown--open" : null].filter(Boolean).join(' ')}>
<a
className="c-button c-button--sm"
onClick={e => e.preventDefault() || e.stopPropagation() || setOpen(true)}
>
Opties
</a>
<div
className="c-dropdown__options"
onClick={e => e.stopPropagation()}
>
{Object.keys(systems).map(sys => {
return (
<div className="c-dropdown__system" key={sys}>
{consentId ? <ConsentModal
close={_ => setConsentId(false)}
loading={loading}
service={consentId ? consentId[1] : null}
patient={consentId ? consentId[0] : null}
saveConsent={saveConsent}
removeConsent={removeConsent}
/> : null}
<span className="c-dropdown__system-option">{sys}</span>
<div className="c-dropdown__ul">
{
systems[sys].map( x => {
return (
<div className="c-dropdown__option" key={x.ID}>
<label className="c-dropdown__label">
<span><input
type="checkbox"
checked={isSubscribed(patient, x)}
disabled={loading}
readOnly={loading}
onChange={(e) => {
e.stopPropagation()
const enable = applyPolicy(e.target.checked, x)
updateSubscription(patient, x, enable)
enable && setConsentId([patient, x])
}} name="" id=""
/> {x.Name} {needConsent(x) ? <ConsentLink saveConsent={saveConsent} setConsentId={setConsentId} patient={patient} service={x}/> : null}</span>
</label>
</div>
)
})
}
</div>
</div>
)
})}
</div>
</div>
)
}
export const ConsentModal = ({loading, close, service, patient, saveConsent, removeConsent}) => {
const [consent, setConsent] = useState(
hasConsent(patient, service)
? Object.assign({}, hasConsent(patient, service), {PatientID: patient.ID, EnableBrochure: !!hasConsent(patient, service).Brochure,})
: {
PatientID: patient.ID,
ServiceID: service.ServiceID,
ConsentGivenOn: format(new Date(), "yyyy-dd-MM"),
VerbalConsent: false,
EnableBrochure: false,
Brochure: "",
Brochureversion: "",
}
)
return (
<div className="c-modal-overlay" onClick={e => e.stopPropagation()}>
<div className="c-modal">
<a onClick={close} className="c-modal__close">x</a>
<div className="c-form-row">
<label htmlFor="">Toestemming gegeven op</label>
<input className="c-input" onChange={x => setConsent(Object.assign({}, consent, {ConsentGivenOn: x.target.value}))} value={consent.ConsentGivenOn} type="date"/>
</div>
<div className="c-form-row" style={{display: "flex"}}>
<input style={{marginRight: 5}} checked={consent.VerbalConsent} onChange={x => {
x.stopPropagation()
setConsent(Object.assign({}, consent, {VerbalConsent: x.target.checked}))
}} type="checkbox"/>
<label htmlFor="">Mondeling geïnformeerd</label>
</div>
<div className="c-form-row" style={{display: "flex"}}>
<input style={{marginRight: 5}} checked={consent.EnableBrochure} onChange={x => {
x.stopPropagation()
setConsent(Object.assign({}, consent, {EnableBrochure: x.target.checked}))
}} type="checkbox"/>
<label htmlFor="">Geïnformeerd d.m.v. brochure</label>
</div>
<div className="c-form-row">
<label htmlFor="" style={{opacity: consent.EnableBrochure ? 1 : 0.3}}>Brochure</label>
<input className="c-input" disabled={!consent.EnableBrochure} onChange={x => setConsent(Object.assign({}, consent, {Brochure: x.target.value}))} value={consent.Brochure} type="text"/>
</div>
<div className="c-form-row">
<label htmlFor="" style={{opacity: consent.EnableBrochure ? 1 : 0.3}}>Brochure versie</label>
<input className="c-input" disabled={!consent.EnableBrochure} onChange={x => setConsent(Object.assign({}, consent, {Brochureversion: x.target.value}))} value={consent.Brochureversion} type="text"/>
</div>
<div className="c-modal-buttons">
<button className="c-button" disabled={loading} onClick={async x => {
await removeConsent(Object.assign({}, consent))
close()
}}>Toestemming intrekken</button>
<button className="c-button" disabled={loading} onClick={async x => {
await saveConsent(Object.assign({}, consent))
close()
}}>Opslaan</button>
</div>
</div>
</div>
)
}
const Patients = () => {
const location = useLocation()
const [loading, setLoading] = useState(false)
const [patients, setPatients] = useState([])
const [services, setServices] = useState([])
const updateServices = async () => fetch(`/api/services`).then(x => x.json()).then(x => setServices(x))
const updatePatients = async () => fetch(`/api/patients`).then(x => x.json()).then(x => setPatients(x))
useEffect(() => {
updatePatients()
}, [location])
useEffect(() => {
updateServices()
}, [location])
const updateSubscription = async (patient, service, active) => {
setLoading(true)
await fetch(`/api/services/${service.ID}/subscriptions`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({active: active, patient: patient.ID})
})
await updateServices()
setLoading(false)
}
const saveConsent = async (consent) => {
setLoading(true)
consent.ConsentGivenOn = new Date(consent.ConsentGivenOn)
if (!consent.EnableBrochure) {
consent.Brochure = ""
consent.Brochureversion = ""
}
await fetch(`/api/patients/${consent.PatientID}/consent`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(consent)
})
await updatePatients()
setLoading(false)
}
const removeConsent = async (consent) => {
setLoading(true)
if (consent.ID) {
await fetch(`/api/patients/${consent.PatientID}/consent/${consent.ID}`, {
method: "DELETE"
})
}
await updatePatients()
setLoading(false)
}
return (
<div>
<table className="c-table">
<thead>
<tr>
<th>Acties</th>
<th>Naam</th>
<th>Geboortedatum</th>
<th>BSN</th>
</tr>
</thead>
<tbody>
{patients.map(x => {
return (<tr key={x.ID}>
<td>
{services.length ? <DropDown
updateSubscription={updateSubscription}
patient={x}
services={services}
loading={loading}
saveConsent={saveConsent}
removeConsent={removeConsent}
/> : null}
</td>
<td>{x.Name}</td>
<td>{x.Birthdate}</td>
<td>{x.ExternalId}</td>
</tr>)
})}
</tbody>
</table>
</div>
);
};
export default Patients;

@ -0,0 +1,35 @@
import React from "react";
import { createRoot } from "react-dom/client";
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
import App from "./App";
import Home from "./Home";
import Connections, { ActivateConnection, NewConnection } from "./Connections";
import ManageConnection from "./ManageConnection";
import Patients from "./Patients";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<BrowserRouter basename="/ui">
<Routes>
<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>
<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>
</Routes>
</BrowserRouter>);

@ -0,0 +1,26 @@
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "../assets/js"),
filename: "index.js",
clean: true,
},
devtool: "source-map",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
}
};

@ -0,0 +1,191 @@
UNA:+.? '
UNB+UNOC:3+whitebox.sender@medeur.mcs-net.nl+whitebox.receiver@medeur.mcs-net.nl+220801:1417+63794960256627'
UNH+63794960256627+MEDEUR:3:3:IT:MWNH12'
BGM+APD'
DTM+137:202208011417:203'
RFF+TN:63794960256146'
RFF+HIS:TETRAHIS'
S01+1'
NAD+MS+00000001:AGB:VEK++whitebox.sender@medeur.mcs-net.nl'
ADR+WO:PH+1:ONBEKEND'
S01+2'
NAD+MR+00000000:AGB:VEK++whitebox.receiver@medeur.mcs-net.nl'
ADR+WO:PH+1:ONBEKEND'
S01+3'
NAD+BV+0:AGB:VEK++[nb]'
S01+4'
NAD+BV+0:AGB:VEK'
S02+1'
PNA+PAT+3927:LOK:229922999+++GN:Bries+EN+TI+RN:Charlotte+VL:C'
ADR+HO:PH+1:Mendelweg:32+Leiden+2333CS'
COM+071-5256747:80'
RFF+WVB:0'
DTM+329:19270505:102'
PDI+2'
S03+1+PRO+:::-1000'
DTM+194:20010430:102'
S04+1'
CIN+DI+T90.02:ICPC1V00:NHG:Diabetes mellitus type 2'
FTX+ACB+++Diabetes mellitus type 2'
S03+2+PRO+:::-1001'
DTM+194:19960629:102'
S04+1'
CIN+DI+K86:ICPC1V00:NHG:Essenti&#235;le hypertensie zonder orgaanbeschadiging'
FTX+ACB+++Essenti??le hypertensie zonder orgaanbeschadiging'
S03+3+PRO+:::-1002'
DTM+194:20021023:102'
S04+1'
CIN+DI+L90:ICPC1V00:NHG:Gonartrose'
FTX+ACB+++Gonartrose'
S03+4+PRO+:::-1003'
DTM+194:20020211:102'
S04+1'
CIN+DI+L84:ICPC1V00:NHG:Artrose/spondylose wervelkolom'
FTX+ACB+++Artrose/spondylose wervelkolom'
S03+5+PRO+:::-1004'
DTM+194:19980630:102'
S04+1'
CIN+DI+T82:ICPC1V00:NHG:Adipositas (Quetelet-index &#62;30)'
FTX+ACB+++adipositas (quet.index 38)'
S03+6+PRO+:::-1005'
DTM+194:19960225:102'
S04+1'
CIN+DI+L95:ICPC1V00:NHG:Osteoporose'
FTX+ACB+++osteopenie twk ?+ lwk'
S03+7+PRO+:::-1006'
DTM+194:19810629:102'
S04+1'
CIN+DI+D92:ICPC1V00:NHG:Diverticulose/diverticulitis'
FTX+ACB+++Diverticulose/diverticulitis'
S03+8+PRO+:::-1007'
DTM+194:20020311:102'
S04+1'
CIN+DI+F92:ICPC1V00:NHG:Staar'
FTX+ACB+++Cataract/staar'
S03+9+CI'
DTM+194:19960101:102'
S04+1'
CIN+DI+18:THE040:KMP:HYPERTENSIE+A'
S03+10+CI'
DTM+194:20001101:102'
S04+1'
CIN+DI+190:THE040:KMP:DIABETES MELLITUS+A'
S06+8000+3:WCIA14V3:NHG:Consult'
DTM+193:20220706145207:204'
RFF+G1:4'
S07+1+S'
FTX+LIN+++Waarnemer Praktijk J. Test - guido?:
ik voel me wat geagiteerd'
PTY+ATT+1'
DTM+145:000000:402'
S07+1+O'
FTX+LIN+++maar het gaat al beter'
PTY+ATT+1'
DTM+145:000001:402'
S07+1+P'
FTX+LIN+++over een maan dweer afspreke n (na vakantie)'
PTY+ATT+1'
DTM+145:000002:402'
S06+8001+3:WCIA14V3:NHG:Consult'
DTM+193:20220505111510:204'
RFF+G1:4'
S07+1+S'
FTX+LIN+++Waarnemer Praktijk J. Test - guido?:
test'
PTY+ATT+1'
DTM+145:000000:402'
S07+1+O'
FTX+LIN+++hallob'
PTY+ATT+1'
DTM+145:000001:402'
S06+1+-1:WCIA14V3:NHG'
DTM+193:20220801141735:204'
S11+1+C+N'
CLI+VRS+56170:PRK:KMP:CANDESARTAN/HYDROCHLOORTHIAZIDE TABLET 16/12,5MG'
PTY+ATT+1'
QTY+AED:90000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170510:102'
DTM+7:20170510:102'
DTM+36:20170807:102'
S11+2+C+N'
CLI+VRS+67911:PRK:KMP:ROSUVASTATINE TABLET FO 10MG (ALS CA-ZOUT)'
PTY+ATT+1'
QTY+AED:90000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170517:102'
DTM+7:20170517:102'
DTM+36:20170814:102'
S11+3+C+N'
CLI+VRS+44423:PRK:KMP:GLIMEPIRIDE TABLET 1MG'
PTY+ATT+1'
QTY+AED:90000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170517:102'
DTM+7:20170517:102'
DTM+36:20170814:102'
S11+4+C+N'
CLI+VRS+25534:PRK:KMP:BISOPROLOL TABLET 5MG'
PTY+ATT+1'
QTY+AED:90000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20120814:102'
DTM+7:20120814:102'
DTM+36:20121111:102'
S11+5+C+N'
CLI+VRS+31402:PRK:KMP:KOOLTEER/LEVOMENTHOL SHAMPOO 75/15MG/G'
PTY+ATT+1'
QTY+AED:125000+215:THE002:KMP:Gram'
QTY+143:0'
QTY+ITC:0'
DNL+1:19::87:WCIA25V3:NHG'
DSG+B+1118:WCIA25V3:NHG:Voor uitwendig gebruik'
FTX+PRE+++1 maal per dag, shampoo, voor uitwendig gebruik'
DTM+145:000000:402'
DTM+4:20170529:102'
DTM+7:20170529:102'
DTM+36:20170529:102'
S11+6+C+N'
CLI+VRS+68438:PRK:KMP:DOXYCYCLINE DISPERTABLET 100MG'
PTY+ATT+1'
QTY+AED:6000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
DSG+B+1061:WCIA25V3:NHG:In water oplossen'
DSG+B+1361:WCIA25V3:NHG:Eerste dag een dubbele dosis'
FTX+PRE+++1 maal per dag, 1x tablet, in water oplossen, eerste dag een dubbele d:osis'
DTM+145:000000:402'
DTM+4:20170524:102'
DTM+7:20170524:102'
DTM+36:20170529:102'
S11+7+C+N'
CLI+VRS+52639:PRK:KMP:MORFINE TABLET MGA 10MG (HCL)'
PTY+ATT+1'
QTY+AED:10000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+2:19:1:100:WCIA25V3:NHG'
FTX+PRE+++2 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20161107:102'
DTM+7:20161107:102'
DTM+36:20161111:102'
UNT+186+63794960256627'
UNZ+1+63794960256627'

@ -0,0 +1,130 @@
{
"resourceType": "Patient",
"id": "2",
"meta": {
"profile": [
"http://fhir.nl/fhir/StructureDefinition/nl-core-patient"
]
},
"text": {
"status": "extensions"
},
"identifier": [
{
"system": "http://example-xis.org/fhir/NamingSystem/patientID",
"value": "229922999"
},
{
"system": "urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6",
"value": "15a0e977-8fdf-11ec-9622-020000000000"
}
],
"name": [
{
"text": "Charlotte Bries",
"family": "Bries",
"_family": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/humanname-own-name",
"valueString": "Bries"
}
]
},
"given": [
"Charlotte"
],
"_given": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier",
"valueCode": "BR"
}
]
}
]
}
],
"telecom": [
{
"system": "phone",
"value": "+31715256747",
"use": "home"
}
],
"gender": "female",
"birthDate": "1927-05-05",
"address": [
{
"extension": [
{
"url": "http://nictiz.nl/fhir/StructureDefinition/zib-AddressInformation-AddressType",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/v3/AddressUse",
"code": "PHYS",
"display": "Visit Address"
}
]
}
}
],
"use": "home",
"type": "physical",
"line": [
"Mendelweg 32"
],
"_line": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetName",
"valueString": "Mendelweg"
},
{
"url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumber",
"valueString": "32"
}
]
}
],
"city": "Leiden",
"postalCode": "2333CS",
"country": "NLD",
"_country": {
"extension": [
{
"url": "http://nictiz.nl/fhir/StructureDefinition/code-specification",
"valueCodeableConcept": {
"coding": [
{
"system": "urn:oid:2.16.840.1.113883.2.4.4.16.34",
"code": "6030",
"display": "Nederland"
}
]
}
}
]
}
}
],
"multipleBirthBoolean": false,
"generalPractitioner": [
{
"extension": [
{
"url": "http://nictiz.nl/fhir/StructureDefinition/practitionerrole-reference",
"valueReference": {
"reference": "PractitionerRole/gpdata-practitionerrole-01",
"display": "H. Teil"
}
}
],
"reference": "Practitioner/gpdata-practitioner-01",
"display": "H. Teil"
}
]
}

@ -0,0 +1,293 @@
UNA:+.? '
UNB+UNOC:3+whitebox.sender@medeur.mcs-net.nl+whitebox.receiver@medeur.mcs-net.nl+220801:1421+63794960467282'
UNH+63794960467282+MEDEUR:3:3:IT:MWNH12'
BGM+APD'
DTM+137:202208011421:203'
RFF+TN:63794960466378'
RFF+HIS:TETRAHIS'
S01+1'
NAD+MS+00000001:AGB:VEK++whitebox.sender@medeur.mcs-net.nl'
ADR+WO:PH+1:ONBEKEND'
S01+2'
NAD+MR+00000000:AGB:VEK++whitebox.receiver@medeur.mcs-net.nl'
ADR+WO:PH+1:ONBEKEND'
S01+3'
NAD+GP+0:AGB:VEK++H. Teil'
S01+4'
NAD+GP+0:AGB:VEK++Vera Teil'
S01+5'
NAD+BV+0:AGB:VEK++[nb]'
S02+1'
PNA+PAT+9085:LOK:383443830+++GN:Korts+EN+TI+RN:Jesse+VL:J'
ADR+HO:PH+1:Straat 1:HuisNr 1+Plaats 0+POSTCODE1'
COM+Nummer 1:80'
RFF+WVB:0'
DTM+329:19840101:102'
PDI+1'
S03+1+PRO+:::-1000'
DTM+194:20070417:102'
S04+1'
CIN+DI+U71:ICPC1V00:NHG:Cystitis/urineweginfectie'
FTX+ACB+++recid.UWI (multiresistente E Coli) , ocv Heldeweg'
S03+2+PRO+:::-1001'
DTM+194:20120208:102'
S04+1'
CIN+DI+U99.01:ICPC1V00:NHG:Nierfunctiestoornis/nierinsuffici&#235;ntie'
FTX+ACB+++nierinsufficientie'
S03+3+PRO+:::-1002'
DTM+194:20111021:102'
S04+1'
CIN+DI+K99.04:ICPC1V00:NHG:Chronisch veneuze insuffici&#235;ntie'
FTX+ACB+++veneuze insufficientie'
S03+4+PRO+:::-1003'
DTM+194:20130704:102'
S04+1'
CIN+DI+A20:ICPC1V00:NHG:Verzoek/gesprek over euthanasie'
FTX+ACB+++niet reanimeren, wil ook geen dialyse meer. euthanasiewens besproken'
S03+5+PRO+:::-1004'
DTM+194:20120815:102'
S04+1'
CIN+DI+F84:ICPC1V00:NHG:Maculadegeneratie'
FTX+ACB+++maculadegeneratie'
S03+6+PRO+:::-1005'
DTM+194:19970301:102'
S04+1'
CIN+DI+H86:ICPC1V00:NHG:Doofheid/slechthorendheid'
FTX+ACB+++symm. perceptief verlies'
S03+7+PRO+:::-1006'
DTM+194:20170524:102'
S04+1'
CIN+DI+A05:ICPC1V00:NHG:Algehele achteruitgang'
FTX+ACB+++* ouderenzorg - contactpersoon hendrik (zoon)?: 6962817/ 06-21247715 - :thuiszorg?: geen, HH-> T -zorg?: 020-6602022'
S03+8+PRO+:::-1007'
DTM+194:20151013:102'
S04+1'
CIN+DI+K77:ICPC1V00:NHG:Decompensatio cordis'
FTX+ACB+++licht hartfalen'
S03+9+PRO+:::-1008'
DTM+194:19930401:102'
S04+1'
CIN+DI+K86:ICPC1V00:NHG:Essenti&#235;le hypertensie zonder orgaanbeschadiging'
FTX+ACB+++hypertensie (wrsch langer)'
S03+10+PRO+:::-1009'
DTM+194:20160316:102'
S04+1'
CIN+DI+A05:ICPC1V00:NHG:Algehele achteruitgang'
FTX+ACB+++Thuiszorg?: Amstelring Andy Heatlie tel 6101641'
S03+11+PRO+:::-1010'
DTM+194:19970301:102'
S04+1'
CIN+DI+S77.01:ICPC1V00:NHG:Basaalcelcarcinoom'
FTX+ACB+++rec. basaliomen'
S03+12+PRO+:::-1011'
DTM+194:20121018:102'
S04+1'
CIN+DI+S77:ICPC1V00:NHG:Maligniteit huid/subcutis'
FTX+ACB+++morbus Bowen li schouderblad'
S03+13+PRO+:::-1012'
DTM+194:20110405:102'
S04+1'
CIN+DI+L90:ICPC1V00:NHG:Gonartrose'
FTX+ACB+++gonarthrose re, total knee'
S03+14+PRO+:::-1013'
DTM+194:20090312:102'
S04+1'
CIN+DI+L89:ICPC1V00:NHG:Coxartrose'
FTX+ACB+++totale heupprothese links , ocv Kooyman'
S03+15+PRO+:::-1014'
DTM+194:20080901:102'
S04+1'
CIN+DI+L89:ICPC1V00:NHG:Coxartrose'
FTX+ACB+++totale heupprothese rechts , ocv Kooyman/Riekerhof'
S03+16+PRO+:::-1015'
DTM+194:20030804:102'
S04+1'
CIN+DI+A85:ICPC1V00:NHG:Geneesmiddelbijwerking'
FTX+ACB+++Augmentin-allergie'
S03+17+PRO+:::-1016'
DTM+194:19860301:102'
S04+1'
CIN+DI+L91:ICPC1V00:NHG:Andere artrose/verwante aandoening'
FTX+ACB+++arthrose handen , ocv RRC'
S03+18+PRO+:::-1017'
DTM+194:19840301:102'
S04+1'
CIN+DI+L76:ICPC1V00:NHG:Andere fractuur'
FTX+ACB+++sternumfractuur na ongeval'
S03+19+PRO+:::-1018'
DTM+194:19820301:102'
S04+1'
CIN+DI+D06:ICPC1V00:NHG:Andere gelokaliseerde buikpijn'
FTX+ACB+++laparoscopie (adnexitis??) , opname SV'
S03+20+PRO+:::-1019'
DTM+194:19440301:102'
S04+1'
CIN+DI+D88:ICPC1V00:NHG:Appendicitis'
FTX+ACB+++appendectomie'
S03+21+EPI+:::-1020'
DTM+194:20121119:102'
S04+1'
CIN+DI+S17:ICPC1V00:NHG:Schaafwond/schram/blaar'
FTX+ACB+++ontstoken wond'
S03+22+EPI+:::-1021'
DTM+194:20000101:102'
S04+1'
CIN+DI+L14:ICPC1V00:NHG:Been/dijbeen symptomen/klachten'
FTX+ACB+++pijn benen eci.'
S03+23+EPI+:::-1022'
DTM+194:20110928:102'
S04+1'
CIN+DI+Z18:ICPC1V00:NHG:Probleem met ziekte kind'
FTX+ACB+++ziekte bij kinderen'
S03+24+EPI+:::-1023'
DTM+194:20170720:102'
S04+1'
CIN+DI+S16:ICPC1V00:NHG:Buil/kneuzing/contusie intacte huid'
FTX+ACB+++hematoom'
S03+25+EPI+:::-1024'
DTM+194:20170404:102'
S04+1'
CIN+DI+L81.02:ICPC1V00:NHG:Ribcontusie'
FTX+ACB+++Ribcontusie li'
S03+26+CI'
DTM+194:19970101:102'
S04+1'
CIN+DI+18:THE040:KMP:HYPERTENSIE+A'
S03+27+CI'
DTM+194:20151013:102'
S04+1'
CIN+DI+72:THE040:KMP:HARTFALEN+A'
S03+28+CI'
DTM+194:20120208:102'
S04+1'
CIN+DI+137:THE040:KMP:NIERFUNCTIE, VERMINDERDE+A'
S03+29+CI'
S04+1'
FTX+ACB+++Penicillines'
S03+30+CI'
S04+1'
FTX+ACB+++Amoxicilline/ampicilline'
S03+31+CI'
S04+1'
FTX+ACB+++Carbapenems'
S03+32+CI'
S04+1'
CIN+STA+23167:REC750:KMP:Amoxicilline+A'
FTX+ACB+++Amoxicilline'
S03+33+CI'
S04+1'
CIN+STA+31232:REC750:KMP:Clavulaanzuur+A'
FTX+ACB+++Clavulaanzuur'
S06+8000+3:WCIA14V3:NHG:Consult'
DTM+193:20220519151832:204'
RFF+G1:4'
S07+1+S'
FTX+LIN+++Waarnemer Praktijk J. Test - guido?:
hallo '
PTY+ATT+1'
DTM+145:000000:402'
S07+1+O'
FTX+LIN+++daar'
PTY+ATT+1'
DTM+145:000001:402'
S06+19+-1:WCIA14V3:NHG'
DTM+193:20220801142106:204'
S11+1+C+N'
CLI+VRS+3662:PRK:KMP:SPIRONOLACTON TABLET 25MG'
PTY+ATT+1'
QTY+AED:7000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+2+C+N'
CLI+VRS+5967:PRK:KMP:FUROSEMIDE TABLET 40MG'
PTY+ATT+1'
QTY+AED:14000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+2:19:1:100:WCIA25V3:NHG'
FTX+PRE+++2 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+3+C+N'
CLI+VRS+1677535:HPK:KMP:PREGABALINE CAPSULE 75MG'
PTY+ATT+1'
QTY+AED:14000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+2:19:1:6:WCIA25V3:NHG'
FTX+PRE+++2 maal per dag, 1x capsule'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+4+C+N'
CLI+VRS+102865:PRK:KMP:COLECALCIFEROL TABLET 800IE'
PTY+ATT+1'
QTY+AED:7000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+5+C+N'
CLI+VRS+26190:PRK:KMP:METOPROLOL TABLET MGA 100MG (SUCCINAAT)'
PTY+ATT+1'
QTY+AED:7000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+6+C+N'
CLI+VRS+60062:PRK:KMP:OMEPRAZOL CAPSULE MSR 20MG'
PTY+ATT+1'
QTY+AED:7000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:6:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x capsule'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+7+C+N'
CLI+VRS+67822:PRK:KMP:SIMVASTATINE TABLET FO 40MG'
PTY+ATT+1'
QTY+AED:7000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
DSG+B+1128:WCIA25V3:NHG:Voor de nacht'
FTX+PRE+++1 maal per dag, 1x tablet, voor de nacht'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
S11+9+C+N'
CLI+VRS+68624:PRK:KMP:ACETYLSALICYLZUUR TABLET 80MG'
PTY+ATT+1'
QTY+AED:7000+245:THE002:KMP:Stuk'
QTY+143:0'
QTY+ITC:0'
DNL+1:19:1:100:WCIA25V3:NHG'
FTX+PRE+++1 maal per dag, 1x tablet'
DTM+145:000000:402'
DTM+4:20170817:102'
DTM+7:20170817:102'
DTM+36:20170823:102'
UNT+775+63794960467282'
UNZ+1+63794960467282'

@ -0,0 +1,126 @@
{
"resourceType": "Patient",
"id": "1",
"meta": {
"profile": [
"http://fhir.nl/fhir/StructureDefinition/nl-core-patient"
]
},
"text": {
"status": "extensions"
},
"identifier": [
{
"system": "http://example-xis.org/fhir/NamingSystem/patientID",
"value": "383443830"
}
],
"name": [
{
"text": "Jesse Korts",
"family": "Korts",
"_family": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/humanname-own-name",
"valueString": "Korts"
}
]
},
"given": [
"Jesse"
],
"_given": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier",
"valueCode": "BR"
}
]
}
]
}
],
"telecom": [
{
"system": "phone",
"value": "+311234567",
"use": "home"
}
],
"gender": "male",
"birthDate": "1984-01-01",
"address": [
{
"extension": [
{
"url": "http://nictiz.nl/fhir/StructureDefinition/zib-AddressInformation-AddressType",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/v3/AddressUse",
"code": "PHYS",
"display": "Visit Address"
}
]
}
}
],
"use": "home",
"type": "physical",
"line": [
"Beterweg 47"
],
"_line": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetName",
"valueString": "Beterweg"
},
{
"url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumber",
"valueString": "47"
}
]
}
],
"city": "Amersfoort",
"postalCode": "3816CS",
"country": "NLD",
"_country": {
"extension": [
{
"url": "http://nictiz.nl/fhir/StructureDefinition/code-specification",
"valueCodeableConcept": {
"coding": [
{
"system": "urn:oid:2.16.840.1.113883.2.4.4.16.34",
"code": "6030",
"display": "Nederland"
}
]
}
}
]
}
}
],
"multipleBirthBoolean": false,
"generalPractitioner": [
{
"extension": [
{
"url": "http://nictiz.nl/fhir/StructureDefinition/practitionerrole-reference",
"valueReference": {
"reference": "PractitionerRole/gpdata-practitionerrole-01",
"display": "H. Teil"
}
}
],
"reference": "Practitioner/gpdata-practitioner-01",
"display": "H. Teil"
}
]
}

@ -0,0 +1,53 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
)
var srvaddr = "localhost:8888"
var patientIf = "localhost:8084"
func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
wg := &sync.WaitGroup{}
// register(srvaddr)
// fmt.Println("Enter ref: ")
// // var then variable name then variable type
// var ref string
// // Taking input from user
// fmt.Scanln(&ref)
// fmt.Println("Enter psk: ")
// var psk string
// fmt.Scanln(&psk)
// complete(srvaddr, ref, psk)
// listMeta(srvaddr)
// enableService(srvaddr, "wbx:visitelijst")
// subscribePatients(srvaddr, "wbx:visitelijst", patients)
// listSubscriptions(srvaddr, "wbx:visitelijst")
srv := NewServer(patientIf)
srv.LoadData("./data/data.db")
go func() {
wg.Add(1)
srv.ListenAndServe()
}()
<-stop
go func() {
log.Println("Shutdown server...")
srv.Shutdown(context.Background())
wg.Done()
log.Println("Server.shutdown...")
}()
wg.Wait()
}

@ -0,0 +1,48 @@
package model
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
func GetDB(location string) (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{})
if err != nil {
return nil, err
}
db.AutoMigrate(&Connection{})
db.AutoMigrate(&sharedmodel.AuthConfig{})
db.AutoMigrate(&Service{})
db.AutoMigrate(&Patient{})
db.AutoMigrate(&Consent{})
patCnt := int64(0)
db.Model(&Patient{}).Count(&patCnt)
if patCnt == 0 {
patients := []Patient{
{
ExternalId: "229922999",
ExternalIdSystem: "http://fhir.nl/fhir/NamingSystem/bsn",
Name: "C. Bries",
Birthdate: "1927-05-05",
PatientID: "1",
FileBase: "cbries",
},
{
ExternalId: "383443830",
ExternalIdSystem: "http://fhir.nl/fhir/NamingSystem/bsn",
Name: "J. Korts",
Birthdate: "1984-01-01",
PatientID: "2",
FileBase: "jkorts",
},
}
db.Create(patients)
}
return db, nil
}

@ -0,0 +1,65 @@
package model
import (
"time"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
type ConnectionState string
const (
ConnectionStatePending = ConnectionState("pending")
ConnectionStateCompleted = ConnectionState("completed")
)
type Connection struct {
gorm.Model
Reference string
AuthConfigID uint
AuthConfig *sharedmodel.AuthConfig
State ConnectionState
Addr string
Supplier string
System string
Services []Service
}
type Service struct {
gorm.Model
ConnectionID uint
Connection *Connection `json:"Connection,omitempty"`
ServiceID string
Name string
Description string
SubscriptionPolicy openkv.SubscriptionPolicy
ConsentPolicy openkv.ConsentPolicy
AuthConfigID uint
AuthConfig *sharedmodel.AuthConfig
Subscriptions []Patient `gorm:"many2many:service_patients;"`
}
type Consent struct {
gorm.Model
ConnectionID uint
ServiceID string
PatientID uint `json:"-"`
Patient Patient `json:"-"`
ConsentGivenOn *time.Time
VerbalConsent bool
Brochure string
Brochureversion string
}
type Patient struct {
gorm.Model
ExternalId string
ExternalIdSystem string
Name string
Birthdate string
PatientID string
FileBase string
Consent []Consent
}

@ -0,0 +1,350 @@
package main
import (
"context"
"fmt"
"log"
"regexp"
"google.golang.org/grpc"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/his/model"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
const CONN_PSK = "0000"
func getUnauthenticatedClient(addr string) (openkv.OpenKVClient, error) {
opts := []grpc.DialOption{
grpc.WithInsecure(), // dont do this in any production env...
}
conn, err := grpc.Dial(addr, opts...)
if err != nil {
return nil, err
}
// defer conn.Close()
return openkv.NewOpenKVClient(conn), nil
}
func getAuthenticatedClient(addr, psk string) (openkv.OpenKVClient, error) {
opts := []grpc.DialOption{
grpc.WithPerRPCCredentials(makePSKAuth(psk, true)),
grpc.WithInsecure(), // dont do this in any production env...
}
conn, err := grpc.Dial(addr, opts...)
if err != nil {
return nil, err
}
// defer conn.Close()
return openkv.NewOpenKVClient(conn), nil
}
type PSKAuth struct {
psk string
insecure bool
}
func (ma PSKAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": ma.psk,
}, nil
}
func (ma PSKAuth) RequireTransportSecurity() bool {
return !ma.insecure
}
func makePSKAuth(psk string, insecure bool) *PSKAuth {
return &PSKAuth{psk, insecure}
}
func (srv *HISServer) register(addr string) (*model.Connection, 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}},
}
resp, err := client.Register(context.Background(), &openkv.RegisterRequest{
OrganisationId: "00009999",
OrganisationIdSystem: "https://vektis.nl/agbz",
OrganisationDisplayName: "Praktijk de oude berg",
Auth: auth,
})
if err != nil {
log.Printf("Err in request: %v", err)
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("%v", resp.Error.Message)
}
connection := &model.Connection{
Addr: addr,
AuthConfig: &sharedmodel.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Raw: CONN_PSK,
},
State: model.ConnectionStatePending,
Reference: resp.Reference,
}
meta, _ := srv.listMeta(connection)
connection.Supplier = meta.Supplier
connection.System = meta.System
if err := srv.data.Create(connection).Error; err != nil {
return nil, err
}
return connection, nil
}
func (srv *HISServer) activate(conn *model.Connection, psk string) (*model.Connection, error) {
client, err := getAuthenticatedClient(conn.Addr, conn.AuthConfig.Raw)
if err != nil {
panic(err)
}
resp, err := client.CompleteRegistration(context.Background(), &openkv.CompleteRegistrationRequest{
Reference: conn.Reference,
RegistrationToken: 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)
if err != nil {
return conn, fmt.Errorf("Failed to retreive metadata: %v ", err)
}
conn.State = model.ConnectionStateCompleted
conn.Supplier = meta.Supplier
conn.System = meta.System
if err := srv.data.Save(conn).Error; err != nil {
return nil, err
}
return conn, err
}
func (srv *HISServer) listMeta(conn *model.Connection) (*openkv.GetMetadataResponse, error) {
client, err := getUnauthenticatedClient(conn.Addr)
if err != nil {
return nil, err
}
resp, err := client.GetMetadata(context.Background(), &openkv.GetMetadataRequest{})
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)
if err != nil {
return err
}
moddedService := &model.Service{}
moddedServiceErr := srv.data.Where("connection_id = ? and service_id = ?", conn.ID, service).First(moddedService).Error
if moddedServiceErr != nil && moddedServiceErr != gorm.ErrRecordNotFound {
return moddedServiceErr
}
if moddedServiceErr != nil && !active {
return nil
}
if moddedServiceErr == nil && active {
return nil
}
meta, _ := srv.listMeta(conn)
var serviceDefinition *openkv.ServiceDefinition
for _, sd := range meta.Services {
if sd.Id == service {
serviceDefinition = sd
}
}
if serviceDefinition == nil {
return fmt.Errorf("Invalid service: %v", service)
}
var resp *openkv.ConfigServiceResponse
if m, _ := regexp.MatchString("wbx:*", service); m { // Whitebox
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": "http://localhost:8084/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": "http://localhost:8084/external/api",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
},
},
})
} else { // DVZA / FHIR
resp, err = client.ConfigService(context.Background(), &openkv.ConfigServiceRequest{
Service: service,
Enabled: active,
Fetch: &openkv.ServiceConfig{
Protocol: "https://hl7.org/fhir",
Config: map[string]string{
"url": "http://localhost:8084/external/fhir/Patient",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
},
},
Push: &openkv.ServiceConfig{
Protocol: "https://hl7.org/fhir",
Config: map[string]string{
"url": "http://localhost:8084/external/fhir/Patient",
},
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
},
},
})
}
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
}
return srv.data.Create(&model.Service{
ConnectionID: conn.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,
},
}).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)
if err != nil {
return nil, err
}
subs := []*openkv.SubscriptionData{}
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,
},
ProtocolMeta: map[string]string{
"patientID": pat.PatientID,
},
})
}
req := &openkv.UpdateSubscriptionsRequest{
ServiceId: service.ServiceID,
SubscriptionData: subs,
}
resp, err := client.UpdateSubscriptions(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 nil, err
}
return resp, nil
}
func listSubscriptions(addr, service string) {
client, err := getAuthenticatedClient(addr, CONN_PSK)
if err != nil {
panic(err)
}
req := &openkv.ListSubscriptionsRequest{
ServiceId: service,
}
if resp, err := client.ListSubscriptions(context.Background(), req); 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
}
}

@ -0,0 +1,420 @@
package main
import (
"context"
"io"
"log"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/his/model"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
type HISServer struct {
srv *http.Server
inited bool
data *gorm.DB
stopTasks chan struct{}
}
func (srv *HISServer) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func (srv *HISServer) Addr() string {
if srv.srv == nil {
return ""
}
return srv.srv.Addr
}
func (srv *HISServer) ListenAndServe() {
if !srv.inited {
srv.init()
}
log.Println("Listening on %v", srv.srv.Addr)
srv.srv.ListenAndServe()
}
func (srv *HISServer) Shutdown(ctx context.Context) error {
return srv.srv.Shutdown(ctx)
}
func (srv *HISServer) init() {
r := srv.srv.Handler.(*gin.Engine)
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./assets")
r.Use(srv.Authenticate)
r.GET("/", func(c *gin.Context) {
c.Redirect(301, "/ui")
})
r.GET("/ui", srv.GetIndex)
r.GET("/ui/*page", srv.GetIndex)
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.GET("/api/services", srv.GetServices)
r.POST("/api/services/:id/subscriptions", srv.UpdateSubscription)
r.Use(srv.Authenticate)
r.GET("/external/api/patients/:id", srv.GetPatient)
r.GET("/external/fhir/Patient", srv.GetFHIRPatient)
srv.inited = true
ticker := time.NewTicker(30 * time.Second)
srv.stopTasks = make(chan struct{})
srv.TaskOptOut()
go func() {
for {
select {
case <-ticker.C:
srv.TaskOptOut()
case <-srv.stopTasks:
ticker.Stop()
return
}
}
}()
}
func (srv *HISServer) GetIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}
func (srv *HISServer) TaskOptOut() {
services := []model.Service{}
srv.data.Where("subscription_policy = ?", openkv.SubscriptionPolicy_optout).Preload("Connection").Preload("Connection.AuthConfig").Preload("Subscriptions").Find(&services)
patients := []model.Patient{}
srv.data.Find(&patients)
for _, service := range services {
activePatients := []model.Patient{}
for _, p := range patients {
include := true
for _, sub := range service.Subscriptions {
if sub.ID == p.ID {
include = false
}
}
if include {
activePatients = append(activePatients, p)
}
}
_, err := srv.subscribePatients(service.Connection, &service, true, activePatients)
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))
}
inactivePatients := service.Subscriptions
_, err = srv.subscribePatients(service.Connection, &service, false, 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(inactivePatients))
}
}
}
func (srv *HISServer) Authenticate(c *gin.Context) {
if !strings.HasPrefix(c.Request.RequestURI, "/external/") {
return
}
authHeader := c.Request.Header.Get("Authorization")
log.Printf("authHeader: %v", authHeader)
authConfig := &sharedmodel.AuthConfig{}
if err := srv.data.Where("raw = ?", authHeader).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 {
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 {
return
}
}
c.Status(401)
c.Abort()
}
func (srv *HISServer) GetPatients(c *gin.Context) {
patients := []model.Patient{}
if err := srv.data.Preload("Consent").Find(&patients).Error; err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(200, patients)
}
func (srv *HISServer) GetConnections(c *gin.Context) {
connections := []model.Connection{}
if err := srv.data.Preload("AuthConfig").Preload("Services").Find(&connections).Error; err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(200, connections)
}
func (srv *HISServer) GetServices(c *gin.Context) {
services := []model.Service{}
if err := srv.data.Preload("Connection").Preload("Subscriptions").Find(&services).Error; err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(200, services)
}
func (srv *HISServer) GetConnection(c *gin.Context) {
connID := c.Param("id")
connection := &model.Connection{}
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)
if err != nil {
c.AbortWithError(400, err)
return
}
c.JSON(200, map[string]interface{}{"connection": connection, "meta": serviceMeta.Services})
}
func (srv *HISServer) ModService(c *gin.Context) {
var payload struct {
Active bool `json:"active"`
Service string `json:"service"`
}
c.BindJSON(&payload)
connID := c.Param("id")
connection := &model.Connection{}
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)
if err != nil {
log.Println("err: %v", err)
c.AbortWithError(400, err)
return
}
c.Status(200)
}
func (srv *HISServer) applyPolicy(sub bool, service *model.Service) bool {
if service.SubscriptionPolicy == openkv.SubscriptionPolicy_optout {
return !sub
}
return sub
}
func (srv *HISServer) UpdateConsent(c *gin.Context) {
consent := &model.Consent{}
c.BindJSON(consent)
pId, _ := strconv.ParseUint(c.Param("id"), 10, 64)
consent.PatientID = uint(pId)
if consent.ID != 0 {
srv.data.Save(consent)
c.Status(200)
} else {
srv.data.Create(consent)
c.Status(201)
}
}
func (srv *HISServer) DeleteConsent(c *gin.Context) {
consent := &model.Consent{}
srv.data.Unscoped().Where("id = ? and patient_id = ?", c.Param("consentID"), c.Param("id")).Delete(consent)
c.Status(200)
}
func (srv *HISServer) UpdateSubscription(c *gin.Context) {
var payload struct {
Active bool `json:"active"`
Patient uint `json:"patient"`
}
c.BindJSON(&payload)
serviceID := c.Param("id")
service := &model.Service{}
if err := srv.data.Where("id = ?", serviceID).Preload("Connection").Preload("Connection.AuthConfig").Find(service).Error; err != nil {
c.AbortWithError(404, err)
return
}
sub := &model.Patient{}
srv.data.Model(service).Where("id = ?", payload.Patient).Association("Subscriptions").Find(sub)
if payload.Active && sub.ID != 0 {
log.Printf("No update needed: %v %v", payload.Patient, service.ServiceID)
c.Status(200)
return
} else if !payload.Active && sub.ID == 0 {
log.Printf("No update needed: %v %v", payload.Patient, service.ServiceID)
c.Status(200)
return
}
patient := &model.Patient{}
if err := srv.data.Where("id = ?", payload.Patient).Find(patient).Error; err != nil {
c.AbortWithError(404, err)
return
}
_, err := srv.subscribePatients(service.Connection, service, srv.applyPolicy(payload.Active, service), []model.Patient{*patient})
if err != nil {
log.Println("err: %v", err)
c.AbortWithError(400, err)
return
}
if payload.Active {
srv.data.Model(service).Association("Subscriptions").Append(patient)
} else {
srv.data.Model(service).Association("Subscriptions").Delete(patient)
}
c.Status(201)
}
func (srv *HISServer) NewConnection(c *gin.Context) {
var payload struct {
URL string `json:"url"`
}
c.BindJSON(&payload)
conn, err := srv.register(payload.URL)
if err != nil {
c.AbortWithError(400, err)
return
}
c.JSON(201, conn)
}
func (srv *HISServer) ActivateConnection(c *gin.Context) {
connID := c.Param("id")
connection := &model.Connection{}
if err := srv.data.Preload("AuthConfig").Where("id = ?", connID).First(connection).Error; err != nil {
c.AbortWithError(404, err)
return
}
var payload struct {
PSK string `json:"psk"`
}
c.BindJSON(&payload)
conn, err := srv.activate(connection, payload.PSK)
if err != nil {
c.AbortWithError(400, err)
return
}
c.JSON(201, conn)
}
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 {
f, err := os.Open(path.Join("./data/patients", patient.FileBase+".edi"))
if err != nil {
c.Error(err)
return
}
io.Copy(c.Writer, f)
return
}
c.JSON(404, nil)
}
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 {
f, err := os.Open(path.Join("./data/patients", patient.FileBase+".fhir.json"))
if err != nil {
c.Error(err)
return
}
io.Copy(c.Writer, f)
return
}
c.JSON(404, nil)
}
func NewServer(addr string) *HISServer {
srv := &HISServer{srv: &http.Server{
Addr: addr,
Handler: gin.Default(),
}}
return srv
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MYHIS</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/js/index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,175 @@
syntax = "proto3";
option go_package = "whiteboxsystems.nl/openkv";
service OpenKV {
// Onboarding
rpc GetMetadata (GetMetadataRequest) returns (GetMetadataResponse) {}
rpc Register (RegisterRequest) returns (RegisterResponse) {}
rpc CompleteRegistration (CompleteRegistrationRequest) returns (CompleteRegistrationResponse) {}
// Service config
rpc ConfigService (ConfigServiceRequest) returns (ConfigServiceResponse) {}
rpc UpdateSubscriptions (UpdateSubscriptionsRequest) returns (UpdateSubscriptionsResponse) {}
rpc ListSubscriptions (ListSubscriptionsRequest) returns (ListSubscriptionsResponse) {}
}
enum AuthMethod {
mTLS = 0;
APIToken = 1;
JWT = 2;
Custom = 3;
}
message MTLSConfig {
string publicKey = 1;
}
message APITokenConfig {
string token = 1;
}
message JWTConfig {
string publicKey = 1;
}
message CustomConfig {
string method = 1;
map<string, string> params = 2;
}
message Error {
int32 code = 1;
string message = 2;
}
enum SubscriptionPolicy {
subnone = 0;
optin = 1;
optout = 2;
}
enum ConsentPolicy {
consentnone = 0;
explicit = 1;
presumed = 2;
}
message ProtocolDefinition {
string protocol = 1;
repeated AuthMethod authMethods = 2;
}
message ServiceDefinition {
string id = 1;
string name = 2;
string description = 3;
SubscriptionPolicy subscriptionPolicy = 4;
ConsentPolicy consentPolicy = 5;
repeated ProtocolDefinition fetchProtocols = 6;
repeated ProtocolDefinition pushProtocols = 7;
}
message ServiceConfig {
string protocol = 1;
map<string,string> config = 2;
AuthConfig auth = 3;
}
message GetMetadataRequest {}
message GetMetadataResponse {
string supplier = 1;
string system = 2;
repeated ServiceDefinition services = 3;
bool success = 4;
Error error = 5;
}
message AuthConfig {
AuthMethod method = 1;
oneof config {
MTLSConfig mtlsConfig = 2;
APITokenConfig apiTokenConfig = 3;
JWTConfig jwtConfig = 4;
}
}
message RegisterRequest {
string organisationId = 1;
string organisationIdSystem = 2; // Type bijv AGB of BIG registratie
string organisationDisplayName = 3;
string organisationFormalName = 4;
AuthConfig auth = 5;
}
message RegisterResponse {
string reference = 1;
bool success = 2;
Error error = 3;
}
message CompleteRegistrationRequest {
string reference = 1;
string registrationToken = 2;
}
message CompleteRegistrationResponse {
bool success = 1;
Error error = 2;
}
message ConfigServiceRequest {
string service = 1;
bool enabled = 2;
ServiceConfig fetch = 3;
ServiceConfig push = 4;
}
message ConfigServiceResponse {
string service = 1;
bool enabled = 2;
ServiceConfig fetch = 3;
ServiceConfig push = 4;
bool success = 5;
Error error = 6;
}
message PatientMeta {
string externalId = 1;
string externalIdSystem = 2;
string name = 3;
string birthdate = 4;
map<string, string> custom = 5;
}
message SubscriptionData {
PatientMeta subject = 1;
bool subscribe = 2;
map<string, string> protocolMeta = 3;
}
message UpdateSubscriptionsRequest {
string serviceId = 1;
repeated SubscriptionData subscriptionData = 2;
bool atomicUpdate = 3;
}
message SubscriptionError {
int32 index = 1;
Error error = 2;
}
message UpdateSubscriptionsResponse {
bool success = 1;
repeated SubscriptionError errors = 2;
}
message ListSubscriptionsRequest {
string serviceId = 1;
}
message ListSubscriptionsResponse {
bool success = 1;
repeated SubscriptionData subscriptionData = 2;
Error error = 3;
}

@ -0,0 +1,285 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package openkv
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// OpenKVClient is the client API for OpenKV service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type OpenKVClient interface {
// Onboarding
GetMetadata(ctx context.Context, in *GetMetadataRequest, opts ...grpc.CallOption) (*GetMetadataResponse, error)
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error)
CompleteRegistration(ctx context.Context, in *CompleteRegistrationRequest, opts ...grpc.CallOption) (*CompleteRegistrationResponse, error)
// Service config
ConfigService(ctx context.Context, in *ConfigServiceRequest, opts ...grpc.CallOption) (*ConfigServiceResponse, error)
UpdateSubscriptions(ctx context.Context, in *UpdateSubscriptionsRequest, opts ...grpc.CallOption) (*UpdateSubscriptionsResponse, error)
ListSubscriptions(ctx context.Context, in *ListSubscriptionsRequest, opts ...grpc.CallOption) (*ListSubscriptionsResponse, error)
}
type openKVClient struct {
cc grpc.ClientConnInterface
}
func NewOpenKVClient(cc grpc.ClientConnInterface) OpenKVClient {
return &openKVClient{cc}
}
func (c *openKVClient) GetMetadata(ctx context.Context, in *GetMetadataRequest, opts ...grpc.CallOption) (*GetMetadataResponse, error) {
out := new(GetMetadataResponse)
err := c.cc.Invoke(ctx, "/OpenKV/GetMetadata", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *openKVClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) {
out := new(RegisterResponse)
err := c.cc.Invoke(ctx, "/OpenKV/Register", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *openKVClient) CompleteRegistration(ctx context.Context, in *CompleteRegistrationRequest, opts ...grpc.CallOption) (*CompleteRegistrationResponse, error) {
out := new(CompleteRegistrationResponse)
err := c.cc.Invoke(ctx, "/OpenKV/CompleteRegistration", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *openKVClient) ConfigService(ctx context.Context, in *ConfigServiceRequest, opts ...grpc.CallOption) (*ConfigServiceResponse, error) {
out := new(ConfigServiceResponse)
err := c.cc.Invoke(ctx, "/OpenKV/ConfigService", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *openKVClient) UpdateSubscriptions(ctx context.Context, in *UpdateSubscriptionsRequest, opts ...grpc.CallOption) (*UpdateSubscriptionsResponse, error) {
out := new(UpdateSubscriptionsResponse)
err := c.cc.Invoke(ctx, "/OpenKV/UpdateSubscriptions", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *openKVClient) ListSubscriptions(ctx context.Context, in *ListSubscriptionsRequest, opts ...grpc.CallOption) (*ListSubscriptionsResponse, error) {
out := new(ListSubscriptionsResponse)
err := c.cc.Invoke(ctx, "/OpenKV/ListSubscriptions", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// OpenKVServer is the server API for OpenKV service.
// All implementations must embed UnimplementedOpenKVServer
// for forward compatibility
type OpenKVServer interface {
// Onboarding
GetMetadata(context.Context, *GetMetadataRequest) (*GetMetadataResponse, error)
Register(context.Context, *RegisterRequest) (*RegisterResponse, error)
CompleteRegistration(context.Context, *CompleteRegistrationRequest) (*CompleteRegistrationResponse, error)
// Service config
ConfigService(context.Context, *ConfigServiceRequest) (*ConfigServiceResponse, error)
UpdateSubscriptions(context.Context, *UpdateSubscriptionsRequest) (*UpdateSubscriptionsResponse, error)
ListSubscriptions(context.Context, *ListSubscriptionsRequest) (*ListSubscriptionsResponse, error)
mustEmbedUnimplementedOpenKVServer()
}
// UnimplementedOpenKVServer must be embedded to have forward compatible implementations.
type UnimplementedOpenKVServer struct {
}
func (UnimplementedOpenKVServer) GetMetadata(context.Context, *GetMetadataRequest) (*GetMetadataResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMetadata not implemented")
}
func (UnimplementedOpenKVServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented")
}
func (UnimplementedOpenKVServer) CompleteRegistration(context.Context, *CompleteRegistrationRequest) (*CompleteRegistrationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CompleteRegistration not implemented")
}
func (UnimplementedOpenKVServer) ConfigService(context.Context, *ConfigServiceRequest) (*ConfigServiceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ConfigService not implemented")
}
func (UnimplementedOpenKVServer) UpdateSubscriptions(context.Context, *UpdateSubscriptionsRequest) (*UpdateSubscriptionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateSubscriptions not implemented")
}
func (UnimplementedOpenKVServer) ListSubscriptions(context.Context, *ListSubscriptionsRequest) (*ListSubscriptionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListSubscriptions not implemented")
}
func (UnimplementedOpenKVServer) mustEmbedUnimplementedOpenKVServer() {}
// UnsafeOpenKVServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to OpenKVServer will
// result in compilation errors.
type UnsafeOpenKVServer interface {
mustEmbedUnimplementedOpenKVServer()
}
func RegisterOpenKVServer(s grpc.ServiceRegistrar, srv OpenKVServer) {
s.RegisterService(&OpenKV_ServiceDesc, srv)
}
func _OpenKV_GetMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMetadataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(OpenKVServer).GetMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/OpenKV/GetMetadata",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(OpenKVServer).GetMetadata(ctx, req.(*GetMetadataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _OpenKV_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(OpenKVServer).Register(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/OpenKV/Register",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(OpenKVServer).Register(ctx, req.(*RegisterRequest))
}
return interceptor(ctx, in, info, handler)
}
func _OpenKV_CompleteRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CompleteRegistrationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(OpenKVServer).CompleteRegistration(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/OpenKV/CompleteRegistration",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(OpenKVServer).CompleteRegistration(ctx, req.(*CompleteRegistrationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _OpenKV_ConfigService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ConfigServiceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(OpenKVServer).ConfigService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/OpenKV/ConfigService",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(OpenKVServer).ConfigService(ctx, req.(*ConfigServiceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _OpenKV_UpdateSubscriptions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateSubscriptionsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(OpenKVServer).UpdateSubscriptions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/OpenKV/UpdateSubscriptions",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(OpenKVServer).UpdateSubscriptions(ctx, req.(*UpdateSubscriptionsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _OpenKV_ListSubscriptions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSubscriptionsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(OpenKVServer).ListSubscriptions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/OpenKV/ListSubscriptions",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(OpenKVServer).ListSubscriptions(ctx, req.(*ListSubscriptionsRequest))
}
return interceptor(ctx, in, info, handler)
}
// OpenKV_ServiceDesc is the grpc.ServiceDesc for OpenKV service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var OpenKV_ServiceDesc = grpc.ServiceDesc{
ServiceName: "OpenKV",
HandlerType: (*OpenKVServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetMetadata",
Handler: _OpenKV_GetMetadata_Handler,
},
{
MethodName: "Register",
Handler: _OpenKV_Register_Handler,
},
{
MethodName: "CompleteRegistration",
Handler: _OpenKV_CompleteRegistration_Handler,
},
{
MethodName: "ConfigService",
Handler: _OpenKV_ConfigService_Handler,
},
{
MethodName: "UpdateSubscriptions",
Handler: _OpenKV_UpdateSubscriptions_Handler,
},
{
MethodName: "ListSubscriptions",
Handler: _OpenKV_ListSubscriptions_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "apispec.proto",
}

@ -0,0 +1,7 @@
package openkv
const (
ErrUnknown = -9999
ErrCodeAlreadySubscribed = iota + 1
ErrServiceException
)

@ -0,0 +1,36 @@
package sharedmodel
import (
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
)
type AuthConfig struct {
gorm.Model
Raw string
Method openkv.AuthMethod
}
func (cfg AuthConfig) Clone() *AuthConfig {
return &AuthConfig{
Raw: cfg.Raw,
Method: cfg.Method,
}
}
func NewAuthConfig(cfg *openkv.AuthConfig) *AuthConfig {
authConfig := &AuthConfig{
Method: cfg.Method,
}
switch cfg.Method {
case openkv.AuthMethod_JWT:
authConfig.Raw = cfg.GetJwtConfig().GetPublicKey()
case openkv.AuthMethod_APIToken:
authConfig.Raw = cfg.GetApiTokenConfig().GetToken()
case openkv.AuthMethod_mTLS:
authConfig.Raw = cfg.GetMtlsConfig().GetPublicKey()
}
return authConfig
}

@ -0,0 +1,15 @@
package sharedmodel
import (
"gorm.io/gorm"
)
type Connection struct {
gorm.Model
OrganisationId string
OrganisationIdSystem string
OrganisationDisplayName string
AuthConfigID uint
AuthConfig *AuthConfig
Services []ServiceConfig
}

@ -0,0 +1,61 @@
package sharedmodel
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"whiteboxsystems.nl/openkvpoc/openkv"
)
type Protocol struct {
Protocol string
AuthMethods AuthMethodArray
}
type ProtocolArray []Protocol
// Scan scan value into Jsonb, implements sql.Scanner interface
func (j *ProtocolArray) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal ProtocolArray value:", value))
}
result := []Protocol{}
err := json.Unmarshal(bytes, &result)
*j = ProtocolArray(result)
return err
}
// Value return json value, implement driver.Valuer interface
func (j ProtocolArray) Value() (driver.Value, error) {
if len(j) == 0 {
return nil, nil
}
return json.Marshal(j)
}
type AuthMethodArray []openkv.AuthMethod
// Scan scan value into Jsonb, implements sql.Scanner interface
func (j *AuthMethodArray) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal AuthMethodArray value:", value))
}
result := []openkv.AuthMethod{}
err := json.Unmarshal(bytes, &result)
*j = AuthMethodArray(result)
return err
}
// Value return json value, implement driver.Valuer interface
func (j AuthMethodArray) Value() (driver.Value, error) {
if len(j) == 0 {
return nil, nil
}
return json.Marshal(j)
}

@ -0,0 +1,42 @@
package sharedmodel
import (
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
)
type RegistrationStatus string
const (
RegistrationStatusPending = RegistrationStatus("pending")
RegistrationStatusCompleted = RegistrationStatus("completed")
)
type Registration struct {
gorm.Model
OrganisationId string
OrganisationIdSystem string
OrganisationDisplayName string
AuthConfigID uint
AuthConfig *AuthConfig
Reference string
PSK string
Status RegistrationStatus
}
func (r *Registration) SetAuthConfig(cfg *openkv.AuthConfig) {
authConfig := &AuthConfig{
Method: cfg.Method,
}
switch cfg.Method {
case openkv.AuthMethod_JWT:
authConfig.Raw = cfg.GetJwtConfig().GetPublicKey()
case openkv.AuthMethod_APIToken:
authConfig.Raw = cfg.GetApiTokenConfig().GetToken()
case openkv.AuthMethod_mTLS:
authConfig.Raw = cfg.GetMtlsConfig().GetPublicKey()
}
r.AuthConfig = authConfig
}

@ -0,0 +1,79 @@
package sharedmodel
import (
"encoding/json"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
)
type Service struct {
gorm.Model
ServiceID string
Name string
Description string
SubscriptionPolicy openkv.SubscriptionPolicy
ConsentPolicy openkv.ConsentPolicy
FetchProtocols ProtocolArray `gorm:"type:text"`
PushProtocols ProtocolArray `gorm:"type:text"`
}
func (s Service) GetFetchProtocols() []*openkv.ProtocolDefinition {
protoDefs := []*openkv.ProtocolDefinition{}
for _, sd := range s.FetchProtocols {
protoDefs = append(protoDefs, &openkv.ProtocolDefinition{
Protocol: sd.Protocol,
AuthMethods: sd.AuthMethods,
})
}
return protoDefs
}
func (s Service) GetPushProtocols() []*openkv.ProtocolDefinition {
protoDefs := []*openkv.ProtocolDefinition{}
for _, sd := range s.PushProtocols {
protoDefs = append(protoDefs, &openkv.ProtocolDefinition{
Protocol: sd.Protocol,
AuthMethods: sd.AuthMethods,
})
}
return protoDefs
}
type ProtocolConfig struct {
gorm.Model
Protocol string
AuthConfigID uint
AuthConfig *AuthConfig
Config string
}
func (pc ProtocolConfig) UnmarshalConfig(in interface{}) error {
return json.Unmarshal([]byte(pc.Config), in)
}
func (pc *ProtocolConfig) SetConfig(in interface{}) error {
b, err := json.Marshal(in)
if err != nil {
return err
}
pc.Config = string(b)
return nil
}
type ServiceConfig struct {
gorm.Model
ServiceID uint
Service Service
Enabled bool
ConnectionID uint
Connection Connection `json:"-"`
PushProtocolID uint
PushProtocol *ProtocolConfig `gorm:"foreignKey:PushProtocolID"`
FetchProtocolID uint
FetchProtocol *ProtocolConfig `gorm:"foreignKey:FetchProtocolID"`
Subscriptions []*Subscription
}

@ -0,0 +1,33 @@
package sharedmodel
import (
"encoding/json"
"gorm.io/gorm"
)
type Subscription struct {
gorm.Model
SubjectExternalId string
SubjectExternalIdSystem string
SubjectName string
SubjectBirthdate string
ProtocolMeta string
ServiceConfigID uint
ServiceConfig *ServiceConfig
}
func (s Subscription) GetProtocolMeta(meta interface{}) error {
return json.Unmarshal([]byte(s.ProtocolMeta), meta)
}
func (s *Subscription) SetProtocolMeta(meta interface{}) error {
b, err := json.Marshal(meta)
if err != nil {
return err
}
s.ProtocolMeta = string(b)
return nil
}

@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
]
}

@ -0,0 +1,32 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {
eval("throw new Error(\"Module parse failed: Unexpected token (8:12)\\nYou may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders\\n| const root = createRoot(container);\\n| \\n> root.render(<App tab='home' />);\");\n\n//# sourceURL=webpack://app/./src/index.js?");
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module doesn't tell about it's top-level declarations so it can't be inlined
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/index.js"]();
/******/
/******/ })()
;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"style-loader": "^3.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0"
}
}

@ -0,0 +1,22 @@
import React from "react";
import {
Link,
Outlet,
} from "react-router-dom";
import "./Index.css";
const App = () => {
return (
<div>
<nav className="c-main-nav">
<p style={{padding: 5, marginRight: 50}}>Whitebox</p>
<Link to="connecties">Connecties</Link>
<Link to="registraties">Registraties</Link>
</nav>
<div className="c-main-content">
<Outlet/>
</div>
</div>
)
};
export default App;

@ -0,0 +1,53 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import "./Index.css";
const Subscriptions = ({service}) => {
if (!service) {
return null
}
return (<table className="c-table">
<thead>
<tr>
<th>Naam</th>
<th>Bsn</th>
<th>Geboortedatum</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
{service.Subscriptions.map(x => {
return (<tr key={x.ID}>
<td>{x.SubjectName}</td>
<td>{x.SubjectExternalId}</td>
<td>{x.SubjectBirthdate}</td>
<td><Link to={`/connecties/${service.ConnectionID}/${service.ID}/${x.ID}`}>Bekijk dossier</Link></td>
</tr>)
})}
</tbody>
</table>
)
}
const Connection = () => {
let params = useParams();
const [connection, setConnection] = useState(null)
const [service, setService] = useState(null)
useEffect(() => {
fetch(`/api/connections/${params.connId}`).then(x => x.json()).then(x => setConnection(x) )
}, [])
useEffect(() => {
fetch(`/api/connections/${params.connId}/${params.serviceId}`).then(x => x.json()).then(x => setService(x) )
}, [])
console.log('connection', connection)
console.log('service', service)
return (
<div>
{(connection && service) ? (<h2>{connection.OrganisationDisplayName} ({connection.OrganisationId}) | {service.Service.Name}</h2>) : null}
{<Subscriptions service={service}/>}
</div>
);
};
export default Connection;

@ -0,0 +1,37 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import "./Index.css";
const App = () => {
const [connections, setConnections] = useState([])
useEffect(() => {
fetch('/api/connections').then(x => x.json()).then(x => setConnections(x) )
}, [])
return (
<div>
<table className="c-table">
<thead>
<tr>
<th>AGB</th>
<th>Naam</th>
<th>Geactiveerde diensten</th>
</tr>
</thead>
<tbody>
{connections.map(x => {
return (<tr key={x.ID}>
<td>{x.OrganisationId}</td>
<td>{x.OrganisationDisplayName}</td>
<td>{x.Services.length ? x.Services.map((s) => {
return <span key={s.Service.ID} style={{marginRight: 10}}><Link to={`/connecties/${x.ID}/${s.ID}`} >{s.Service.Name}</Link></span>
}) : '-'}</td>
</tr>)
})}
</tbody>
</table>
</div>
);
};
export default App;

@ -0,0 +1,10 @@
import React from "react";
import "./Index.css";
const App = () => {
return (
<div></div>
);
};
export default App;

@ -0,0 +1,63 @@
body {
font-family: helvetica;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
h2 {
margin-bottom: 35px;
}
.c-main-nav {
padding: 15px;
display: flex;
width: 100%;
margin-bottom: 50px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
}
.c-main-nav a {display: block; padding: 5px; color: #137ad4; text-decoration: none}
.c-table {
width: 100%;
}
.c-table table a {color: #137ad4; text-decoration: none}
.c-main-content {
padding: 25px;
width: 90%;
max-width: 1024;
margin: auto;
}
.c-table th, td {
text-align: left;
padding: 10px;
}
.c-table th {
background-color: rgba(0,0,0,0.1);
}
.c-button {
padding: 8px 17px;
font-size: 95%;
color: white;
background-color: #137ad4;
border: 0;
outline: 0;
cursor: pointer;
margin-bottom: 10px;
display: inline-block;
text-decoration: none;
}
.c-modal {
position: fixed;
top: 50%;
left: 50%;
}

@ -0,0 +1,20 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import "./Index.css";
const Patient = () => {
let params = useParams();
const [patient, setPatient] = useState(null)
useEffect(() => {
fetch(`/api/connections/${params.connId}/${params.serviceId}/${params.patientId}`).then(x => x.text()).then(x => setPatient(x) )
}, [])
return (
<div>
{patient ? <div dangerouslySetInnerHTML={{__html: patient}}></div> : null}
</div>
);
};
export default Patient;

@ -0,0 +1,33 @@
import React from "react";
import { useEffect, useState } from "react";
import "./Index.css";
const App = () => {
const [registrations, setRegistrations] = useState([])
useEffect(() => {
fetch('/api/registrations').then(x => x.json()).then(x => setRegistrations(x) )
}, [])
console.log('registrations', registrations)
return (
<div>
<table className="c-table">
<tr>
<th>AGB</th>
<th>Naam</th>
<th>Referentie</th>
<th>Tijdelijk wachtwoord</th>
</tr>
{registrations.map(x => {
return (<tr key={x.ID}>
<td>{x.OrganisationId}</td>
<td>{x.OrganisationDisplayName}</td>
<td>{x.Reference}</td>
<td>{x.PSK}</td>
</tr>)
})}
</table>
</div>
);
};
export default App;

@ -0,0 +1,34 @@
import React from "react";
import { createRoot } from "react-dom/client";
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
import App from "./App";
import Home from "./Home";
import Registrations from "./Registrations";
import Connections from "./Connections";
import Connection from "./Connection";
import Patient from "./Patient";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<BrowserRouter basename="/ui">
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="/registraties" element={<Registrations />}/>
<Route path="/connecties" element={<Connections />}/>
<Route path="/connecties/:connId/:serviceId" element={<Connection />}/>
<Route path="/connecties/:connId/:serviceId/:patientId" element={<Patient />}/>
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
</Route>
</Routes>
</BrowserRouter>);

@ -0,0 +1,26 @@
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "../assets/js"),
filename: "index.js",
clean: true,
},
devtool: "source-map",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
}
};

Binary file not shown.

@ -0,0 +1 @@
{"format":[{"name":"UNB","description":"Interchange header"},{"name":"UNH","description":"Head of message"},{"name":"BGM","children":[{"name":"DTM","description":"Datum aangemaakt"},{"name":"RFF","description":"Referenties"},{"name":"FTX","description":"Bericht afhankelijke mededeling"}],"description":"Begin van het bericht"},{"name":"S01","children":[{"name":"NAD","description":"Naam en adres"},{"name":"ADR","description":"Adres"},{"name":"COM","description":"Telefoon- en fax-nummers"},{"name":"RFF"},{"name":"DTM"},{"name":"LAN"},{"name":"SPR","description":"Type medewerker"},{"name":"QUA","description":"Kwalificatie"},{"name":"FTX","description":"Vrije tekst"}],"description":"Medebehandelaar"},{"name":"S02","children":[{"name":"PNA","description":"Naam van de patient"},{"name":"ADR","description":"Adres-patient"},{"name":"COM","description":"Telefoon- en fax-nummers"},{"name":"RFF","description":"Referenties"},{"name":"DTM","description":"Tijdsindicatie"},{"name":"NAT"},{"name":"LAN"},{"name":"LOC"},{"name":"PDI","description":"Demografische gegevens"},{"name":"QUA","description":"Kwalificatie"},{"name":"STS"},{"name":"INS","description":"Verzekeringsgegevens"},{"name":"REL"},{"name":"FTX","description":"Vrije tekst"},{"name":"S03","children":[{"name":"DTM","description":"Tijdsindicatie"},{"name":"S04","children":[{"name":"CIN","description":"Diagnose signalering of risico-factor"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Referentie naar groep 1"},{"name":"FTX","description":"Vrije tekst"},{"name":"DTM","description":"Tijdsindicatie"}],"description":"Medisch kenmerk"},{"name":"S05","children":[{"name":"INV"},{"name":"DTM"}]}],"description":"Contact onafhankelijke medische gegevens"},{"name":"S06","children":[{"name":"DTM","description":"Datum/tijd van een contact"},{"name":"RFF","description":"Verantwoordelijke persoon"},{"name":"S07","children":[{"name":"FTX"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"DTM","description":"Datum/tijd van een contact"}],"description":"Ongeclassificeerde journaalregel"},{"name":"S08","children":[{"name":"INV","description":"Meting identificatie"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"RSL","description":"Resultaat meting"},{"name":"RND","description":"Normaalwaarden"},{"name":"FTX","description":"Resultaat"},{"name":"DTM","description":"Datum/tijd"}],"description":"Metingen"},{"name":"S09","children":[{"name":"CIN","description":"Diagnose"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"FTX","description":"Diagnose in vrije tekst"},{"name":"DTM","description":"Datum/tijd"}],"description":"Diagnoses"},{"name":"S10","children":[{"name":"SPR","description":"Specialisme"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"PRC","description":"Soort verwijzing"},{"name":"FTX","description":"Beschrijving"},{"name":"DTM","description":"Datum/tijd"}],"description":"Verwijzingen of terugverwijzingen"},{"name":"S11","children":[{"name":"CLI","description":"Type therapie"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"FTX","description":"Vrije tekst"},{"name":"QTY","description":"Hoeveelheid"},{"name":"DNL","children":[{"name":"DSG","description":"Dose administration"},{"name":"FTX","description":"Vrije tekst"}],"description":"Dosering Nederlandse stijl"},{"name":"SPC","children":[{"name":"QTY","description":"Hoeveelheid"}],"description":"Afzonderlijke stoffen van recept"},{"name":"CIN","description":"Indicatie"},{"name":"SPR","description":"Specialisme voorschrijver"},{"name":"DTM","description":"Datum/tijd"}],"description":"Therapie"}],"description":"Journaalregel"}],"description":"Pati\u00ebnt"},{"name":"UNT","description":"Einde van het bericht"},{"name":"UNZ","description":"Einde uitwisseling"}]}

@ -0,0 +1,442 @@
<style>
.container { margin: 1em auto; font-size: 12pt; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; }
@media screen and (max-width: 700px) {
.container { width: 700px; }
}
@media screen and (min-width: 1200px) {
.container { width: 1200px; }
}
.container h3 { margin-bottom: 8px; }
.container ul { list-style-type: none; }
.container li + li { margin-top: 10px; }
.container td { vertical-align: top; }
.container .seg { margin-top: 8px; padding: 8px 10px; border: 0 solid #e7e7e7; border-width: 0 1px 1px 1px; }
.container .seg-hdr { margin-top: 8px; border-top-width: 1px; background: #F8F8F8; }
.container .seg-hdr-white { margin-top: 8px; background: #FFFFFF; border-color: transparent; }
.container a { color: royalblue }
.container table.patient th { text-align: left; padding-right: 1em }
.container table.SOEPkind { margin: -6px 0 }
.container table.SOEPkind tr { height: 1em }
.container table.SOEPkind th { text-align: left; width: 32px; height: 1em; font-weight: normal }
.container table.results th { text-align: left }
.container .error { position: fixed; top: 10px; margin-left: 32px; border: 2px solid #000000; background-color: #FF2222; }
.row + .row { margin-top: 4px; }
.table { font-size: 100%; width: 100%; border-spacing: 0; border-collapse: collapse; }
.table tr { font-size: 100% }
.table td { font-size: 100% }
.table th { font-size: 100%; text-align: left; }
.table tbody td { font-size: 100%; text-align: left; padding: 2px 8px 2px 0; }
.table thead th { font-size: 100%; padding: 0 0 2px 0; }
.table tr.highlight { background: #E3E9E3 }
.column-attn { width: 1em; }
.column-code { width: 4em; }
.column-date { width: 6.5em; }
.column-small-name { width: 10em; }
.column-medium-name { width: 14em; }
.column-large-name { width: 28em; }
.column-name { width: 20em; }
.column-instr{ width: 15em; }
.column-quantity { width: 12em; }
.no-margin { margin-bottom: 0px; }
.seg-date { float: left; width: 6.5em; }
.seg-content-offset { margin-left: 6.5em }
.column-result-id { width: 16em }
.column-result-name { width: 10em }
.container h3.fold-toggle { margin-bottom: 8px }
.container h4.fold-toggle { margin-bottom: 0; margin-top: 4px }
.container h3.fold-toggle.fold-toggle-hidden { margin-bottom: 0 }
.container h4.fold-toggle.fold-toggle-hidden { margin-bottom: 4px }
.fold-toggle .fold-marker:before,
.container h3.fold-toggle:before,
.container h4.fold-toggle:before { content: "▾ " }
.fold-toggle.fold-toggle-hidden .fold-marker:before,
.container h3.fold-toggle.fold-toggle-hidden:before,
.container h4.fold-toggle.fold-toggle-hidden:before { content: "▸ " }
.fold-toggle { cursor: pointer; }
.fold-hidden {display: none}
.hidden
{
display: none !important
}
.container textarea
{
border: 1px solid #333;
padding: .5em;
font-size: 100%
}
.container input[type=submit]
{
font-size: 90%;
padding: .25em
}
.container button
{
font-size: 75%;
padding: .25em
}
</style>
<div class="container">
{{define "person-row"}}
<tr id="person-{{.Anchor}}">
<td class="column-small-name">{{if .Name}}{{.Name}}{{else}}<em>Geen naam</em>{{end}}</td>
<td class="column-small-name">{{.Function}}</td>
<td class="column-medium-name">{{.Address}}</td>
<td class="column-small-name">{{if .AGB}}AGB: {{.AGB}}{{end}}</td>
</tr>{{end}}
{{define "medi-group-epi"}}
{{$root := .}}
<table class="table">
<thead>
<tr>
<th class="column-date">Datum</th>
<th class="column-code">Type</th>
<th class="column-date">ICPC</th>
<th class="column-name">Beschrijving</th>
{{if eq .Extra.verbose_icpc_description "true"}}
<th class="column-desc">Beschrijving arts</th>
{{else}}
{{end}}
<th class="column-desc"></th>
</tr>
</thead>
<tbody>
{{range .Episodes}}
<tr{{if .Anchor}} id="medigroup-{{.Anchor}}"{{end}}>
<td>{{.Date}}</td>
<td>{{.Type}}</td>
<td>{{.ICPC}}</td>
{{if eq $root.Extra.verbose_icpc_description "true"}}
<td>{{.Title}}</td>
<td>{{.Description}}</td>
{{else}}
{{if eq .Description ""}}
<td colspan="2">{{.Title}}</td>
{{else}}
<td colspan="2">{{.Description}}</td>
{{end}}
{{end}}
</tr>{{end}}
</tbody>
</table>
{{end}}
{{define "medi-group-ica"}}
{{$root := .}}
<table class="table">
<thead>
<tr>
<th class="column-date">Datum</th>
<th class="column-code">Type</th>
<th class="column-date">ICPC</th>
<th class="column-name" colspan="2">Beschrijving</th>
{{if eq $root.Extra.verbose_icpc_description "true"}}
<th class="column-desc"></th>
{{else}}
{{end}}
</tr>
</thead>
<tbody>
{{range .Indicators}}
<tr{{if .Anchor}} id="medigroup-{{.Anchor}}"{{end}}>
<td>{{.Date}}</td>
<td>{{.Type}}</td>
<td>{{.ICPC}}</td>
<td>{{.Title}}</td>
{{if eq $root.Extra.verbose_icpc_description "true"}}
<td>{{.Description}}</td>
{{else}}
<td></td>
{{end}}
</tr>{{end}}
</tbody>
</table>
{{end}}
<div class="seg seg-hdr" data-his="{{.HIS}}">
<h3>Huisarts</h3>
<table class="table">
{{template "person-row" .GP}}
{{if .Pharmacy.Anchor}}{{template "person-row" .Pharmacy}}{{end}}
</table>
{{if .Practitioners}}
<h4 data-fold-toggle="practicioners" data-fold-hide="1">Medebehandelaren</h4>
<div data-fold="practicioners">
<table class="table">{{range .Practitioners}}
{{template "person-row" .}}{{end}}
</table>
</div>
{{end}}
</div>
<div class="seg seg-hdr">
<h3>Patiënt</h3>
<table class="patient">
<tr>
<th>Naam:
<td>{{.Patient.Name}}
{{if .Patient.BSN}}
<tr>
<th>BSN:
<td>{{.Patient.BSN}}
{{end}}
<tr>
<th>Geboortedatum:
<td>{{.Patient.Birthdate}}
<tr>
<th>Adres:
<td>{{.Patient.Address}}
<tr>
<th>Geslacht:
<td>{{.Patient.Gender}}
{{range .Patient.OtherFields}}
<tr>
<th>{{.Key}}:
<td{{if eq .Key "Toegepast filter"}} title="Een filter is een (standaard) filter dat door een huisarts over alle of een deel van de patiëntendossiers wordt toegepast. De minimale professionele samenvatting (PS) bevat een samenvatting van alleen zeer recente informatie uit het huisartsdossier, met alleen actuele / chronische medicatie. Dit is minder informatie dan de standaard NHG PS, die journaalregels tot 4 maanden terug, of de laatste 5 consultverslagen kan bevatten. Houd er svp rekening mee dat geen enkel type PS alle, of gegarandeerd volledige of correcte informatie bevat."{{end}}>{{.Value}}
{{end}}
</table>
</div>
{{if .Memo}}
<div class="seg seg-hdr">
<h3>Memo</h3>
{{.Memo}}
</div>
{{end}}
{{if .Episodes}}
<div class="seg seg-hdr">
<h3>Episodelijst</h3>
{{template "medi-group-epi" .}}
</div>
{{end}}
{{if .Indicators}}
<div class="seg seg-hdr">
<h3>Contra-indicaties, interacties en allergie&#235;n</h3>
{{template "medi-group-ica" .}}
</div>
{{end}}
{{if .Medication}}
<div class="seg seg-hdr">
<h3 data-fold-toggle="med-table">Medicatieoverzicht</h3>
<table data-fold="med-table" class="table">
<thead>
<tr>
<th class="column-large-name">Recept</th>
<th class="column-instr">Gebruiksvoorschrift</th>
<th class="column-quantity">Hoeveelheid</th>
<th class="column-date">Ingang</th>
<th class="column-date">Vervalt</th>
</tr>
</thead>
<tbody>
{{range $index, $med := .Medication}}
{{if gt (len $med.Fold) 1}}
<tr data-fold-toggle="medfold-{{$index}}" data-fold-hide="1">
<td>{{$med.Recipe}}</td>
<td>{{$med.Instructions}}</td>
<td>{{ YieldDosage $med }}</td>
<td>{{$med.StartDate}}</td>
<td>{{$med.EndDate}}</td>
</tr>
{{range $fold := $med.Fold}}
<tr data-fold="medfold-{{$index}}">
<td></td>
<td>{{ if ne $med.Instructions $fold.Instructions}}{{$fold.Instructions}}{{else}}{{end}}</td>
<td>{{ YieldDosage $fold }}</td>
<td>{{$fold.StartDate}}</td>
<td>{{$fold.EndDate}}</td>
</tr>
{{end}}
{{else}}
<tr>
<td>{{$med.Recipe}}</td>
<td>{{$med.Instructions}}</td>
<td>{{ YieldDosage $med }}</td>
<td>{{$med.StartDate}}</td>
<td>{{$med.EndDate}}</td>
</tr>
{{end}}
{{end }}
</tbody>
</table>
</div>
{{end}}
{{if .Journal}}
<div class="seg seg-hdr-white">
<h3>Journaal</h3>
</div>
{{range .Journal}}
<div class="seg seg-hdr">
<div class="seg-date">{{.Date}}</div>
<div class="seg-content-offset" title="{{.AuthorName}}{{if .Kind}} ({{.Kind}}){{end}}">
{{if .Results}}
<div class="row">
<table class="results">
<thead>
<tr>
<th class="column-result-id">Identificatie</th>
<th class="column-result-name">Resultaat</th>
<th class="column-result-desc">Beschrijving</th>
</tr>
</thead>
<tbody>
{{range .Results}}
<tr {{if .Episode}}data-episode="{{.Episode}}"{{end}}><td>{{.Kind}}</td>
<td>{{.Result}}</td>
<td>{{if eq .Identification "Buiten normaalwaarde"}}<strong>{{.Identification}}</strong>{{else}}{{.Identification}}{{end}}
{{if and .Identification .Description}}<br>{{end}}
{{.Description}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{range .Lines}}
<div class="row"{{if .Episode}} data-episode="{{.Episode}}"{{end}}>
<table class="SOEPkind">
<tr>
{{if .SOEP}}
<th style="padding-top: 10px; vertical-align: top;">{{.SOEP}}:
<td style="padding-top: 10px; vertical-align: top;">{{ConvertLinebreaks .Text}}
{{end}}
{{if .Kind}}
<th style="padding-top: 10px; vertical-align: top;">{{.Kind}}{{if .Text}}:{{end}}
<td style="padding-top: 10px; vertical-align: top;">{{ConvertLinebreaks .Text}}
{{end}}
</tr>
</table>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
{{if .Extra.csrf_token }}
<div class="seg seg-hdr-white">
<br>
<h3 id="feedback-header"><button onClick="feedbackHeaderClicked()">Notitie schrijven</button></h3>
<form id="feedback-form" class="hidden" method="POST" onSubmit="sendFeedback(); return false"> <!-- TODO: contruct proper URL in golang -->
<input type="hidden" name="csrf_token" value="{{.Extra.csrf_token}}" id="csrf">
<p><br><textarea id="feedback-textarea" rows="7" cols="50"></textarea>
<p><br><input type="submit" value="Opsturen">
</form>
</div>
{{end}}
</div>
<script>
(function() {
var folds = document.querySelectorAll('[data-fold-toggle]');
for (var i = 0; i < folds.length; i++) {
(function(btn) {
var t = btn.getAttribute('data-fold-toggle');
var fds = document.querySelectorAll('[data-fold="' + t + '"]');
btn.classList.add('fold-toggle');
var hidden = false;
var sync = function() {
if (hidden) {
btn.classList.add('fold-toggle-hidden');
} else {
btn.classList.remove('fold-toggle-hidden');
}
for (var j = 0; j < fds.length; j++) {
var e = fds[j].classList;
if (hidden) {
e.add('fold-hidden');
} else {
e.remove('fold-hidden');
}
}
setTimeout(send_height_to_parent, 0);
};
if (btn.getAttribute('data-fold-hide') == '1') {
hidden = true;
}
btn.addEventListener('click', function() {
hidden = !hidden;
sync();
});
sync();
})(folds[i]);
}
})();
function feedbackHeaderClicked()
{
toggle('feedback-form')
var textarea = document.getElementById('feedback-textarea')
textarea.focus()
}
function toggle(id)
{
var elem = document.getElementById(id)
if (elem)
{
var className = elem.getAttribute('class')
if (className == 'hidden')
{
elem.setAttribute('class', '')
}
else
{
elem.setAttribute('class', 'hidden')
}
}
}
function getXMLHttpRequest()
{
if (window.XMLHttpRequest)
{
return new XMLHttpRequest()
}
else if (window.ActiveXObject)
{
return new ActiveXObject('Microsoft.XMLHTTP')
}
}
</script></body></html>

@ -0,0 +1,67 @@
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"sync"
"google.golang.org/grpc"
"whiteboxsystems.nl/openkvpoc/openkv"
)
var srvaddr = "localhost:8888"
var patientIf = "localhost:8085"
func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
wg := &sync.WaitGroup{}
openapisrv := NewServer()
openapisrv.LoadData("./data/data.db")
opts := []grpc.ServerOption{
// grpc.UnaryInterceptor(openapisrv.EnsureValidModule),
}
grpcServer := grpc.NewServer(opts...)
go func() {
lis, err := net.Listen("tcp", srvaddr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
openkv.RegisterOpenKVServer(grpcServer, openapisrv)
log.Printf("RPC Listening on %v", srvaddr)
wg.Add(1)
grpcServer.Serve(lis)
}()
srv := NewUIServer(patientIf)
srv.data = openapisrv.data
go func() {
wg.Add(1)
srv.ListenAndServe()
}()
<-stop
go func() {
grpcServer.GracefulStop()
wg.Done()
log.Println("Shutdown RPC server")
}()
go func() {
log.Println("Shutdown UI server...")
srv.Shutdown(context.Background())
wg.Done()
log.Println("UI Server shutdown...")
}()
wg.Wait()
}

@ -0,0 +1,71 @@
package model
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
func GetDB(location string) (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{})
if err != nil {
return nil, err
}
// Migrate the schema
db.AutoMigrate(&sharedmodel.Registration{})
db.AutoMigrate(&sharedmodel.Connection{})
db.AutoMigrate(&sharedmodel.Service{})
db.AutoMigrate(&sharedmodel.AuthConfig{})
db.AutoMigrate(&sharedmodel.ProtocolConfig{})
db.AutoMigrate(&sharedmodel.ServiceConfig{})
db.AutoMigrate(&sharedmodel.Subscription{})
var cnt int64
db.Model(&sharedmodel.Service{}).Count(&cnt)
if cnt == 0 {
db.Create(&sharedmodel.Service{
Name: "Visitelijst",
Description: "Visitelijst voor het inzien van patientendossiers op visite",
SubscriptionPolicy: openkv.SubscriptionPolicy_optin,
ConsentPolicy: openkv.ConsentPolicy_presumed,
ServiceID: "wbx:visitelijst",
FetchProtocols: sharedmodel.ProtocolArray{
{
"https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
PushProtocols: sharedmodel.ProtocolArray{
{
"https://whiteboxsystems.nl/protospecs/whitebox-push/http",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
})
db.Create(&sharedmodel.Service{
Name: "Waarneminglijst",
Description: "Waarneminglijst om patienten inzichtelijk te maken op de HAP",
SubscriptionPolicy: openkv.SubscriptionPolicy_optin,
ConsentPolicy: openkv.ConsentPolicy_explicit,
ServiceID: "wbx:waarneming",
FetchProtocols: sharedmodel.ProtocolArray{
{
"https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
PushProtocols: sharedmodel.ProtocolArray{
{
"https://whiteboxsystems.nl/protospecs/whitebox-push/http",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
})
}
return db, nil
}

@ -0,0 +1,416 @@
package main
import (
"context"
"fmt"
"log"
"github.com/gofrs/uuid"
"google.golang.org/grpc/metadata"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
"whiteboxsystems.nl/openkvpoc/whiteboxservice/model"
)
var errNotAuthorized = fmt.Errorf("Not Authorized")
var errInvalidService = fmt.Errorf("Invalid service")
var errActiveServiceConfig = fmt.Errorf("Service not activated")
type OpenKVServer struct {
openkv.UnimplementedOpenKVServer
data *gorm.DB
}
func (srv *OpenKVServer) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func requireConnection(db *gorm.DB, ctx context.Context) (*sharedmodel.Connection, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Printf("No metadata")
return nil, errNotAuthorized
}
connection := &sharedmodel.Connection{}
if a, ok := md["authorization"]; !ok {
log.Printf("No token provided")
return nil, errNotAuthorized
} else {
if err := db.Preload("AuthMethod").Raw(`
SELECT conn.*
FROM connections conn
JOIN auth_configs a on conn.auth_config_id = a.id WHERE a.method = ? and a.raw = ?
`, openkv.AuthMethod_APIToken, a[0]).Scan(connection).Error; err != nil {
log.Printf("Invalid token; err: %v;", err)
return nil, errNotAuthorized
}
}
return connection, nil
}
func requireService(db *gorm.DB, conn *sharedmodel.Connection, serviceID string) (*sharedmodel.ServiceConfig, error) {
service := &sharedmodel.Service{}
if err := db.Where("service_id = ?", serviceID).First(service).Error; err != nil {
return nil, errInvalidService
}
srvConfig := &sharedmodel.ServiceConfig{}
if err := db.Where("connection_id = ? and enabled = ? and service_id = ?", conn.ID, true, service.ID).First(srvConfig).Error; err != nil {
return nil, errActiveServiceConfig
}
return srvConfig, nil
}
func (srv *OpenKVServer) GetMetadata(
ctx context.Context, in *openkv.GetMetadataRequest,
) (*openkv.GetMetadataResponse, error) {
log.Printf("Got metadata request")
services := []*openkv.ServiceDefinition{}
presentServices := []sharedmodel.Service{}
srv.data.Find(&presentServices)
for _, service := range presentServices {
services = append(services, &openkv.ServiceDefinition{
Name: service.Name,
Description: service.Description,
Id: service.ServiceID,
SubscriptionPolicy: service.SubscriptionPolicy,
ConsentPolicy: service.ConsentPolicy,
FetchProtocols: service.GetFetchProtocols(),
PushProtocols: service.GetPushProtocols(),
})
}
resp := &openkv.GetMetadataResponse{
Supplier: "Whitebox Systems",
System: "Whitebox",
Services: services,
Success: true,
}
return resp, nil
}
func (srv *OpenKVServer) Register(
ctx context.Context, in *openkv.RegisterRequest,
) (*openkv.RegisterResponse, error) {
ref, _ := uuid.NewV4()
psk, _ := uuid.NewV4()
reg := &sharedmodel.Registration{
Reference: ref.String(),
OrganisationId: in.OrganisationId,
OrganisationIdSystem: in.OrganisationIdSystem,
OrganisationDisplayName: in.OrganisationDisplayName,
PSK: psk.String()[0:6],
Status: sharedmodel.RegistrationStatusPending,
}
reg.SetAuthConfig(in.Auth)
srv.data.Create(reg)
resp := &openkv.RegisterResponse{
Reference: ref.String(),
Success: true,
}
log.Printf("Got registration request from %v; ref: %v; PSK: %v", reg.OrganisationDisplayName, reg.Reference, reg.PSK)
return resp, nil
}
func (srv *OpenKVServer) CompleteRegistration(
ctx context.Context, in *openkv.CompleteRegistrationRequest,
) (*openkv.CompleteRegistrationResponse, error) {
registration := &sharedmodel.Registration{}
if err := srv.data.Preload("AuthConfig").Where("reference = ? and status = ?", in.Reference, sharedmodel.RegistrationStatusPending).First(registration).Error; err != nil {
log.Printf("Invalid ref")
return nil, errNotAuthorized
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Printf("No metadata")
return nil, errNotAuthorized
}
// The keys within metadata.MD are normalized to lowercase.
// See: https://godoc.org/google.golang.org/grpc/metadata#New
if a, ok := md["authorization"]; !ok {
log.Printf("No token provided")
return nil, errNotAuthorized
} else {
if a[0] != registration.AuthConfig.Raw {
log.Printf("Invalid token; eXpected: %v; got: %v", registration.AuthConfig.Raw, a[0])
return nil, errNotAuthorized
}
}
resp := &openkv.CompleteRegistrationResponse{}
if in.RegistrationToken != registration.PSK {
resp.Error = &openkv.Error{
Code: 1,
Message: "Invalid PSK",
}
return resp, nil
}
conn := &sharedmodel.Connection{
OrganisationId: registration.OrganisationId,
OrganisationIdSystem: registration.OrganisationIdSystem,
OrganisationDisplayName: registration.OrganisationDisplayName,
AuthConfig: registration.AuthConfig.Clone(),
}
srv.data.Create(conn)
registration.Status = sharedmodel.RegistrationStatusCompleted
srv.data.Save(registration)
resp.Success = true
return resp, nil
}
func (srv *OpenKVServer) ConfigService(
ctx context.Context, in *openkv.ConfigServiceRequest,
) (*openkv.ConfigServiceResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
service := &sharedmodel.Service{}
if err := srv.data.Where("service_id = ?", in.Service).First(service).Error; err != nil {
return nil, fmt.Errorf("Invalid service: %v", service.ServiceID)
}
cnf := &sharedmodel.ServiceConfig{}
if err := srv.data.Where("connection_id = ? and service_id = ?", conn.ID, service.ID).First(cnf); err != nil {
cnf.ConnectionID = conn.ID
cnf.ServiceID = service.ID
}
log.Printf("Update service config %v for conn: %v", cnf.Service.Name, conn.ID)
cnf.Enabled = in.Enabled
cnf.PushProtocol = &sharedmodel.ProtocolConfig{
Protocol: in.Push.Protocol,
AuthConfig: sharedmodel.NewAuthConfig(in.Push.Auth),
}
cnf.PushProtocol.SetConfig(in.Push.Config)
// TODO actually init authdata
cnf.PushProtocol.AuthConfig.Raw = "1111"
cnf.FetchProtocol = &sharedmodel.ProtocolConfig{
Protocol: in.Fetch.Protocol,
AuthConfig: sharedmodel.NewAuthConfig(in.Fetch.Auth),
}
cnf.FetchProtocol.SetConfig(in.Fetch.Config)
// TODO actually init authdata
cnf.FetchProtocol.AuthConfig.Raw = "1111"
if cnf.ID == 0 {
if err := srv.data.Create(cnf).Error; err != nil {
return nil, err
}
} else {
if err := srv.data.Save(cnf).Error; err != nil {
return nil, err
}
}
// If disabled unsubscribe all subscriptions
if !cnf.Enabled {
srv.data.Unscoped().Where("service_config_id = ?", cnf.ID).Delete(&sharedmodel.Subscription{})
}
resp := &openkv.ConfigServiceResponse{
Success: true,
Service: in.Service,
Enabled: in.Enabled,
Fetch: &openkv.ServiceConfig{
Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Config: &openkv.AuthConfig_ApiTokenConfig{&openkv.APITokenConfig{Token: "1111"}},
},
},
Push: &openkv.ServiceConfig{
Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-push/http",
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Config: &openkv.AuthConfig_ApiTokenConfig{&openkv.APITokenConfig{Token: "1111"}},
},
},
}
return resp, nil
}
func (srv *OpenKVServer) UpdateSubscriptions(
ctx context.Context, in *openkv.UpdateSubscriptionsRequest,
) (*openkv.UpdateSubscriptionsResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
serviceConfig, err := requireService(srv.data, conn, in.ServiceId)
if err != nil {
return nil, err
}
subscriptionErrors := []*openkv.SubscriptionError{}
if err := srv.data.Transaction(func(tx *gorm.DB) error {
for idx, sd := range in.SubscriptionData {
subscription := &sharedmodel.Subscription{}
err := srv.data.Where(
"subject_external_id = ? and subject_external_id_system = ? and service_config_id = ?",
sd.Subject.ExternalId,
sd.Subject.ExternalIdSystem,
serviceConfig.ID,
).First(subscription).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
} else if err != nil && sd.Subscribe {
sub := &sharedmodel.Subscription{
SubjectExternalId: sd.Subject.ExternalId,
SubjectExternalIdSystem: sd.Subject.ExternalIdSystem,
SubjectName: sd.Subject.Name,
SubjectBirthdate: sd.Subject.Birthdate,
ServiceConfigID: serviceConfig.ID,
}
// TODO check if it is valid metadata for the specified protocol
sub.SetProtocolMeta(sd.ProtocolMeta)
if err := srv.data.Create(sub).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be persisted; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err),
},
})
}
log.Printf("add subscription: %v", sd.Subject.ExternalId)
continue
} else if err != nil && !sd.Subscribe {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrCodeAlreadySubscribed,
Message: fmt.Sprintf("Subject with id: %v (%v) already unsubscribed", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem),
},
})
continue
} else if !sd.Subscribe {
if err := srv.data.Unscoped().Delete(subscription).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be removed; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err)},
})
}
log.Printf("delete subscription: %v", sd.Subject.ExternalId)
continue
}
subscription.SubjectExternalId = sd.Subject.ExternalId
subscription.SubjectExternalIdSystem = sd.Subject.ExternalIdSystem
subscription.SubjectName = sd.Subject.Name
subscription.SubjectBirthdate = sd.Subject.Birthdate
subscription.SetProtocolMeta(sd.ProtocolMeta)
if err := srv.data.Save(subscription).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be updated; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err)},
})
continue
}
log.Printf("update subscription: %v", sd.Subject.ExternalId)
}
return nil
}); err != nil {
return nil, err
}
resp := &openkv.UpdateSubscriptionsResponse{
Success: true,
Errors: subscriptionErrors,
}
return resp, nil
}
func (srv *OpenKVServer) ListSubscriptions(
ctx context.Context, in *openkv.ListSubscriptionsRequest,
) (*openkv.ListSubscriptionsResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
serviceConfig, err := requireService(srv.data, conn, in.ServiceId)
if err != nil {
return nil, err
}
subscriptions := []*sharedmodel.Subscription{}
srv.data.Where("service_config_id = ?", serviceConfig.ID).Find(&subscriptions)
subs := []*openkv.SubscriptionData{}
for _, s := range subscriptions {
meta := map[string]string{}
s.GetProtocolMeta(&meta)
subs = append(subs, &openkv.SubscriptionData{
Subject: &openkv.PatientMeta{
ExternalId: s.SubjectExternalId,
ExternalIdSystem: s.SubjectExternalIdSystem,
Name: s.SubjectName,
Birthdate: s.SubjectBirthdate,
},
ProtocolMeta: meta,
})
}
resp := &openkv.ListSubscriptionsResponse{
Success: true,
SubscriptionData: subs,
}
return resp, nil
}
func NewServer() *OpenKVServer {
return &OpenKVServer{}
}

@ -0,0 +1,277 @@
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"sync"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/his/model"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
type UIService struct {
srv *http.Server
inited bool
data *gorm.DB
}
func (srv *UIService) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func (srv *UIService) Addr() string {
if srv.srv == nil {
return ""
}
return srv.srv.Addr
}
func (srv *UIService) ListenAndServe() {
if !srv.inited {
srv.init()
}
log.Println("Listening on %v", srv.srv.Addr)
srv.srv.ListenAndServe()
}
func (srv *UIService) Shutdown(ctx context.Context) error {
return srv.srv.Shutdown(ctx)
}
func (srv *UIService) init() {
r := srv.srv.Handler.(*gin.Engine)
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./assets")
r.Use(srv.Authenticate)
r.GET("/", func(c *gin.Context) {
c.Redirect(301, "/ui")
})
r.GET("/ui", srv.GetIndex)
r.GET("/ui/*page", srv.GetIndex)
r.GET("/api/connections", srv.GetConnections)
r.GET("/api/connections/:connID", srv.GetConnection)
r.GET("/api/connections/:connID/:serviceID", srv.GetSubscriptions)
r.GET("/api/connections/:connID/:serviceID/:patientID", srv.GetPatient)
r.GET("/api/registrations", srv.GetRegistrations)
// r.GET("/api/systems/:sysid/patients", srv.GetPatients)
// r.GET("/api/systems/:sysid/patients/:patid", srv.GetPatient)
srv.inited = true
}
func (srv *UIService) GetIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}
func (srv *UIService) GetConnection(c *gin.Context) {
connID := c.Param("connID")
connection := &sharedmodel.Connection{}
srv.data.Where("id = ?", connID).Find(&connection)
c.JSON(200, connection)
}
func (srv *UIService) GetSubscriptions(c *gin.Context) {
connID := c.Param("connID")
serviceID := c.Param("serviceID")
serviceConfig := &sharedmodel.ServiceConfig{}
srv.data.Preload("Service").Preload("Subscriptions").Where("connection_id = ? and id = ?", connID, serviceID).Find(&serviceConfig)
c.JSON(200, serviceConfig)
}
func (srv *UIService) GetPatient(c *gin.Context) {
connID := c.Param("connID")
serviceID := c.Param("serviceID")
patientID := c.Param("patientID")
patient := &sharedmodel.Subscription{}
serviceConfig := &sharedmodel.ServiceConfig{}
if err := srv.data.Preload("FetchProtocol").Preload("FetchProtocol.AuthConfig").Preload("Service").Where("connection_id = ? and id = ?", connID, serviceID).Find(&serviceConfig).Error; err != nil {
c.AbortWithError(500, err)
return
}
srv.data.Where("service_config_id = ? and id = ?", serviceID, patientID).Find(&patient)
protoconfig := map[string]string{}
protometa := map[string]string{}
err := serviceConfig.FetchProtocol.UnmarshalConfig(&protoconfig)
log.Println(err, protoconfig)
err = patient.GetProtocolMeta(&protometa)
log.Println(err, protometa)
url := fmt.Sprintf("%v/%v/%v", protoconfig["url"], "patients", protometa["patientID"])
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", serviceConfig.FetchProtocol.AuthConfig.Raw)
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.AbortWithError(500, err)
return
}
cmd := exec.Command("./bin/ediviewer", "./bin/medeur.json", "./bin/template.html", "/dev/stdin")
sin, err := cmd.StdinPipe()
if err != nil {
log.Println("[ediviewer] Failed to open stdin pipe:", err)
c.AbortWithError(500, err)
return
}
defer sin.Close()
serr, err := cmd.StderrPipe()
if err != nil {
log.Println("[ediviewer] Failed to open stderr pipe:", err)
c.AbortWithError(500, err)
return
}
defer serr.Close()
sout, err := cmd.StdoutPipe()
if err != nil {
log.Println("[ediviewer] Failed to open stdout pipe:", err)
c.AbortWithError(500, err)
return
}
defer sout.Close()
if err := cmd.Start(); err != nil {
log.Println("[ediviewer] Failed to start:", err)
c.AbortWithError(500, err)
return
}
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
if _, err := io.Copy(sin, resp.Body); err != nil {
log.Println("[ediviewer] Error reading EDIFACT:", err)
c.AbortWithError(500, err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(c.Writer, sout); err != nil {
log.Println("[ediviewer] Error output EDIFACT:", err)
c.AbortWithError(500, err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(os.Stderr, serr); err != nil {
log.Println("[ediviewer] Error stderr EDIFACT:", err)
c.AbortWithError(500, err)
}
}()
wg.Wait()
if err := cmd.Wait(); err != nil {
log.Println("[ediviewer] Failed:", err)
c.AbortWithError(500, err)
}
}
func (srv *UIService) Authenticate(c *gin.Context) {
// authHeader := c.Request.Header.Get("Authorization")
// log.Printf("authHeader: %v", authHeader)
// if authHeader != "1111" {
// c.Status(401)
// c.Abort()
// }
}
func (srv *UIService) GetConnections(c *gin.Context) {
connections := []*sharedmodel.Connection{}
srv.data.Preload("Services").Preload("Services.Service").Find(&connections)
c.JSON(200, connections)
}
func (srv *UIService) GetRegistrations(c *gin.Context) {
registrations := []*sharedmodel.Registration{}
srv.data.Where("status = ?", sharedmodel.RegistrationStatusPending).Find(&registrations)
c.JSON(200, registrations)
}
// func (srv *UIService) GetSystems(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
// func (srv *UIService) GetPatients(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
// func (srv *UIService) GetPatient(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
func NewUIServer(addr string) *UIService {
srv := &UIService{srv: &http.Server{
Addr: addr,
Handler: gin.Default(),
}}
return srv
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whitebox</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/js/index.js"></script>
</body>
</html>
Loading…
Cancel
Save