release version 4
This commit is contained in:
parent
a1651c914e
commit
33e99c167d
@ -18,3 +18,4 @@ WALLET_ENABLED=false
|
|||||||
WALLET_URL=https://conectathon-balancer.izer.tech
|
WALLET_URL=https://conectathon-balancer.izer.tech
|
||||||
WALLET_IDENTIFIER=test
|
WALLET_IDENTIFIER=test
|
||||||
WALLET_API_KEY=""
|
WALLET_API_KEY=""
|
||||||
|
ICVP_VALIDATOR_URL=http://lacpass.create.cl:7089
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type Config struct {
|
|||||||
WalletUrl string
|
WalletUrl string
|
||||||
WalletIdentifier string
|
WalletIdentifier string
|
||||||
WalletAPIKey string
|
WalletAPIKey string
|
||||||
|
ICVPValidatorUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() Config {
|
func LoadConfig() Config {
|
||||||
@ -39,13 +40,14 @@ func LoadConfig() Config {
|
|||||||
AuthEmailClientID: "app",
|
AuthEmailClientID: "app",
|
||||||
FhirBaseUrl: "http://lacpass.create.cl:8080",
|
FhirBaseUrl: "http://lacpass.create.cl:8080",
|
||||||
VhlBaseUrl: "http://lacpass.create.cl:8182",
|
VhlBaseUrl: "http://lacpass.create.cl:8182",
|
||||||
FhirMediatorBaseUrl: "http://lacpass.create.cl:3000/",
|
FhirMediatorBaseUrl: "http://lacpass.create.cl:3000",
|
||||||
APISwagger: false,
|
APISwagger: false,
|
||||||
LogLevel: "info",
|
LogLevel: "info",
|
||||||
WalletEnabled: false,
|
WalletEnabled: false,
|
||||||
WalletUrl: "https://conectathon-balancer.izer.tech/",
|
WalletUrl: "https://conectathon-balancer.izer.tech/",
|
||||||
WalletIdentifier: "test",
|
WalletIdentifier: "test",
|
||||||
WalletAPIKey: "",
|
WalletAPIKey: "",
|
||||||
|
ICVPValidatorUrl: "http://lacpass.create.cl:7089",
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverPort, exists := os.LookupEnv("API_PORT"); exists {
|
if serverPort, exists := os.LookupEnv("API_PORT"); exists {
|
||||||
@ -112,5 +114,9 @@ func LoadConfig() Config {
|
|||||||
cfg.WalletAPIKey = walletAPIKey
|
cfg.WalletAPIKey = walletAPIKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if icvpValidatorUrl, exists := os.LookupEnv("ICVP_VALIDATOR_URL"); exists {
|
||||||
|
cfg.ICVPValidatorUrl = icvpValidatorUrl
|
||||||
|
}
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,11 +140,12 @@ func (a *App) loadIpsRoute(router chi.Router) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) loadVhlRoute(router chi.Router) {
|
func (a *App) loadVhlRoute(router chi.Router) {
|
||||||
r := vhlClient.NewClient(a.config.VhlBaseUrl)
|
r := vhlClient.NewClient(a.config.VhlBaseUrl, a.config.ICVPValidatorUrl)
|
||||||
s := vhlCore.NewService(&r)
|
s := vhlCore.NewService(&r)
|
||||||
h := vhlHandler.NewHandler(&s)
|
h := vhlHandler.NewHandler(&s)
|
||||||
router.Post("/", h.Create)
|
router.Post("/", h.Create)
|
||||||
router.Post("/fetch", h.Get)
|
router.Post("/fetch", h.Get)
|
||||||
|
router.Post("/validate", h.Validate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) loadWalletRoutes(router chi.Router) {
|
func (a *App) loadWalletRoutes(router chi.Router) {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
API_PORT: ${API_PORT:-3000}
|
API_PORT: ${API_PORT:-3000}
|
||||||
AUTH_INTERNAL_URL: ${AUTH_INTERNAL_URL:-http://auth:8080}
|
AUTH_INTERNAL_URL: ${AUTH_INTERNAL_URL:-http://auth:8080}
|
||||||
AUTH_HOSTNAME: ${KEYCLOAK_URL:-http://localhost:9083}
|
AUTH_HOSTNAME: ${KEYCLOAK_HOSTNAME:-http://localhost:9083}
|
||||||
AUTH_REALM: ${KEYCLOAK_REALM:-lacpass}
|
AUTH_REALM: ${KEYCLOAK_REALM:-lacpass}
|
||||||
AUTH_CLIENT_ID: ${AUTH_CLIENT_ID:-admin-cli}
|
AUTH_CLIENT_ID: ${AUTH_CLIENT_ID:-admin-cli}
|
||||||
# Need to set this after creating a client for Keycloak Admin API access, using service account
|
# Need to set this after creating a client for Keycloak Admin API access, using service account
|
||||||
@ -27,6 +27,7 @@ services:
|
|||||||
WALLET_URL: ${WALLET_URL:-https://conectathon-balancer.izer.tech/}
|
WALLET_URL: ${WALLET_URL:-https://conectathon-balancer.izer.tech/}
|
||||||
WALLET_IDENTIFIER: ${WALLET_IDENTIFIER:-test}
|
WALLET_IDENTIFIER: ${WALLET_IDENTIFIER:-test}
|
||||||
WALLET_API_KEY: ${WALLET_API_KEY:-}
|
WALLET_API_KEY: ${WALLET_API_KEY:-}
|
||||||
|
ICVP_VALIDATOR_URL: ${ICVP_VALIDATOR_URL:-http://lacpass.create.cl:7089}
|
||||||
ports:
|
ports:
|
||||||
- "9081:3000"
|
- "9081:3000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@ -40,16 +40,21 @@ And then go to the `Users` tab and create a new user:
|
|||||||

|

|
||||||
|
|
||||||
> In the compose we have a mail-catcher container running on port 25 that will show you any email sent by keycloak to
|
> In the compose we have a mail-catcher container running on port 25 that will show you any email sent by keycloak to
|
||||||
the users registered. This emails will not be sent out is just for development.
|
> the users registered. This emails will not be sent out is just for development.
|
||||||
|
|
||||||
Once the user is created, we can use the helper script to get an access token from Keycloak:
|
Once the user is created, we can use the helper script to get an access token from Keycloak:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sh scripts/auth.sh
|
sh scripts/auth.sh access-token
|
||||||
|
```
|
||||||
|
|
||||||
|
The access token will show after the command, like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Successfully logged in!
|
||||||
|
Access Token:
|
||||||
|
XXXXX.VVVV.BBBB
|
||||||
```
|
```
|
||||||
|
|
||||||
For this to work we need to define both `KEYCLOAK_DEFAULT_USER_EMAIL` and `KEYCLOAK_DEFAULT_USER_PASSWORD` in our `.env`
|
For this to work we need to define both `KEYCLOAK_DEFAULT_USER_EMAIL` and `KEYCLOAK_DEFAULT_USER_PASSWORD` in our `.env`
|
||||||
file.
|
file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -54,3 +54,6 @@ Fhir server endpoint for FHIR IPS managment. Default: `http://lacpass.create.cl:
|
|||||||
|
|
||||||
`VHL_BASE_URL`
|
`VHL_BASE_URL`
|
||||||
VHL server endpoint for VHL QR generation, validation and retrieve. Default: `http://lacpass.create.cl:8182`
|
VHL server endpoint for VHL QR generation, validation and retrieve. Default: `http://lacpass.create.cl:8182`
|
||||||
|
|
||||||
|
`ICVP_VALIDATOR_URL`
|
||||||
|
Endpoint to validate the QR content of ICVPs not linked to an IPS. Default: `http://lacpass.create.cl:7089`
|
||||||
|
|||||||
@ -100,3 +100,49 @@ To do this:
|
|||||||
4. On the bottom set your STMP credentials in the **Connection & Authentication** section and save.
|
4. On the bottom set your STMP credentials in the **Connection & Authentication** section and save.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Enable HTTP access
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Do not use HTTP in Production, since this will expose users data. We recomended always use a secure HTTPS endpoint.
|
||||||
|
|
||||||
|
By default Keycloak supports to be hosted only in a HTTPS endpoint. If you host it in a HTTP endpoint, a error message will be displayed when trying to login
|
||||||
|
into the webclient, and when trying yo authenticate an user.
|
||||||
|
|
||||||
|
To disable the required HTTPS, please add to `docker/compose.yaml` this environment variables for `auth`
|
||||||
|
service, under the `environments` section:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
#...
|
||||||
|
KC_HOSTNAME_STRICT: false
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS: false
|
||||||
|
```
|
||||||
|
|
||||||
|
This will tell keycloak to not require a HTTPS connection.
|
||||||
|
But if you already build and started your keycloak service, this parameter is already saved
|
||||||
|
into keycloak's database. To override it we need to go into the `auth-db` container.
|
||||||
|
|
||||||
|
To get inside the `auth-db` container, please run the next command in the project root folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec -it auth-db sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start initialize a shell console inside the container. Then to connect to the
|
||||||
|
database run the following command, where `<POSTGRES_USER>` is the enviroment variable in your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
psql -U <POSTGRES_USER> keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
This will connect you into a Postgres console inside the keycloak database. Finally run
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE realm SET ssl_required='NONE' WHERE name='master';
|
||||||
|
UPDATE realm SET ssl_required='NONE' WHERE name='lacpass';
|
||||||
|
```
|
||||||
|
|
||||||
|
This will update the keycloak parameters to not required HTTPS.
|
||||||
|
|
||||||
|
To exit just run `exit` twice, one to get out of Postgres and a second time to get out of the container.
|
||||||
|
|||||||
@ -12,17 +12,20 @@ import (
|
|||||||
type ClientInterface interface {
|
type ClientInterface interface {
|
||||||
GetDocumentReference(identifier string) (*Bundle, error)
|
GetDocumentReference(identifier string) (*Bundle, error)
|
||||||
GetIpsBundle(url string) (map[string]interface{}, error)
|
GetIpsBundle(url string) (map[string]interface{}, error)
|
||||||
|
GetIpsICVP(idBundle string, immunizationId *string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IpsClient struct {
|
type IpsClient struct {
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
MediatorBaseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(baseURL string) IpsClient {
|
func NewClient(baseURL string, mediatorBaseURL string) IpsClient {
|
||||||
return IpsClient{
|
return IpsClient{
|
||||||
Client: &http.Client{},
|
Client: &http.Client{},
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
|
MediatorBaseURL: mediatorBaseURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,3 +65,12 @@ func (c *IpsClient) GetIpsBundle(url string) (map[string]interface{}, error) {
|
|||||||
Err: fmt.Errorf("failed to get document reference"),
|
Err: fmt.Errorf("failed to get document reference"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *IpsClient) GetIpsICVP(idBundle string, immunizationId *string) (string, error) {
|
||||||
|
// TODO: To be implemented by the participant
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: 500,
|
||||||
|
Body: []map[string]interface{}{{"error": "Not implemented error", "message": "this method is not implemented yet"}},
|
||||||
|
Err: fmt.Errorf("failed to get document reference"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"ips-lacpass-backend/internal/ips/client"
|
"ips-lacpass-backend/internal/ips/client"
|
||||||
customErrors "ips-lacpass-backend/pkg/errors"
|
customErrors "ips-lacpass-backend/pkg/errors"
|
||||||
authMiddleware "ips-lacpass-backend/pkg/middleware"
|
authMiddleware "ips-lacpass-backend/pkg/middleware"
|
||||||
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -143,6 +144,113 @@ func removeDuplicates(entries []Entry) []Entry {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findCountryInBundle(bundle Bundle) string {
|
||||||
|
for _, entry := range bundle.Entry {
|
||||||
|
if entry.Resource == nil {
|
||||||
|
slog.Warn("No resource found in bundle: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rtype, ok := entry.Resource["resourceType"]
|
||||||
|
if !ok || rtype == nil || rtype != "Organization" {
|
||||||
|
slog.Warn("Resource type is not an Organization: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addressEntry, ok := entry.Resource["address"]
|
||||||
|
if !ok || addressEntry == nil {
|
||||||
|
slog.Warn("Resource has has noo Address: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses, ok := entry.Resource["address"].([]interface{})
|
||||||
|
if !ok || len(addresses) == 0 {
|
||||||
|
slog.Warn("Resource has no address: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
address, ok := addresses[0].(map[string]interface{})
|
||||||
|
if !ok || address == nil {
|
||||||
|
slog.Warn("Resource address is not a map: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
country, ok := address["country"]
|
||||||
|
if !ok || country == nil {
|
||||||
|
slog.Warn("Resource address has no country: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
countryCode, ok := country.(string)
|
||||||
|
if !ok {
|
||||||
|
slog.Warn("Resource country is not a string: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return countryCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOrganizationEntries(bundle Bundle) []Entry {
|
||||||
|
var entries []Entry
|
||||||
|
for _, entry := range bundle.Entry {
|
||||||
|
if entry.Resource == nil {
|
||||||
|
slog.Warn("No resource found in bundle: ", entry.FullURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rtype, ok := entry.Resource["resourceType"]
|
||||||
|
if !ok || rtype == nil || rtype != "Organization" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOriginExtension(resource map[string]interface{}, bundleID string, country string) {
|
||||||
|
if resource == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
originExtension := map[string]interface{}{
|
||||||
|
"url": "http://lacpass.org/fhir/StructureDefinition/resource-origin",
|
||||||
|
"extension": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"url": "bundleId",
|
||||||
|
"valueString": bundleID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "country",
|
||||||
|
"valueString": country,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions, ok := resource["extension"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
extensions = []interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid adding duplicate extensions
|
||||||
|
for _, ext := range extensions {
|
||||||
|
extMap, ok := ext.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
url, ok := extMap["url"].(string)
|
||||||
|
if !ok || url != "http://lacpass.org/fhir/StructureDefinition/resource-origin" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions = append(extensions, originExtension)
|
||||||
|
resource["extension"] = extensions
|
||||||
|
}
|
||||||
|
|
||||||
func (is *IpsService) MergeIPS(ctx context.Context, currentIpsBundle map[string]interface{}, newIpsBundle map[string]interface{}) (map[string]interface{}, error) {
|
func (is *IpsService) MergeIPS(ctx context.Context, currentIpsBundle map[string]interface{}, newIpsBundle map[string]interface{}) (map[string]interface{}, error) {
|
||||||
var currIPS, newIPS Bundle
|
var currIPS, newIPS Bundle
|
||||||
if err := mapstructure.Decode(currentIpsBundle, &currIPS); err != nil {
|
if err := mapstructure.Decode(currentIpsBundle, &currIPS); err != nil {
|
||||||
@ -161,6 +269,17 @@ func (is *IpsService) MergeIPS(ctx context.Context, currentIpsBundle map[string]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentCountry := findCountryInBundle(currIPS)
|
||||||
|
newCountry := findCountryInBundle(newIPS)
|
||||||
|
|
||||||
|
for i := range currIPS.Entry {
|
||||||
|
addOriginExtension(currIPS.Entry[i].Resource, currIPS.ID, currentCountry)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range newIPS.Entry {
|
||||||
|
addOriginExtension(newIPS.Entry[i].Resource, newIPS.ID, newCountry)
|
||||||
|
}
|
||||||
|
|
||||||
curComp, err := getIPSComposition(currIPS.Entry)
|
curComp, err := getIPSComposition(currIPS.Entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &customErrors.HttpError{
|
return nil, &customErrors.HttpError{
|
||||||
@ -275,6 +394,16 @@ func (is *IpsService) MergeIPS(ctx context.Context, currentIpsBundle map[string]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add original Organization resources from both IPSs
|
||||||
|
currIPSOrganizations := findOrganizationEntries(currIPS)
|
||||||
|
newIPSOrganizations := findOrganizationEntries(newIPS)
|
||||||
|
for _, org := range newIPSOrganizations {
|
||||||
|
mergedIPS.Entry = append(mergedIPS.Entry, org)
|
||||||
|
}
|
||||||
|
for _, org := range currIPSOrganizations {
|
||||||
|
mergedIPS.Entry = append(mergedIPS.Entry, org)
|
||||||
|
}
|
||||||
mergedIPS.Entry = removeDuplicates(mergedIPS.Entry)
|
mergedIPS.Entry = removeDuplicates(mergedIPS.Entry)
|
||||||
|
|
||||||
jsonData, err = json.Marshal(mergedIPS)
|
jsonData, err = json.Marshal(mergedIPS)
|
||||||
|
|||||||
@ -14,12 +14,14 @@ import (
|
|||||||
type VhlClient struct {
|
type VhlClient struct {
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
ICVPValidatorUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(baseURL string) VhlClient {
|
func NewClient(baseURL string, icvpValidatorUrl string) VhlClient {
|
||||||
return VhlClient{
|
return VhlClient{
|
||||||
Client: &http.Client{},
|
Client: &http.Client{},
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
|
ICVPValidatorUrl: icvpValidatorUrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,3 +51,71 @@ func (c *VhlClient) GetIpsUrl(ctx context.Context, shLink string, passCode strin
|
|||||||
Err: fmt.Errorf("failed to get document reference"),
|
Err: fmt.Errorf("failed to get document reference"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *VhlClient) ICVPValidate(ctx context.Context, qrData string) (*ICVPQRValidationResponse, error) {
|
||||||
|
r := ICVPQrValidationRequest{
|
||||||
|
IncludeRaw: true,
|
||||||
|
QRData: qrData,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal user payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vu := fmt.Sprintf("%s/decode/hcert", c.ICVPValidatorUrl)
|
||||||
|
req, err := http.NewRequest("POST", vu, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: 500,
|
||||||
|
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to create request"}},
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.Client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: 502,
|
||||||
|
Body: []map[string]interface{}{{"error": "service_unavailable", "message": "Failed to connect to VHL service"}},
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer utils.CloseBody(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to read response body"}},
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Body: []map[string]interface{}{{"error": "service_error", "message": string(bodyBytes)}},
|
||||||
|
Err: fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bb, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: 500,
|
||||||
|
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to read response body"}},
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var valResp ICVPQRValidationResponse
|
||||||
|
err = json.Unmarshal(bb, &valResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.HttpError{
|
||||||
|
StatusCode: 500,
|
||||||
|
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to read parse response body"}},
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &valResp, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,11 @@ type QrValidationRequest struct {
|
|||||||
QRCodeContent string `json:"qrCodeContent,required"`
|
QRCodeContent string `json:"qrCodeContent,required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ICVPQrValidationRequest struct {
|
||||||
|
IncludeRaw bool `json:"include_raw,required"`
|
||||||
|
QRData string `json:"qr_data,required"`
|
||||||
|
}
|
||||||
|
|
||||||
type ValidationResponseStep struct {
|
type ValidationResponseStep struct {
|
||||||
Step string `json:"step,omitempty"`
|
Step string `json:"step,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
@ -48,3 +53,73 @@ type VhlManifestResponseFile struct {
|
|||||||
ContentType string `json:"contentType,omitempty"`
|
ContentType string `json:"contentType,omitempty"`
|
||||||
Location string `json:"location,required"`
|
Location string `json:"location,required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------
|
||||||
|
// ICVP Validation response strucs
|
||||||
|
type ICVPQRValidationResponse struct {
|
||||||
|
COSE COSEData `json:"cose"`
|
||||||
|
Diagnostics DiagnosticsData `json:"diagnostics"`
|
||||||
|
HCERT interface{} `json:"hcert"` // Can be null, use interface{}
|
||||||
|
Payload PayloadData `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type COSEData struct {
|
||||||
|
Raw RawData `json:"_raw"`
|
||||||
|
KidB64 string `json:"kid_b64"`
|
||||||
|
KidHex string `json:"kid_hex"`
|
||||||
|
Protected ProtectedData `json:"protected"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
Unprotected map[string]interface{} `json:"unprotected"` // Empty object, use map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawData struct {
|
||||||
|
PayloadBstr string `json:"payload_bstr"`
|
||||||
|
ProtectedBstr string `json:"protected_bstr"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtectedData struct {
|
||||||
|
Key1 int `json:"1"` // The key is the number '1'
|
||||||
|
Key4 Key4Data `json:"4"` // The key is the number '4'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key4Data struct {
|
||||||
|
B64 string `json:"_b64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiagnosticsData struct {
|
||||||
|
Base45DecodedLen int `json:"base45_decoded_len"`
|
||||||
|
ZlibDecompressedLen int `json:"zlib_decompressed_len"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayloadData struct {
|
||||||
|
Key260 Payload260Data `json:"-260"` // The key is the number '-260'
|
||||||
|
Key1 string `json:"1"` // The key is the number '1'
|
||||||
|
Key6 int `json:"6"` // The key is the number '6'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload260Data struct {
|
||||||
|
Key6 InnerPayloadData `json:"-6"` // The key is the number '-6'
|
||||||
|
}
|
||||||
|
|
||||||
|
type InnerPayloadData struct {
|
||||||
|
DOB string `json:"dob"`
|
||||||
|
N string `json:"n"`
|
||||||
|
NDT string `json:"ndt"`
|
||||||
|
NID string `json:"nid"`
|
||||||
|
NTL string `json:"ntl"`
|
||||||
|
S string `json:"s"`
|
||||||
|
V VaccinationData `json:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VaccinationData struct {
|
||||||
|
BO string `json:"bo"`
|
||||||
|
CN string `json:"cn"`
|
||||||
|
DT string `json:"dt"`
|
||||||
|
IS string `json:"is"`
|
||||||
|
VLE string `json:"vle"`
|
||||||
|
VLS string `json:"vls"`
|
||||||
|
VP string `json:"vp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------
|
||||||
|
|||||||
@ -78,3 +78,11 @@ func (vs *VhlService) GetQrIps(ctx context.Context, qrData string, passCode stri
|
|||||||
}
|
}
|
||||||
return ipsBundle, nil
|
return ipsBundle, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vs *VhlService) GetICVPValidation(ctx context.Context, qrData string) (*client.ICVPQRValidationResponse, error) {
|
||||||
|
validationData, err := vs.Client.ICVPValidate(ctx, qrData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return validationData, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,10 @@ type VhlGetRequest struct {
|
|||||||
PassCode string `json:"pass_code,omitempty"`
|
PassCode string `json:"pass_code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ICVPValidateRequest struct {
|
||||||
|
Data string `json:"data,require"`
|
||||||
|
}
|
||||||
|
|
||||||
type VhlResponse struct {
|
type VhlResponse struct {
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
Payload map[string]interface{} `json:"payload"`
|
Payload map[string]interface{} `json:"payload"`
|
||||||
@ -168,3 +172,65 @@ func (vh *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate ICVP data for ICVP that dont come from a IPS
|
||||||
|
//
|
||||||
|
// @Summary Validate ICVP.
|
||||||
|
// @Description Validate ICVP data. Usefull for ICVPs not linked to a IPS.
|
||||||
|
// @Tags IPS FHIR
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
//
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
//
|
||||||
|
// @Param data body ICVPValidateRequest true "Data parameters"
|
||||||
|
//
|
||||||
|
// @Success 200 {object} any
|
||||||
|
// @Failure 400
|
||||||
|
// @Failure 404
|
||||||
|
// @Failure 500
|
||||||
|
// @Router /qr/validate [post]
|
||||||
|
func (vh *Handler) Validate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// TODO throw correct error body
|
||||||
|
var body ICVPValidateRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
icvpValidationResponseData, err := vh.Service.GetICVPValidation(ctx, body.Data)
|
||||||
|
if err != nil {
|
||||||
|
var httpErr *customErrors.HttpError
|
||||||
|
if errors.As(err, &httpErr) {
|
||||||
|
res, err := json.Marshal(httpErr.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to encode error response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(httpErr.StatusCode)
|
||||||
|
_, err = w.Write(res)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := json.Marshal(icvpValidationResponseData)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = w.Write(res)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user