diff --git a/config.yaml b/config.yaml index 46e0845b..5dcd9cbc 100644 --- a/config.yaml +++ b/config.yaml @@ -110,6 +110,7 @@ ui: session_cookie_authentication_key: "PjanW5cOBIlWzjLK23Q8NIo4va53e1bsgWmcqMdznVzkW3uEozfotj7MZsD7HpBo" #The encryption key, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes. session_store_encryption_key: "SQxqb3LKw1YFyAiy4j7FaGGJKeEzr8Db" + session_inactivity_timeout_in_seconds: 600 services: apigw: base_url: http://vc_dev_apigw:8080 diff --git a/internal/mockas/apiv1/utils.go b/internal/mockas/apiv1/utils.go index aefae1a8..407ef5c5 100644 --- a/internal/mockas/apiv1/utils.go +++ b/internal/mockas/apiv1/utils.go @@ -19,6 +19,7 @@ type MockInputData struct { FamilyName string `json:"family_name"` BirthDate string `json:"birth_date"` CollectID string `json:"collect_id"` + IdentitySchemaName string `json:"identity_schema_name"` } type uploadMock struct { @@ -52,10 +53,15 @@ func (c *Client) mockOne(ctx context.Context, data MockInputData) (*uploadMock, if data.CollectID == "" { data.CollectID = gofakeit.UUID() } + if data.DocumentID == "" { data.DocumentID = gofakeit.UUID() } + if data.IdentitySchemaName == "" { + data.IdentitySchemaName = "SE" + } + meta := &model.MetaData{ AuthenticSource: data.AuthenticSource, DocumentType: data.DocumentType, @@ -84,7 +90,7 @@ func (c *Client) mockOne(ctx context.Context, data MockInputData) (*uploadMock, { AuthenticSourcePersonID: data.AuthenticSourcePersonID, Schema: &model.IdentitySchema{ - Name: "SE", + Name: data.IdentitySchemaName, Version: "1.0.0", }, FamilyName: data.FamilyName, diff --git a/internal/ui/apiv1/apigw_client.go b/internal/ui/apiv1/apigw_client.go index fc9f5f7d..33cca1b6 100644 --- a/internal/ui/apiv1/apigw_client.go +++ b/internal/ui/apiv1/apigw_client.go @@ -40,3 +40,27 @@ func (c *APIGWClient) Upload(req *apiv1_apigw.UploadRequest) (any, error) { } return reply, nil } + +func (c *APIGWClient) Credential(req *CredentialRequest) (any, error) { + reply, err := c.DoPostJSON("/api/v1/credential", req) + if err != nil { + return nil, err + } + return reply, nil +} + +func (c *APIGWClient) GetDocument(req *GetDocumentRequest) (any, error) { + reply, err := c.DoPostJSON("/api/v1/document", req) + if err != nil { + return nil, err + } + return reply, nil +} + +func (c *APIGWClient) Notification(request *NotificationRequest) (any, error) { + reply, err := c.DoPostJSON("/api/v1/notification", request) + if err != nil { + return nil, err + } + return reply, nil +} diff --git a/internal/ui/apiv1/handlers.go b/internal/ui/apiv1/handlers.go index f3a66484..5f99e915 100644 --- a/internal/ui/apiv1/handlers.go +++ b/internal/ui/apiv1/handlers.go @@ -16,14 +16,14 @@ func (c *Client) Health(ctx context.Context, req *apiv1_status.StatusRequest) (* } type LoginRequest struct { - Username string `json:"username" binding:"required"` - Password string `json:"password" binding:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` } type LoggedinReply struct { - Username string `json:"username" binding:"required"` + Username string `json:"username" validate:"required"` // LoggedInTime RFC3339 - LoggedInTime time.Time `json:"logged_in_time" binding:"required"` + LoggedInTime time.Time `json:"logged_in_time" validate:"required"` } func (c *Client) Login(ctx context.Context, req *LoginRequest) (*LoggedinReply, error) { @@ -55,12 +55,6 @@ type DocumentListRequest struct { ValidTo int64 `json:"valid_to"` } -type PortalRequest struct { - DocumentType string `json:"document_type" binding:"required"` - AuthenticSource string `json:"authentic_source" binding:"required"` - AuthenticSourcePersonId string `json:"authentic_source_person_id" binding:"required"` -} - func (c *Client) DocumentList(ctx context.Context, req *DocumentListRequest) (any, error) { reply, err := c.apigwClient.DocumentList(req) if err != nil { @@ -77,8 +71,55 @@ func (c *Client) Upload(ctx context.Context, req *apiv1_apigw.UploadRequest) (an return reply, nil } +type CredentialRequest struct { + AuthenticSource string `json:"authentic_source" validate:"required"` + Identity *model.Identity `json:"identity" validate:"required"` + DocumentType string `json:"document_type" validate:"required"` + CredentialType string `json:"credential_type" validate:"required"` + CollectID string `json:"collect_id" validate:"required"` +} + +func (c *Client) Credential(ctx context.Context, req *CredentialRequest) (any, error) { + reply, err := c.apigwClient.Credential(req) + if err != nil { + return nil, err + } + return reply, nil +} + +type GetDocumentRequest struct { + AuthenticSource string `json:"authentic_source" validate:"required"` + DocumentType string `json:"document_type" validate:"required"` + DocumentID string `json:"document_id" validate:"required"` +} + +func (c *Client) GetDocument(ctx context.Context, req *GetDocumentRequest) (any, error) { + reply, err := c.apigwClient.GetDocument(req) + if err != nil { + return nil, err + } + return reply, nil +} + +type NotificationRequest struct { + AuthenticSource string `json:"authentic_source" validate:"required"` + DocumentType string `json:"document_type" validate:"required"` + DocumentID string `json:"document_id" validate:"required"` +} + +func (c *Client) Notification(ctx context.Context, request *NotificationRequest) (any, error) { + reply, err := c.apigwClient.Notification(request) + if err != nil { + return nil, err + } + return reply, nil +} + type MockNextRequest struct { - PortalRequest + DocumentType string `json:"document_type" validate:"required"` + AuthenticSource string `json:"authentic_source" validate:"required"` + AuthenticSourcePersonId string `json:"authentic_source_person_id" validate:"required"` + IdentitySchemaName string `json:"identity_schema_name" validate:"required"` } func (c *Client) MockNext(ctx context.Context, req *MockNextRequest) (any, error) { diff --git a/internal/ui/httpserver/api.go b/internal/ui/httpserver/api.go index 9cd5f7fc..6e2515f7 100644 --- a/internal/ui/httpserver/api.go +++ b/internal/ui/httpserver/api.go @@ -9,16 +9,19 @@ import ( type Apiv1 interface { // ui - Health(ctx context.Context, req *apiv1_status.StatusRequest) (*apiv1_status.StatusReply, error) - Login(ctx context.Context, req *apiv1.LoginRequest) (*apiv1.LoggedinReply, error) + Health(ctx context.Context, request *apiv1_status.StatusRequest) (*apiv1_status.StatusReply, error) + Login(ctx context.Context, request *apiv1.LoginRequest) (*apiv1.LoggedinReply, error) Logout(ctx context.Context) error User(ctx context.Context) (*apiv1.LoggedinReply, error) // apigw StatusAPIGW(ctx context.Context, request *apiv1_status.StatusRequest) (any, error) - DocumentList(ctx context.Context, req *apiv1.DocumentListRequest) (any, error) - Upload(ctx context.Context, req *apigw_apiv1.UploadRequest) (any, error) + DocumentList(ctx context.Context, request *apiv1.DocumentListRequest) (any, error) + Upload(ctx context.Context, request *apigw_apiv1.UploadRequest) (any, error) + Credential(ctx context.Context, request *apiv1.CredentialRequest) (any, error) + GetDocument(ctx context.Context, request *apiv1.GetDocumentRequest) (any, error) + Notification(ctx context.Context, reguest *apiv1.NotificationRequest) (any, error) // mockas - MockNext(ctx context.Context, req *apiv1.MockNextRequest) (any, error) + MockNext(ctx context.Context, request *apiv1.MockNextRequest) (any, error) } diff --git a/internal/ui/httpserver/endpoints.go b/internal/ui/httpserver/endpoints.go index 4a59cf5d..e0617d80 100644 --- a/internal/ui/httpserver/endpoints.go +++ b/internal/ui/httpserver/endpoints.go @@ -23,8 +23,7 @@ func (s *Service) endpointHealth(ctx context.Context, c *gin.Context) (any, erro func (s *Service) endpointLogin(ctx context.Context, c *gin.Context) (any, error) { request := &apiv1.LoginRequest{} - //TODO(mk): use pkg bind.go after it has been fixed instead of context.go - if err := c.ShouldBindJSON(&request); err != nil { + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { return nil, err } @@ -98,12 +97,11 @@ func (s *Service) endpointAPIGWStatus(ctx context.Context, c *gin.Context) (any, func (s *Service) endpointDocumentList(ctx context.Context, c *gin.Context) (any, error) { request := &apiv1.DocumentListRequest{} - //TODO(mk): use pkg bind.go after it has been fixed instead of context.go - if err := c.ShouldBindJSON(&request); err != nil { + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { return nil, err } - reply, err := s.apiv1.DocumentList(ctx, request) + reply, err := s.apiv1.DocumentList(ctx, request) if err != nil { return nil, err } @@ -112,14 +110,48 @@ func (s *Service) endpointDocumentList(ctx context.Context, c *gin.Context) (any func (s *Service) endpointUpload(ctx context.Context, c *gin.Context) (any, error) { request := &apiv1_apigw.UploadRequest{} - //TODO(mk): use pkg bind.go after it has been fixed instead of context.go - if err := c.ShouldBindJSON(&request); err != nil { - s.log.Debug("Binding error", "error", err) + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { return nil, err } reply, err := s.apiv1.Upload(ctx, request) + if err != nil { + return nil, err + } + return reply, nil +} + +func (s *Service) endpointCredential(ctx context.Context, c *gin.Context) (any, error) { + request := &apiv1.CredentialRequest{} + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { + return nil, err + } + + reply, err := s.apiv1.Credential(ctx, request) + if err != nil { + return nil, err + } + return reply, nil +} + +func (s *Service) endpointGetDocument(ctx context.Context, c *gin.Context) (any, error) { + request := &apiv1.GetDocumentRequest{} + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { + return nil, err + } + reply, err := s.apiv1.GetDocument(ctx, request) + if err != nil { + return nil, err + } + return reply, nil +} +func (s *Service) endpointNotification(ctx context.Context, c *gin.Context) (any, error) { + request := &apiv1.NotificationRequest{} + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { + return nil, err + } + reply, err := s.apiv1.Notification(ctx, request) if err != nil { return nil, err } @@ -128,12 +160,11 @@ func (s *Service) endpointUpload(ctx context.Context, c *gin.Context) (any, erro func (s *Service) endpointMockNext(ctx context.Context, c *gin.Context) (any, error) { request := &apiv1.MockNextRequest{} - //TODO(mk): use pkg bind.go after it has been fixed instead of context.go - if err := c.ShouldBindJSON(&request); err != nil { + if err := s.httpHelpers.Binding.Request(ctx, c, request); err != nil { return nil, err } - reply, err := s.apiv1.MockNext(ctx, request) + reply, err := s.apiv1.MockNext(ctx, request) if err != nil { return nil, err } diff --git a/internal/ui/httpserver/service.go b/internal/ui/httpserver/service.go index 01a31f5a..c3acb1a1 100644 --- a/internal/ui/httpserver/service.go +++ b/internal/ui/httpserver/service.go @@ -49,7 +49,7 @@ func New(ctx context.Context, cfg *model.Cfg, apiv1 *apiv1.Client, tracer *trace server: &http.Server{}, sessionConfig: &sessionConfig{ name: "vc_ui_auth_session", - inactivityTimeoutInSeconds: 300, + inactivityTimeoutInSeconds: cfg.UI.SessionInactivityTimeoutInSeconds, path: "/", httpOnly: true, secure: cfg.UI.APIServer.TLS.Enabled, @@ -91,6 +91,9 @@ func New(ctx context.Context, cfg *model.Cfg, apiv1 *apiv1.Client, tracer *trace s.httpHelpers.Server.RegEndpoint(ctx, rgAPIGW, http.MethodGet, "health", s.endpointAPIGWStatus) s.httpHelpers.Server.RegEndpoint(ctx, rgAPIGW, http.MethodPost, "document/list", s.endpointDocumentList) s.httpHelpers.Server.RegEndpoint(ctx, rgAPIGW, http.MethodPost, "upload", s.endpointUpload) + s.httpHelpers.Server.RegEndpoint(ctx, rgAPIGW, http.MethodPost, "credential", s.endpointCredential) + s.httpHelpers.Server.RegEndpoint(ctx, rgAPIGW, http.MethodPost, "document", s.endpointGetDocument) + s.httpHelpers.Server.RegEndpoint(ctx, rgAPIGW, http.MethodPost, "notification", s.endpointNotification) rgMockAS := rgSecure.Group("mockas") s.httpHelpers.Server.RegEndpoint(ctx, rgMockAS, http.MethodPost, "mock/next", s.endpointMockNext) diff --git a/internal/ui/static/index.html b/internal/ui/static/index.html index 08e09dca..ff579180 100644 --- a/internal/ui/static/index.html +++ b/internal/ui/static/index.html @@ -19,15 +19,11 @@
+ + + +
diff --git a/internal/ui/static/ui.js b/internal/ui/static/ui.js index 9dda3b59..fac5591e 100644 --- a/internal/ui/static/ui.js +++ b/internal/ui/static/ui.js @@ -138,6 +138,17 @@ function handleErrorInArticle(err, elements) { updateTextContentInChildPreTagFor(elements.payloadDiv, ""); } + +function openModalQR() { + const modal = document.getElementById("qrModal"); + modal.classList.add("is-active"); +} + +function closeModalQR() { + const modal = document.getElementById("qrModal"); + modal.classList.remove("is-active"); +} + async function doFetchAPICallAndHandleResult(url, options, elements) { try { //TODO(mk): add timeout on clientside for fetch @@ -158,6 +169,15 @@ async function doFetchAPICallAndHandleResult(url, options, elements) { updateTextContentInChildPreTagFor(elements.respMetaDiv, buildResponseMeta(response)); updateTextContentInChildPreTagFor(elements.errorDiv, ""); updateTextContentInChildPreTagFor(elements.payloadDiv, JSON.stringify(jsonBody, null, 2)); + + //TODO(mk): refactor this quick and dirty solution to display the QR code in a modal for /notification before the standard response article + if (url.href.includes("notification") && jsonBody && jsonBody.data && jsonBody.data.base64_image) { + const base64Image = jsonBody.data.base64_image; + const imgElement = document.getElementById("qrCodeImage"); + imgElement.src = "data:image/png;base64," + base64Image; + openModalQR(); + } + } catch (err) { handleErrorInArticle(err, elements); } @@ -182,8 +202,7 @@ async function getAndDisplayInArticleContainerFor(path, articleHeaderText) { await doFetchAPICallAndHandleResult(url, options, elements); } - -async function postAndDisplayInArticleContainerFor(path, postBody, articleHeaderText) { +async function postAndDisplayInArticleContainerFor(path, requestBody, articleHeaderText) { const url = new URL(path, baseUrl); console.debug("Call to postAndDisplayInArticleContainerFor: " + url); @@ -193,7 +212,7 @@ async function postAndDisplayInArticleContainerFor(path, postBody, articleHeader 'Accept': 'application/json', 'Content-Type': 'application/json; charset=utf-8', }; const options = { - method: `POST`, headers: headers, body: JSON.stringify(postBody), + method: `POST`, headers: headers, body: JSON.stringify(requestBody), }; updateTextContentInChildPreTagFor(elements.reqMetaDiv, `${JSON.stringify(options, null, 2)}`) @@ -201,53 +220,43 @@ async function postAndDisplayInArticleContainerFor(path, postBody, articleHeader await doFetchAPICallAndHandleResult(url, options, elements); } -function doPostForDemo(path, articleHeaderText) { + +const createMock = () => { + console.debug("createMock"); + const path = "/secure/mockas/mock/next"; + const articleHeaderText = "Upload new mock document result"; + const documentTypeElement = getElementById("document-type-select"); const authenticSourceElement = getElementById("authentic-source-input"); const authenticSourcePersonIdElement = getElementById("authentic_source_person_id-input"); - - if (!(validateHasValueAndNotEmpty(documentTypeElement) && validateHasValueAndNotEmpty(authenticSourceElement) && validateHasValueAndNotEmpty(authenticSourcePersonIdElement))) { - //TODO(mk): show an error message for input params - return - } + const identitySchemaNameElement = getElementById("identity-schema-name"); const postBody = { document_type: documentTypeElement.value, authentic_source: authenticSourceElement.value, authentic_source_person_id: authenticSourcePersonIdElement.value, + identity_schema_name: identitySchemaNameElement.value, }; postAndDisplayInArticleContainerFor(path, postBody, articleHeaderText); -} - -const createMock = () => { - console.debug("createMock"); - const path = "/secure/mockas/mock/next"; - const articleHeaderText = "Upload new mock"; - doPostForDemo(path, articleHeaderText); }; const postDocumentList = () => { console.debug("postDocumentList"); const path = "/secure/apigw/document/list"; - const articleHeaderText = "document/list"; + const articleHeaderText = "List documents result"; const documentTypeElement = getElementById("document-type-select"); const authenticSourceElement = getElementById("authentic-source-input"); const authenticSourcePersonIdElement = getElementById("authentic_source_person_id-input"); - - if (!(validateHasValueAndNotEmpty(documentTypeElement) && validateHasValueAndNotEmpty(authenticSourceElement) && validateHasValueAndNotEmpty(authenticSourcePersonIdElement))) { - //TODO(mk): show an error message for input params - return; - } + const identitySchemaName = getElementById("identity-schema-name"); const documentListRequest = { authentic_source: authenticSourceElement.value, identity: { authentic_source_person_id: authenticSourcePersonIdElement.value, schema: { - name: "SE", - version: "1.0.0" + name: identitySchemaName.value } }, document_type: documentTypeElement.value @@ -279,23 +288,23 @@ const buildArticle = (articleID, articleHeaderText, bodyChildrenElementArray) => const expandCollapseButton = document.createElement('button'); expandCollapseButton.onclick = () => toggleExpandCollapseArticle(articleID); expandCollapseButton.classList.add("button", "is-dark"); - expandCollapseButton.textContent = "Collapse/Expand" - expandCollapseButton.ariaLabel = "toggle collapse/expand" + expandCollapseButton.textContent = "Collapse/Expand"; + expandCollapseButton.ariaLabel = "toggle collapse/expand"; const removeButton = document.createElement('button'); removeButton.onclick = () => removeElementById(articleID); removeButton.classList.add("delete", "is-medium"); - removeButton.ariaLabel = "delete" + removeButton.ariaLabel = "delete"; const pElement = document.createElement('p'); pElement.textContent = articleHeaderText ? articleHeaderText : ""; const divHeader = document.createElement('div'); - divHeader.classList.add("message-header") - divHeader.prepend(pElement, expandCollapseButton, removeButton) + divHeader.classList.add("message-header"); + divHeader.prepend(pElement, expandCollapseButton, removeButton); const divBody = document.createElement('div'); - divBody.classList.add("message-body") + divBody.classList.add("message-body"); if (bodyChildrenElementArray != null && bodyChildrenElementArray.length !== 0) { // Add to body in the same order as the elements in the array for (const bodyChildElement of bodyChildrenElementArray.reverse()) { @@ -313,7 +322,7 @@ const buildArticle = (articleID, articleHeaderText, bodyChildrenElementArray) => async function doLogin() { const url = new URL("/login", baseUrl); - console.debug("doLogin for url: " + url) + console.debug("doLogin for url: " + url); const doLoginButton = getElementById("do-login-btn"); doLoginButton.disabled = true; @@ -398,13 +407,169 @@ const addUploadFormArticleToContainer = () => { }; const articleIdBasis = generateArticleIDBasis(); - const articleDiv = buildArticle(articleIdBasis.articleID, "Upload", buildUploadFormElements()); + const articleDiv = buildArticle(articleIdBasis.articleID, "Upload document", buildUploadFormElements()); const articleContainer = getElementById('article-container'); articleContainer.prepend(articleDiv); getElementById("upload-textarea").focus(); }; +const createInputElement = (placeholder, value = '', type = 'text', disabled = false) => { + const input = document.createElement('input'); + input.id = generateUUID(); + input.classList.add('input'); + input.type = type; + input.placeholder = placeholder; + input.value = value; + input.disabled = disabled; + return input; +}; + +const disableElements = (elements) => { + elements.forEach(el => el.disabled = true); +}; + +const addViewDocumentFormArticleToContainer = () => { + const buildFormElements = () => { + + const documentIDElement = createInputElement('document id'); + const documentTypeElement = createInputElement('document type (EHIC/PDA1)', 'EHIC'); + const authenticSourceElement = createInputElement('authentic source', 'SUNET'); + + const viewButton = document.createElement('button'); + viewButton.id = generateUUID(); + viewButton.classList.add('button', 'is-link'); + viewButton.textContent = 'View'; + viewButton.onclick = () => { + viewButton.disabled = true; + + const requestBody = { + document_id: documentIDElement.value, + authentic_source: authenticSourceElement.value, + document_type: documentTypeElement.value, + }; + + disableElements([ + documentIDElement, documentTypeElement, authenticSourceElement + ]); + + postAndDisplayInArticleContainerFor("/secure/apigw/document", requestBody, "Document"); + }; + + return [documentIDElement, documentTypeElement, authenticSourceElement, viewButton]; + }; + + const articleIdBasis = generateArticleIDBasis(); + const articleDiv = buildArticle(articleIdBasis.articleID, "View document", buildFormElements()); + const articleContainer = document.getElementById('article-container'); + articleContainer.prepend(articleDiv); + + document.getElementById(articleIdBasis.articleID).querySelector('input').focus(); +}; + + +const addViewNotificationFormArticleToContainer = () => { + const buildFormElements = () => { + + const documentIDElement = createInputElement('document id'); + const documentTypeElement = createInputElement('document type (EHIC/PDA1)', 'EHIC'); + const authenticSourceElement = createInputElement('authentic source', 'SUNET'); + + const viewButton = document.createElement('button'); + viewButton.id = generateUUID(); + viewButton.classList.add('button', 'is-link'); + viewButton.textContent = 'View'; + viewButton.onclick = () => { + viewButton.disabled = true; + + const requestBody = { + document_id: documentIDElement.value, + authentic_source: authenticSourceElement.value, + document_type: documentTypeElement.value, + }; + + disableElements([ + documentIDElement, documentTypeElement, authenticSourceElement + ]); + + postAndDisplayInArticleContainerFor("/secure/apigw/notification", requestBody, "Notification"); + }; + + return [documentIDElement, documentTypeElement, authenticSourceElement, viewButton]; + }; + + const articleIdBasis = generateArticleIDBasis(); + const articleDiv = buildArticle(articleIdBasis.articleID, "View notification", buildFormElements()); + const articleContainer = document.getElementById('article-container'); + articleContainer.prepend(articleDiv); + + document.getElementById(articleIdBasis.articleID).querySelector('input').focus(); +}; + +const addCredentialFormArticleToContainer = () => { + const buildFormElements = () => { + + const authenticSourcePersonIdElement = createInputElement('authentic source person id'); + const familyNameElement = createInputElement('family name', '', 'text'); + const givenNameElement = createInputElement('given name', '', 'text'); + const birthdateElement = createInputElement('birth date', '', 'text'); + const schemaNameElement = createInputElement('identity schema name', 'SE'); + const documentTypeElement = createInputElement('document type (EHIC/PDA1)', 'EHIC'); + const credentialTypeElement = createInputElement('credential type', 'SD-JWT'); + const authenticSourceElement = createInputElement('authentic source', 'SUNET'); + const collectIdElement = createInputElement('collect id'); + + const createButton = document.createElement('button'); + createButton.id = generateUUID(); + createButton.classList.add('button', 'is-link'); + createButton.textContent = 'Create'; + createButton.onclick = () => { + createButton.disabled = true; + + const requestBody = { + authentic_source: authenticSourceElement.value, + identity: { + authentic_source_person_id: authenticSourcePersonIdElement.value, //required if not EIDAS attributes is set (family_name, given_name and birth_date) + schema: { + name: schemaNameElement.value + }, + family_name: familyNameElement.value, + given_name: givenNameElement.value, + birth_date: birthdateElement.value, + }, + document_type: documentTypeElement.value, + credential_type: credentialTypeElement.value, + collect_id: collectIdElement.value, + }; + + disableElements([ + authenticSourcePersonIdElement, familyNameElement, givenNameElement, + birthdateElement, schemaNameElement, documentTypeElement, + credentialTypeElement, authenticSourceElement, collectIdElement + ]); + + postAndDisplayInArticleContainerFor("/secure/apigw/credential", requestBody, "Credential"); + }; + + const lineElement = document.createElement('hr'); + const orTextElement = document.createElement('p'); + orTextElement.textContent = 'or'; + + return [ + authenticSourcePersonIdElement, orTextElement, familyNameElement, givenNameElement, + birthdateElement, lineElement, collectIdElement, schemaNameElement, documentTypeElement, + credentialTypeElement, authenticSourceElement, createButton + ]; + }; + + const articleIdBasis = generateArticleIDBasis(); + const articleDiv = buildArticle(articleIdBasis.articleID, "Create credential", buildFormElements()); + const articleContainer = document.getElementById('article-container'); + articleContainer.prepend(articleDiv); + + document.getElementById(articleIdBasis.articleID).querySelector('input').focus(); +}; + const addLoginArticleToContainer = () => { const buildLoginElements = () => { const usernameField = document.createElement('div'); diff --git a/pkg/model/config.go b/pkg/model/config.go index 231cdc30..2d1c8edd 100644 --- a/pkg/model/config.go +++ b/pkg/model/config.go @@ -182,12 +182,13 @@ type OTEL struct { // UI holds the user-interface configuration type UI struct { - APIServer APIServer `yaml:"api_server" validate:"required"` - Username string `yaml:"username" validate:"required"` - Password string `yaml:"password" validate:"required"` - SessionCookieAuthenticationKey string `yaml:"session_cookie_authentication_key" validate:"required"` - SessionStoreEncryptionKey string `yaml:"session_store_encryption_key" validate:"required"` - Services struct { + APIServer APIServer `yaml:"api_server" validate:"required"` + Username string `yaml:"username" validate:"required"` + Password string `yaml:"password" validate:"required"` + SessionCookieAuthenticationKey string `yaml:"session_cookie_authentication_key" validate:"required"` + SessionStoreEncryptionKey string `yaml:"session_store_encryption_key" validate:"required"` + SessionInactivityTimeoutInSeconds int `yaml:"session_inactivity_timeout_in_seconds" validate:"required"` + Services struct { APIGW struct { BaseURL string `yaml:"base_url"` } `yaml:"apigw"`