Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show qr codes in portal and Search documents in UI when not in production mode #123

Merged
merged 28 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5db5098
Add test for POST document.
masv3971 Dec 19, 2024
6c4ba2c
UI+APIGW: first working embryo-version of search documents when not i…
matskramer Nov 29, 2024
f3aeaed
UI: concept for exact response type supported in post
matskramer Dec 2, 2024
ed4900a
APIGW: search documents now has a limit for number of results
matskramer Dec 3, 2024
2dc0f55
APIGW: search documents now can return a dynamic projection and/or so…
matskramer Dec 3, 2024
b5c1417
APIGW: even more advanced search documents to support known needs in …
matskramer Dec 4, 2024
69207a8
UI: more search filters, dynamic limit and checkbox to show complete …
matskramer Dec 5, 2024
11646f0
Portal qr: portal service now serve a static index.html with a bulma …
matskramer Dec 5, 2024
771dc68
Portal qr: common worker files for ui and portal
matskramer Dec 5, 2024
1f513bd
Portal qr: upgraded bulma.io css to v1.0.2 and simple static HTML lay…
matskramer Dec 5, 2024
7675c29
Portal qr: endpoint to search documents in portal calling apigw using…
matskramer Dec 5, 2024
5ee2323
Portal qr: work in progress to display qr code and other attributes f…
matskramer Dec 6, 2024
ce33f4e
Portal qr: simple display of users qr codes for business desicions (d…
matskramer Dec 9, 2024
cfe24c5
Portal qr: some info about each qr-code.
matskramer Dec 9, 2024
497831d
UI: result of search documents now displays in a table incl. error ha…
matskramer Dec 11, 2024
184c618
UI: now possible to display complete document and create a credential…
matskramer Dec 12, 2024
f101484
UI: removed local config value for qr url and now displays search err…
matskramer Dec 12, 2024
751e801
UI: qr-code can now be displayed in modal from documents table
matskramer Dec 13, 2024
f1ea640
UI: follow qr-code link in new window/tab, better copy of json from m…
matskramer Dec 13, 2024
eb839d0
UI: Search documents button in main menu bar and the search form is a…
matskramer Dec 13, 2024
373a419
UI: A document can now be deleted.
matskramer Dec 13, 2024
8b0456e
UI: Renamed Documents to documents in search reply
matskramer Dec 13, 2024
8569c1a
UI: minor UI improvments (complete document extracted from search res…
matskramer Dec 16, 2024
1e7c6b9
UI: added some documentation to exported funcs. Max limit for search …
matskramer Dec 18, 2024
484d5da
Enhanced pkg http client impl and also using it for search documents …
matskramer Dec 19, 2024
b736eb2
Portal now has config with fewer levels
matskramer Dec 20, 2024
d3f2f8f
Removed custom http clients from Portal
matskramer Dec 20, 2024
64be2b0
New config values for Portal
matskramer Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ build-ui:

docker-build: docker-build-verifier docker-build-registry docker-build-persistent docker-build-mockas docker-build-apigw docker-build-issuer docker-build-ui docker-build-portal

docker-build-goland-debug: docker-build-verifier docker-build-registry docker-build-persistent docker-build-mockas docker-build-apigw docker-build-issuer docker-build-ui-goland-debug
docker-build-debug: docker-build-verifier docker-build-registry docker-build-persistent docker-build-mockas docker-build-apigw docker-build-issuer docker-build-ui-debug docker-build-portal-debug

docker-build-gobuild:
$(info Docker Building gobuild with tag: $(VERSION))
Expand Down Expand Up @@ -108,15 +108,19 @@ docker-build-issuer:

docker-build-ui:
$(info Docker building ui with tag: $(VERSION))
docker build --build-arg SERVICE_NAME=ui --tag $(DOCKER_TAG_UI) --file dockerfiles/ui_worker .
docker build --build-arg SERVICE_NAME=ui --tag $(DOCKER_TAG_UI) --file dockerfiles/web_worker .

docker-build-ui-goland-debug:
docker-build-ui-debug:
$(info Docker building ui with tag: $(VERSION))
docker build --build-arg SERVICE_NAME=ui --tag $(DOCKER_TAG_UI) --file dockerfiles/ui_worker_goland_debug .
docker build --build-arg SERVICE_NAME=ui --tag $(DOCKER_TAG_UI) --file dockerfiles/web_worker_debug .

docker-build-portal:
$(info Docker building portal with tag: $(VERSION))
docker build --build-arg SERVICE_NAME=portal --tag $(DOCKER_TAG_PORTAL) --file dockerfiles/worker .
docker build --build-arg SERVICE_NAME=portal --tag $(DOCKER_TAG_PORTAL) --file dockerfiles/web_worker .

docker-build-portal-debug:
$(info Docker building portal with tag: $(VERSION))
docker build --build-arg SERVICE_NAME=portal --tag $(DOCKER_TAG_PORTAL) --file dockerfiles/web_worker_debug .

docker-push-gobuild:
$(info Pushing docker images)
Expand Down
7 changes: 7 additions & 0 deletions cmd/portal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package main

import (
"context"
"encoding/gob"
"os"
"os/signal"
"sync"
"syscall"
"time"
"vc/internal/portal/apiv1"
"vc/internal/portal/httpserver"
"vc/pkg/configuration"
"vc/pkg/logger"
"vc/pkg/trace"
)

func init() {
// Needed to serialize/deserialize time.Time in the session and cookie
gob.Register(time.Time{})
}

type service interface {
Close(ctx context.Context) error
}
Expand Down
10 changes: 6 additions & 4 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ authentic_sources:
country_code: "SE"
notification_endpoint:
url: "http://vc_dev_apigw:8080/api/v1/document/notify"
authentic_source_endpoint:
authentic_source_endpoint:
url: "http://vc_dev_apigw:8080"
signature_service_endpoint:
signature_service_endpoint:
url: "http://vc_dev_issuer:8080"
revocation_service_endpoint:
revocation_service_endpoint:
url: "http://vc_dev_apigw:8080/api/v1/document/revok"
credential_types:
EHIC:
Expand Down Expand Up @@ -59,7 +59,7 @@ issuer:
addr: vc_dev_issuer:8090
signing_key_path: "/private_ec256.pem"
jwt_attribute:
issuer: https://issuer.sunet.se
issuer: https://issuer.sunet.se
enable_not_before: true
valid_duration: 3600
verifiable_credential_type: "https://credential.sunet.se/identity_credential"
Expand Down Expand Up @@ -123,3 +123,5 @@ ui:
portal:
api_server:
addr: :8080
apigw_api_server:
addr: http://vc_dev_apigw:8080
3 changes: 2 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ services:
restart: always
volumes:
- ./config.yaml:/config.yaml:ro
- ./statics:/statics:ro
depends_on:
- apigw
networks:
vc-dev-net:
ipv4_address: 172.16.50.24
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/ui_worker → dockerfiles/web_worker
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ RUN apt-get update && apt-get install -y curl procps iputils-ping less
RUN rm -rf /var/lib/apt/lists/*

COPY --from=builder /go/src/app/bin/vc_${SERVICE_NAME} /vc_service
COPY --from=builder /go/src/app/internal/ui/static /static/
COPY --from=builder /go/src/app/internal/${SERVICE_NAME}/static /static/

EXPOSE 8080

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ RUN rm -rf /var/lib/apt/lists/*

COPY --from=builder /go/bin/dlv /
COPY --from=builder /go/src/app/bin/vc_${SERVICE_NAME} /vc_service
COPY --from=builder /go/src/app/internal/ui/static /static/
COPY --from=builder /go/src/app/internal/${SERVICE_NAME}/static /static/

EXPOSE 8080 40000

Expand Down
24 changes: 24 additions & 0 deletions internal/apigw/apiv1/handlers_datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,27 @@ func (c *Client) RevokeDocument(ctx context.Context, req *RevokeDocumentRequest)

return nil
}

// SearchDocuments search for documents
func (c *Client) SearchDocuments(ctx context.Context, req *model.SearchDocumentsRequest) (*model.SearchDocumentsReply, error) {
docs, hasMore, err := c.db.VCDatastoreColl.SearchDocuments(ctx, &db.SearchDocumentsQuery{
AuthenticSource: req.AuthenticSource,
DocumentType: req.DocumentType,
DocumentID: req.DocumentID,
CollectID: req.CollectID,

AuthenticSourcePersonID: req.AuthenticSourcePersonID,
FamilyName: req.FamilyName,
GivenName: req.GivenName,
BirthDate: req.BirthDate,
}, req.Limit, req.Fields, req.SortFields)

if err != nil {
return nil, err
}
resp := &model.SearchDocumentsReply{
Documents: docs,
HasMoreResults: hasMore,
}
return resp, nil
}
123 changes: 123 additions & 0 deletions internal/apigw/db/methods_vc_datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package db

import (
"context"
"errors"
"vc/pkg/helpers"
"vc/pkg/logger"
"vc/pkg/model"
Expand Down Expand Up @@ -357,3 +358,125 @@ func (c *VCDatastoreColl) Replace(ctx context.Context, doc *model.CompleteDocume
c.log.Info("updated document", "document_id", doc.Meta.DocumentID)
return nil
}

// SearchDocumentsQuery the query to search for documents
type SearchDocumentsQuery struct {
AuthenticSource string `json:"authentic_source,omitempty" validate:"omitempty,max=1000"`
DocumentType string `json:"document_type,omitempty" validate:"omitempty,max=1000"`
DocumentID string `json:"document_id,omitempty" validate:"omitempty,max=1000"`
CollectID string `json:"collect_id,omitempty" validate:"omitempty,max=1000"`

AuthenticSourcePersonID string `json:"authentic_source_person_id,omitempty"`
FamilyName string `json:"family_name,omitempty" validate:"omitempty,max=597"`
GivenName string `json:"given_name,omitempty" validate:"omitempty,max=1019"`
BirthDate string `json:"birth_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
}

// SearchDocuments search documents in datastore
//
// @return return matching documents, has more results (refine query), or error
// @Description not supported in production mode
// @Deprecated
func (c *VCDatastoreColl) SearchDocuments(ctx context.Context, query *SearchDocumentsQuery, limit int64, fields []string, sortFields map[string]int) ([]*model.CompleteDocument, bool, error) {
matskramer marked this conversation as resolved.
Show resolved Hide resolved
if c.Service.cfg.Common.Production {
return nil, false, errors.New("Not supported in production mode")
}

if err := helpers.Check(ctx, c.Service.cfg, query, c.Service.log); err != nil {
matskramer marked this conversation as resolved.
Show resolved Hide resolved
return nil, false, err
}
//TODO(mk): validate to prevent NoSQL Injection

filter := buildSearchDocumentsFilter(query)

findOptions := options.Find()
const maxLimit = 500
if limit == 0 {
limit = 50
} else if limit > maxLimit {
limit = maxLimit
}
// Set one more than wanted to see if there are more results i db
findOptions.SetLimit(limit + 1)

if len(fields) > 0 {
projection := bson.M{}
for _, field := range fields {
projection[field] = 1
}
findOptions.SetProjection(projection)
}

sort := bson.D{}
if len(sortFields) > 0 {
for field, order := range sortFields {
// 1 for ascending, -1 for descending
sort = append(sort, bson.E{Key: field, Value: order})
}
findOptions.SetSort(sort)
} else {
sort = append(sort, bson.E{Key: "meta.document_id", Value: 1})
}

c.log.Debug("Searching documents using", "filter", filter, "findOptions", findOptions)

cursor, err := c.Coll.Find(ctx, filter, findOptions)
if err != nil {
return nil, false, err
}

res := []*model.CompleteDocument{}
if err := cursor.All(ctx, &res); err != nil {
return nil, false, err
}

hasMoreResults := len(res) > int(limit)
if hasMoreResults {
// Remove the last entry from the result to fit limit value
res = res[:limit]
}

return res, hasMoreResults, nil
}

func buildSearchDocumentsFilter(query *SearchDocumentsQuery) bson.M {
filter := bson.M{}

//TODO(mk): check explain to see if any indexes are needed
matskramer marked this conversation as resolved.
Show resolved Hide resolved

if query.AuthenticSource != "" {
filter["meta.authentic_source"] = bson.M{"$eq": query.AuthenticSource}
}
if query.DocumentType != "" {
filter["meta.document_type"] = bson.M{"$eq": query.DocumentType}
}
if query.DocumentID != "" {
filter["meta.document_id"] = bson.M{"$eq": query.DocumentID}
}
if query.CollectID != "" {
filter["meta.collect.id"] = bson.M{"$eq": query.CollectID}
}

identityConditions := bson.M{}
if query.AuthenticSourcePersonID != "" {
identityConditions["authentic_source_person_id"] = query.AuthenticSourcePersonID
}
if query.FamilyName != "" {
identityConditions["family_name"] = query.FamilyName
}
if query.GivenName != "" {
identityConditions["given_name"] = query.GivenName
}
if query.BirthDate != "" {
identityConditions["birth_date"] = query.BirthDate
}
if len(identityConditions) > 0 {
filter["identities"] = bson.M{
"$elemMatch": identityConditions,
}
}

//TODO(mk): add more filters?

return filter
}
3 changes: 3 additions & 0 deletions internal/apigw/httpserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type Apiv1 interface {
AddConsent(ctx context.Context, req *apiv1.AddConsentRequest) error
GetConsent(ctx context.Context, req *apiv1.GetConsentRequest) (*model.Consent, error)

// datastore endpoints - disabled in production
SearchDocuments(ctx context.Context, req *model.SearchDocumentsRequest) (*model.SearchDocumentsReply, error)

// credential endpoints
Revoke(ctx context.Context, req *apiv1.RevokeRequest) (*apiv1.RevokeReply, error)
Credential(ctx context.Context, req *apiv1.CredentialRequest) (*apiv1_issuer.MakeSDJWTReply, error)
Expand Down
18 changes: 18 additions & 0 deletions internal/apigw/httpserver/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"vc/internal/apigw/apiv1"
"vc/internal/gen/status/apiv1_status"
"vc/pkg/model"

"go.opentelemetry.io/otel/codes"

Expand Down Expand Up @@ -102,6 +103,23 @@ func (s *Service) endpointGetDocument(ctx context.Context, c *gin.Context) (any,
return reply, nil
}

func (s *Service) endpointSearchDocuments(ctx context.Context, c *gin.Context) (any, error) {
ctx, span := s.tracer.Start(ctx, "httpserver:endpointSearchDocuments")
defer span.End()

request := &model.SearchDocumentsRequest{}
if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, err
}
reply, err := s.apiv1.SearchDocuments(ctx, request)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, err
}
return reply, nil
}

func (s *Service) endpointRevokeDocument(ctx context.Context, c *gin.Context) (any, error) {
ctx, span := s.tracer.Start(ctx, "httpserver:endpointRevokeDocument")
defer span.End()
Expand Down
1 change: 1 addition & 0 deletions internal/apigw/httpserver/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func New(ctx context.Context, cfg *model.Cfg, apiv1 *apiv1.Client, tracer *trace
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/identity/mapping", s.endpointIdentityMapping)
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/document/list", s.endpointDocumentList)
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/document", s.endpointGetDocument)
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/document/search", s.endpointSearchDocuments)
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/consent", s.endpointAddConsent)
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/consent/get", s.endpointGetConsent)
s.httpHelpers.Server.RegEndpoint(ctx, rgAPIv1, http.MethodPost, "/document/revoke", s.endpointRevokeDocument)
Expand Down
9 changes: 9 additions & 0 deletions internal/portal/apiv1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apiv1

import (
"context"
"vc/pkg/datastoreclient"
"vc/pkg/logger"
"vc/pkg/model"
"vc/pkg/trace"
Expand All @@ -12,6 +13,8 @@ type Client struct {
cfg *model.Cfg
log *logger.Log
tracer *trace.Tracer

apigwClient *datastoreclient.Client
}

// New creates a new instance of the public api
Expand All @@ -22,6 +25,12 @@ func New(ctx context.Context, tracer *trace.Tracer, cfg *model.Cfg, log *logger.
log: log.New("apiv1"),
}

var err error
c.apigwClient, err = datastoreclient.New(&datastoreclient.Config{URL: cfg.Portal.ApigwApiServer.Addr})
if err != nil {
return nil, err
}

c.log.Info("Started")

return c, nil
Expand Down
14 changes: 14 additions & 0 deletions internal/portal/apiv1/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package apiv1

import (
"context"
"errors"
"net/http"
"vc/internal/gen/status/apiv1_status"
"vc/pkg/model"
)
Expand All @@ -14,3 +16,15 @@ func (c *Client) Status(ctx context.Context, req *apiv1_status.StatusRequest) (*

return status, nil
}

// SearchDocuments search for documents
func (c *Client) SearchDocuments(ctx context.Context, req *model.SearchDocumentsRequest) (*model.SearchDocumentsReply, error) {
reply, httpResponse, err := c.apigwClient.Document.Search(ctx, req)
if err != nil {
return nil, err
}
if httpResponse.StatusCode != http.StatusOK {
return nil, errors.New(httpResponse.Status)
}
return reply, nil
}
3 changes: 3 additions & 0 deletions internal/portal/httpserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package httpserver
import (
"context"
"vc/internal/gen/status/apiv1_status"
"vc/pkg/model"
)

// Apiv1 interface
type Apiv1 interface {
Status(ctx context.Context, req *apiv1_status.StatusRequest) (*apiv1_status.StatusReply, error)

SearchDocuments(ctx context.Context, request *model.SearchDocumentsRequest) (*model.SearchDocumentsReply, error)
}
Loading
Loading