commit
3e81a54556
@ -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(®istrations) |
||||
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 |
||||
) |
@ -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ë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 >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ëntie' |
||||
FTX+ACB+++nierinsufficientie' |
||||
S03+3+PRO+:::-1002' |
||||
DTM+194:20111021:102' |
||||
S04+1' |
||||
CIN+DI+K99.04:ICPC1V00:NHG:Chronisch veneuze insufficië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ë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ë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(®istrations) |
||||
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…
Reference in new issue