From 1a96060195687ba5b20a3f1be2dfa17ea7f434c0 Mon Sep 17 00:00:00 2001 From: Mats Kramer Date: Thu, 18 Apr 2024 10:40:45 +0200 Subject: [PATCH 1/6] Fixed some todos and removed some commented out code --- adminweb/assets/index.html | 230 +++++-------------------------------- 1 file changed, 29 insertions(+), 201 deletions(-) diff --git a/adminweb/assets/index.html b/adminweb/assets/index.html index 48e52d3..8826727 100644 --- a/adminweb/assets/index.html +++ b/adminweb/assets/index.html @@ -1,11 +1,10 @@ - + - VC demo web @@ -20,6 +19,10 @@ getElementById(id)?.remove(); } + const validateHasValueAndNotEmpty = (element) => { + return element && element.value && element.value.trim() !== "" && element.value.trim() !== " "; + } + const clearContainer = (id) => { console.debug(`Clearing element : ${id}`); const element = getElementById(id); @@ -31,7 +34,6 @@ const clearAllContentContainers = () => { clearContainer("login-container"); clearContainer("article-container"); - //clearSearchDocumentForm(); } function displayAElement(id) { @@ -64,7 +66,7 @@ let currentIdNumber = 0; const secureGenerateUUID = () => { - //TODO: JonL: hur kan jag få till UUID:er, medför "Uncaught TypeError: crypto.randomUUID is not a function" + //TODO: JonL: hur kan jag få till UUID:er, medför crypto.randomUUID() "Uncaught TypeError: crypto.randomUUID is not a function" //return crypto.randomUUID(); return currentIdNumber++; }; @@ -88,32 +90,17 @@ }; } - //TODO: Inför kontroll om att användaren med största sannolikhet är inloggad, om ej skicka till login med något meddelande om det - function isLoggedIn() { const cookie = document.cookie if (cookie && cookie.match(/vcadminwebsession=(.*?)(;|$)/)[1]) { console.debug("User is logged in"); - // TODO: visa en timer när sessionen upphör att gälla om inga nya secure request utförs - // // Beräkna när cookien upphör att gälla - // const expiryDate = new Date(Date.now() + maxAge * 1000); - // - // // Visa en timer som visar hur länge användaren har varit inloggad - // const timerElement = document.getElementById("timer"); - // const updateTimer = () => { - // const remainingTime = Math.floor((expiryDate - Date.now()) / 1000); - // timerElement.textContent = `${remainingTime} sekunder kvar`; - // }; - // updateTimer(); - // setInterval(updateTimer, 1000); return true; } console.debug("User is not logged in"); - //Note: Giltighetstiden hanteras av webbläsaren och en cookie försvinner från document.cookie när den har löpt ut. + //Note: Expire time for cookie is handled by the browser and is removed from document.cookie when expired return false; } - const addNewRequestResponseArticleToContainer = (articleHeaderText) => { const articleIDBasis = generateArticleIDBasis(); const uuid = articleIDBasis.uuid; @@ -132,26 +119,21 @@ reqMetaDiv: buildDiv('Request meta', 'req-meta'), errorDiv: buildDiv('Error', 'error'), respMetaDiv: buildDiv('Respons meta', 'resp-meta'), - //debugDiv: buildDiv('Debug', 'debug-meta'), payloadDiv: buildDiv('Payload', 'payload'), }; - const articleDiv = buildArticle(articleID, articleHeaderText, [bodyChildren.reqMetaDiv, //bodyChildren.debugDiv, - bodyChildren.errorDiv, bodyChildren.respMetaDiv, bodyChildren.payloadDiv]); + const articleDiv = buildArticle(articleID, articleHeaderText, [bodyChildren.reqMetaDiv, bodyChildren.errorDiv, bodyChildren.respMetaDiv, bodyChildren.payloadDiv]); const articleContainer = getElementById('article-container'); articleContainer.prepend(articleDiv); - return bodyChildren; } function buildResponseMeta(response) { - const status = response.status; // HTTP-statuskoden - const statusText = response.statusText; // Status textbeskrivning ("OK", "Not Found", etc.) - const respUrl = response.url; // Den slutliga URL:en efter omdirigeringar - const contentType = response.headers.get('Content-Type'); // Innehållstypen från svaret - const responseMetaData = `Response status: ${status} (${statusText}), URL: ${respUrl}, Content-Type: ${contentType}`; - console.debug(responseMetaData); - return responseMetaData; + const status = response.status; // + const statusText = response.statusText; + const respUrl = response.url; + const contentType = response.headers.get('Content-Type'); + return `Response status: ${status} (${statusText}), url: ${respUrl}, Content-Type: ${contentType}`; } function updateTextContentInChildPreTagFor(parentElement, textContent) { @@ -165,14 +147,13 @@ const errorPreElement = elements.errorDiv.querySelector("pre"); errorPreElement.style.color = "red"; updateTextContentInChildPreTagFor(elements.errorDiv, err.name + " " + err.message); - //updateTextContentInChildPreTagFor(elements.debugDiv, ""); updateTextContentInChildPreTagFor(elements.respMetaDiv, ""); updateTextContentInChildPreTagFor(elements.payloadDiv, ""); } async function getAndDisplayInArticleContainerFor(path, articleHeaderText) { const url = baseUrl.concat(path); - console.debug("Call to fetchJsonAndDisplayInArticleContainerFor: " + url); + console.debug("Call to getAndDisplayInArticleContainerFor: " + url); const elements = addNewRequestResponseArticleToContainer(articleHeaderText); @@ -185,19 +166,13 @@ headers: headers, }; - - // Skriv ut request headers för demonstration - // console.debug(`Request method: ${request.method}`); - // for (const [key, value] of request.headers.entries()) { - // console.debug(`Request header ${key}: ${value}`); - // } - updateTextContentInChildPreTagFor(elements.reqMetaDiv, `${JSON.stringify(options, null, 2)}`) try { //TODO: add timeout on clientside for fetch? const response = await fetch(url, options); const jsonBody = await response.json(); + console.debug(jsonBody); if (!response.ok) { if (response.status === 401) { @@ -206,33 +181,17 @@ hideSecureMenyItems(); return; } - throw new Error(`HTTP error! status: ${response.status}, method: ${response.method}, body: ${JSON.stringify(jsonBody, null, 2)}`); - //TODO: tagit bort url: ${url}, för demon + throw new Error(`HTTP error! status: ${response.status}, method: ${response.method}, url: ${url}, body: ${JSON.stringify(jsonBody, null, 2)}`); } - const responseMetaData = buildResponseMeta(response); - updateTextContentInChildPreTagFor(elements.respMetaDiv, responseMetaData); - - console.debug(jsonBody); - + updateTextContentInChildPreTagFor(elements.respMetaDiv, buildResponseMeta(response)); updateTextContentInChildPreTagFor(elements.errorDiv, ""); - //updateTextContentInChildPreTagFor(elements.debugDiv, ""); updateTextContentInChildPreTagFor(elements.payloadDiv, JSON.stringify(jsonBody, null, 2)); } catch (err) { handleErrorInArticle(err, elements); } } - const validateHasValueAndNotEmpty = (element) => { - return element && element.value && element.value.trim() !== "" && element.value.trim() !== " "; - } - - function clearSearchDocumentForm(docTypeSelect) { - getElementById("document-id-input").value = ""; - getElementById('doc-type-select').selectedIndex = 0; - updateFetchDocumentByIdButton(); - } - async function postAndDisplayInArticleContainerFor(path, postBody, articleHeaderText) { const url = baseUrl.concat(path); @@ -250,18 +209,13 @@ body: JSON.stringify(postBody), }; - // Skriv ut request headers för demonstration - // console.debug(`Request method: ${request.method}`); - // for (const [key, value] of request.headers.entries()) { - // console.debug(`Request header ${key}: ${value}`); - // } - updateTextContentInChildPreTagFor(elements.reqMetaDiv, `${JSON.stringify(options, null, 2)}`) try { //TODO: add timeout on clientside for fetch? const response = await fetch(url, options); const jsonBody = await response.json(); + console.debug(jsonBody); if (!response.ok) { if (response.status === 401) { @@ -270,18 +224,11 @@ hideSecureMenyItems(); return; } - - throw new Error(`HTTP error! status: ${response.status}, method: ${response.method}, body: ${JSON.stringify(jsonBody, null, 2)}`); - //TODO: tagit bort url: ${url}, för demon + throw new Error(`HTTP error! status: ${response.status}, method: ${response.method}, url: ${url}, body: ${JSON.stringify(jsonBody, null, 2)}`); } - const responseMetaData = buildResponseMeta(response); - updateTextContentInChildPreTagFor(elements.respMetaDiv, responseMetaData); - - console.debug(jsonBody); - + updateTextContentInChildPreTagFor(elements.respMetaDiv, buildResponseMeta(response)); updateTextContentInChildPreTagFor(elements.errorDiv, ""); - //updateTextContentInChildPreTagFor(elements.debugDiv, ""); updateTextContentInChildPreTagFor(elements.payloadDiv, JSON.stringify(jsonBody, null, 2)); } catch (err) { handleErrorInArticle(err, elements); @@ -300,7 +247,7 @@ && validateHasValueAndNotEmpty(authenticSourcePersonIdElement) ) ) { - //TODO: visa felmeddelande i GUI + //TODO: show an error message for input params return } @@ -310,11 +257,10 @@ authentic_source_person_id: authenticSourcePersonIdElement.value, }; - postAndDisplayInArticleContainerFor(path, postBody, articleHeaderText); - //TODO: kanske rensa formuläret? } + /* ie upload */ const createMock = () => { console.debug("createMock"); const path = "/secure/mock"; @@ -329,44 +275,16 @@ doPostForDemo(path, articleHeaderText); } - - // const fetchDocumentById = () => { - // console.debug("fetchDocumentById"); - // const documentIdInput = getElementById("document-id-input"); - // if (validateHasValueAndNotEmpty(documentIdInput)) { - // const documentId = documentIdInput.value.trim(); - // console.debug("documentId: " + documentId); - // - // const docTypeSelect = getElementById('doc-type-select'); - // const docType = docTypeSelect.value; - // console.debug("docType: " + docType) - // //TODO: skicka med docType i requestet (bodyn vid post) - // - // // Reset search fields to enable another search - // clearSearchDocumentForm(); - // - // //TODO: gör om till en POST istället då det kommer variera vad vi vill söka på (samt säkrare) - // getAndDisplayInArticleContainerFor(`/secure/document/${documentId}`) - // } - // } - const updateUploadAndFetchButtons = () => { const input = getElementById('authentic_source_person_id-input'); const mockButton = getElementById('create-mock-btn'); const fetchButton = getElementById('fetch-from-portal-btn'); - // const inputIsValid = ...; //TODO: Validering för input, ex så ej blank, dvs "", eller " "- kär tr + //TODO: Validate input values? mockButton.disabled = !(input.value); fetchButton.disabled = !(input.value); } - // const updateFetchDocumentByIdButton = () => { - // const input = getElementById('document-id-input'); - // const button = getElementById('fetch-document-by-id-btn'); - // // const inputIsValid = ...; //TODO: Validering för input, ex så ej blank, dvs "", eller " "- kär tr - // button.disabled = !(input.value); - // } - /** Builds an article with custom body children but does not add it to the DOM * * @param articleID required @@ -405,7 +323,6 @@ return article; } - async function doLogin() { const url = baseUrl.concat("/login"); console.debug("doLogin for url: " + url) @@ -421,8 +338,6 @@ const password = passwordInput.value; passwordInput.disabled = true; - //console.debug("Login try with: ", username, password); - const postBody = { username: username, password: password, @@ -443,15 +358,11 @@ const response = await fetch(url, request); if (!response.ok) { - //TODO: bättre felmeddelande throw new Error(`HTTP error! status: ${response.status}, method: ${request.method}, url: ${url}, headers: ${JSON.stringify(response.headers)}`); } - // Loggar metadata för response - buildResponseMeta(response); - - const jsonPromise = await response.json(); - console.debug(jsonPromise); + // const jsonBody = await response.json(); + // console.debug(jsonBody); authOK = true; } catch (err) { console.debug("Login attempt failed: ", err.message); @@ -460,9 +371,7 @@ if (authOK) { clearContainer("login-container"); displaySecureMenyItems(); - //TODO: visa menyer som ska vara dolda när man ej är auth - även det sistnämnda behöver fixas (dvs ej visa ~privata - - //TODO: show logged in user + //TODO: show logged in user? } else { usernameInput.disabled = false; passwordInput.disabled = false; @@ -559,28 +468,13 @@ headers: headers }); - // Skriv ut request headers för demonstration - // console.debug(`Request method: ${request.method}`); - // for (const [key, value] of request.headers.entries()) { - // console.debug(`Request header ${key}: ${value}`); - // } - //TODO: inför felhantering await fetch(request); hideSecureMenyItems(); clearAllContentContainers(); } - // document.addEventListener('DOMContentLoaded', function () { - // // Hides all secure menu iterm when page is loaded - // // Comment out the line below to display secure meny items on page load regardless of auth or not - // hideSecureMenyItems(); - // - // }); - window.addEventListener('load', function () { - // Hides all secure menu iterm when page is loaded (including css, images, etc) - // Comment out the line below to display secure meny items on page load regardless of auth or not if (isLoggedIn()) { displaySecureMenyItems(); } else { @@ -591,22 +485,13 @@ - - - - - - - +
- -
+ -
From e1da477d335fc6a7aade068fdfa940080491c1e0 Mon Sep 17 00:00:00 2001 From: Mats Kramer Date: Thu, 18 Apr 2024 12:53:09 +0200 Subject: [PATCH 2/6] Added Collapse/Expand for each article --- adminweb/assets/index.html | 106 ++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 61 deletions(-) diff --git a/adminweb/assets/index.html b/adminweb/assets/index.html index 8826727..1eb3b94 100644 --- a/adminweb/assets/index.html +++ b/adminweb/assets/index.html @@ -5,6 +5,7 @@ + VC demo web @@ -133,7 +134,7 @@ const statusText = response.statusText; const respUrl = response.url; const contentType = response.headers.get('Content-Type'); - return `Response status: ${status} (${statusText}), url: ${respUrl}, Content-Type: ${contentType}`; + return `Response status: ${status} (${statusText}), Content-Type: ${contentType}`; } function updateTextContentInChildPreTagFor(parentElement, textContent) { @@ -151,23 +152,7 @@ updateTextContentInChildPreTagFor(elements.payloadDiv, ""); } - async function getAndDisplayInArticleContainerFor(path, articleHeaderText) { - const url = baseUrl.concat(path); - console.debug("Call to getAndDisplayInArticleContainerFor: " + url); - - const elements = addNewRequestResponseArticleToContainer(articleHeaderText); - - const headers = { - 'Accept': 'application/json', - }; - - const options = { - method: `GET`, - headers: headers, - }; - - updateTextContentInChildPreTagFor(elements.reqMetaDiv, `${JSON.stringify(options, null, 2)}`) - + async function doFetchAPICallAndHandleResult(url, options, elements) { try { //TODO: add timeout on clientside for fetch? const response = await fetch(url, options); @@ -192,9 +177,29 @@ } } + async function getAndDisplayInArticleContainerFor(path, articleHeaderText) { + const url = new URL(path, baseUrl); + console.debug("Call to getAndDisplayInArticleContainerFor: " + url); + + const elements = addNewRequestResponseArticleToContainer(articleHeaderText); + + const headers = { + 'Accept': 'application/json', + }; + + const options = { + method: `GET`, + headers: headers, + }; + + updateTextContentInChildPreTagFor(elements.reqMetaDiv, `${JSON.stringify(options, null, 2)}`) + + await doFetchAPICallAndHandleResult(url, options, elements); + } + async function postAndDisplayInArticleContainerFor(path, postBody, articleHeaderText) { - const url = baseUrl.concat(path); + const url = new URL(path, baseUrl); console.debug("Call to postAndDisplayInArticleContainerFor: " + url); const elements = addNewRequestResponseArticleToContainer(articleHeaderText); @@ -211,28 +216,7 @@ updateTextContentInChildPreTagFor(elements.reqMetaDiv, `${JSON.stringify(options, null, 2)}`) - try { - //TODO: add timeout on clientside for fetch? - const response = await fetch(url, options); - const jsonBody = await response.json(); - console.debug(jsonBody); - - if (!response.ok) { - if (response.status === 401) { - // Not auth/session expired - clearAllContentContainers(); - hideSecureMenyItems(); - return; - } - throw new Error(`HTTP error! status: ${response.status}, method: ${response.method}, url: ${url}, body: ${JSON.stringify(jsonBody, null, 2)}`); - } - - updateTextContentInChildPreTagFor(elements.respMetaDiv, buildResponseMeta(response)); - updateTextContentInChildPreTagFor(elements.errorDiv, ""); - updateTextContentInChildPreTagFor(elements.payloadDiv, JSON.stringify(jsonBody, null, 2)); - } catch (err) { - handleErrorInArticle(err, elements); - } + await doFetchAPICallAndHandleResult(url, options, elements); } function doPostForDemo(path, articleHeaderText) { @@ -294,6 +278,13 @@ * @returns {HTMLElement} article */ const buildArticle = (articleID, articleHeaderText, bodyChildrenElementArray) => { + // + const expandCollapseButton = document.createElement('button'); + expandCollapseButton.onclick = () => toggleExpandCollapseArticle(articleID); + expandCollapseButton.classList.add("button", "is-text", "is-medium"); + expandCollapseButton.textContent = "Collapse/Expand" + expandCollapseButton.ariaLabel = "toggle expand/collapse" + const removeButton = document.createElement('button'); removeButton.onclick = () => removeElementById(articleID); removeButton.classList.add("delete", "is-medium"); @@ -304,7 +295,7 @@ const divHeader = document.createElement('div'); divHeader.classList.add("message-header") - divHeader.prepend(pElement, removeButton) + divHeader.prepend(pElement, expandCollapseButton, removeButton) const divBody = document.createElement('div'); divBody.classList.add("message-body") @@ -324,7 +315,7 @@ } async function doLogin() { - const url = baseUrl.concat("/login"); + const url = new URL("/login", baseUrl); console.debug("doLogin for url: " + url) const doLoginButton = getElementById("do-login-btn"); @@ -455,7 +446,7 @@ } async function doLogout() { - const url = baseUrl.concat("/secure/logout"); + const url = new URL("/secure/logout", baseUrl); console.debug("doLogout for url: " + url) const headers = { @@ -474,6 +465,16 @@ clearAllContentContainers(); } + function toggleExpandCollapseArticle(articleId) { + const article = document.getElementById(articleId); + const content = article.querySelector('.message-body'); + if (content.style.display === 'none') { + content.style.display = 'block'; // Ändra detta värde beroende på din stil + } else { + content.style.display = 'none'; + } + } + window.addEventListener('load', function () { if (isLoggedIn()) { displaySecureMenyItems(); @@ -491,23 +492,6 @@ From e295ca22e0b3e2cd4c2f5a5c65ed1f588c18294a Mon Sep 17 00:00:00 2001 From: Mats Kramer Date: Thu, 18 Apr 2024 21:21:58 +0200 Subject: [PATCH 3/6] Added config for session auth (cookie) and session internal crypt/decrypt. Values has to be changed in production. --- adminweb/cmd/main.go | 263 +++++------------------------------ adminweb/config.yaml | 2 + adminweb/pkg/model/config.go | 9 +- 3 files changed, 40 insertions(+), 234 deletions(-) diff --git a/adminweb/cmd/main.go b/adminweb/cmd/main.go index 466cecd..a3b180b 100644 --- a/adminweb/cmd/main.go +++ b/adminweb/cmd/main.go @@ -17,51 +17,21 @@ import ( "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/google/uuid" - //Backup of some imports since the IDE sometimes removes them to fast - //"bytes" - //"context" - //"encoding/json" - //"github.com/gin-contrib/gzip" - //"github.com/gin-contrib/sessions" - //"github.com/gin-contrib/sessions/cookie" - //"github.com/gin-gonic/gin" - //"github.com/google/uuid" - //"io" - //"log" - //"net/http" - //"strings" - //"vcweb1/pkg/configuration" - //"vcweb1/pkg/logger" - //"vcweb1/pkg/model" ) -//TODO: Inför vettigare loggning (och ta bort log.Print...) -//TODO: inför timeout vid anrop -//TODO: inför rate-limit -//TODO: ... +//TODO: add logging const ( - //TODO: ta in apigwBaseUrl via config.yaml - //apigwBaseUrl = "http://172.16.50.2:8080" - //mockasBaseUrl = "http://172.16.50.13:8080" - //mockasAPIBaseURL = mockasBaseUrl + "/api/v1" - //apigwAPIBaseUrl = apigwBaseUrl + "/api/v1" - - /* session... constants also used for the session cookie */ - //TODO: ta in sessionKey via config.yaml + /* session... constants is also used for the session cookie */ sessionName = "vcadminwebsession" //if changed, the web (javascript) must also be updated with the new name sessionKey = "user" sessionInactivityTimeoutInSeconds = 3600 //one hour - also the value for the cookie sessionPath = "/" sessionHttpOnly = true - sessionSecure = false //TODO: Aktivate for https - sessionSameSite = 0 //TODO: http.SameSiteDefaultMode //TODO: se över lämpligt val + sessionSecure = false //TODO: activate for https + sessionSameSite = http.SameSiteStrictMode ) -/* Secret for session cookie store (16-byte, 32-, ...) */ -//TODO: ta sessionStoreSecret koden från config.yaml -var sessionStoreSecret = []byte("very-secret-code") - func main() { ctx := context.Background() cfg, err := configuration.Parse(ctx, logger.NewSimple("Configuration")) @@ -74,11 +44,10 @@ func main() { router.Use(gin.Logger()) //TODO: router.Use(gin.MinifyHTML()) router.Use(gzip.Gzip(gzip.DefaultCompression)) - router.Use(setupSessionMiddleware(sessionStoreSecret, sessionInactivityTimeoutInSeconds, "/")) + router.Use(setupSessionMiddleware(cfg)) // Static route router.Static("/assets", "./assets") - router.LoadHTMLFiles("./assets/index.html") router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) @@ -97,14 +66,10 @@ func main() { secureRouter.POST("/portal", fetchFromPortalHandler(cfg, &httpClient)) secureRouter.DELETE("/logout", logoutHandler) secureRouter.GET("/health", getHealthHandler(cfg, &httpClient)) - secureRouter.GET("/document/:document_id", getDocumentByIdHandler(cfg, &httpClient)) - secureRouter.GET("/devjsonobj", getDevJsonObjHandler) - secureRouter.GET("/devjsonarray", getDevJsonArrayHandler) secureRouter.GET("/user", getUserHandler) - secureRouter.GET("/loginstatus", getLoginStatusHandler) } - //TODO: Inför https (TLS) stöd + //TODO: add https (TLS) support if err := router.Run(":8080"); err != nil { log.Fatal("Unable to start gin engine:", err) } @@ -126,34 +91,30 @@ func fetchFromPortalHandler(cfg *model.Cfg, client *http.Client) gin.HandlerFunc } } -func doPostForDemoFlows(c *gin.Context, url string, client *http.Client) { - //{ - // document_type: - // authentic_source: "SUNET", ta även in denna från GUI (sätt som statisk) - // authentic_source_person_id: - //} - - type Body struct { - DocumentType string `json:"document_type" binding:"required"` - AuthenticSource string `json:"authentic_source" binding:"required"` - AuthenticSourcePersonId string `json:"authentic_source_person_id" binding:"required"` - } +type demoFlowRequestBody struct { + DocumentType string `json:"document_type" binding:"required"` + AuthenticSource string `json:"authentic_source" binding:"required"` + AuthenticSourcePersonId string `json:"authentic_source_person_id" binding:"required"` +} - var reqBody Body +func doPostForDemoFlows(c *gin.Context, url string, client *http.Client) { + var reqBody demoFlowRequestBody if err := c.ShouldBindJSON(&reqBody); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } - log.Print("reqBody: ", reqBody) - reqBodyJSON, err := json.Marshal(reqBody) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Error marshalling body"}) return } + doPostRequest(c, url, client, err, reqBodyJSON) +} + +func doPostRequest(c *gin.Context, url string, client *http.Client, err error, reqBodyJSON []byte) { req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBodyJSON)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"Error creating new http req": err.Error()}) @@ -177,27 +138,26 @@ func doPostForDemoFlows(c *gin.Context, url string, client *http.Client) { var jsonResp map[string]interface{} if err := json.Unmarshal(body, &jsonResp); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"Error Unmarshal resp Body": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"Error Unmarshal response to json": err.Error()}) return } - log.Print("resp statuscode from vc: ", resp.StatusCode) - log.Print("#### resp body: ", jsonResp) - c.JSON(resp.StatusCode, jsonResp) } -func setupSessionMiddleware(secret []byte, maxAge int, path string) gin.HandlerFunc { +func setupSessionMiddleware(cfg *model.Cfg) gin.HandlerFunc { // Configure session cookie store - store := configureSessionStore(secret, maxAge, path) + store := configureSessionStore(cfg) return sessions.Sessions(sessionName, store) } -func configureSessionStore(secret []byte, maxAge int, path string) sessions.Store { - store := cookie.NewStore(secret) +func configureSessionStore(cfg *model.Cfg) sessions.Store { + //The first parameter is used to encrypt and decrypt cookies. + //The second parameter is used internally by cookie.Store to handle the encryption and decryption process + store := cookie.NewStore([]byte(cfg.Web1.SessionCookieAuthenticationKey), []byte(cfg.Web1.SessionStoreEncryptionKey)) store.Options(sessions.Options{ Path: sessionPath, - MaxAge: maxAge, // 5 minuter i sekunder - javascript koden tar hänsyn till detta för att försöka gissa om användaren fortsatt är inloggad (om inloggad också vill säga) + MaxAge: sessionInactivityTimeoutInSeconds, Secure: sessionSecure, HttpOnly: sessionHttpOnly, SameSite: sessionSameSite, @@ -209,7 +169,6 @@ func authRequired(c *gin.Context) { session := sessions.Default(c) user := session.Get(sessionKey) if user == nil { - // Abort the request with the appropriate error code c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized/session expired"}) return } @@ -239,17 +198,17 @@ func isLogoutRoute(c *gin.Context) bool { return strings.HasSuffix(path, "/logout") && method == "DELETE" } +type loginBody struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + func loginHandler(cfg *model.Cfg) func(c *gin.Context) { //closure return func(c *gin.Context) { session := sessions.Default(c) - type LoginBody struct { - Username string `json:"username" binding:"required"` - Password string `json:"password" binding:"required"` - } - - var loginBody LoginBody + var loginBody loginBody if err := c.ShouldBindJSON(&loginBody); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return @@ -260,7 +219,7 @@ func loginHandler(cfg *model.Cfg) func(c *gin.Context) { return } - // TODO: ev. use a userID instead of username + // TODO: use a userID or UUID instead of username session.Set(sessionKey, loginBody.Username) if err := session.Save(); err != nil { //This is also where the cookie is created c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"}) @@ -301,53 +260,27 @@ func getUserHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"user": user}) } -func getLoginStatusHandler(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "You are logged in"}) -} - func getHealthHandler(cfg *model.Cfg, client *http.Client) gin.HandlerFunc { return func(c *gin.Context) { - //url := cfg.Web1.Services.MockAS.Addr + "/api/v1/mock/next" - //doPostForDemoFlows(c, url, client) - - //return func(c *gin.Context) { url := cfg.Web1.Services.APIGW.Addr + "/health" - //log.Printf("URL: %s", url) - - //TODO: MS: vad är konceptet för att hantera/köra https client mot apigw? - //TODO: lägga in timeout - //TODO: på flera ställen: HTTP Client: You create a new HTTP client for every request. Instead of creating a new client for every HTTP request, use a single client for all requests can be beneficial so that TCP connections can be reused. - //client := http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { - //log.Printf("Error while preparing request to url: %s %s", url, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"Error creating new http req": err.Error()}) - //return nil } resp, err := client.Do(req) - //if resp != nil { - // log.Print("Respons header:", resp.Header) - //} if err != nil { - //log.Printf("Error during reguest to url: %s %s", url, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"Error req": err.Error()}) - // return nil } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { - //log.Printf("Error during reguest to url: %s %s", url, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"Error read resp": err.Error()}) - // return nil } - //log.Print("Response Body:", string(data)) - c.Data(resp.StatusCode, "application/json", data) - //} } } @@ -356,140 +289,8 @@ func isValidUUID(str string) bool { if str == "" { return false } - if _, err := uuid.Parse(str); err != nil { return false } - return true } - -func getDocumentByIdHandler(cfg *model.Cfg, client *http.Client) gin.HandlerFunc { - - return func(c *gin.Context) { - - documentId := c.Param("document_id") - - if !isValidUUID(documentId) { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "UUID expected or has wrong format"}) - return - } - - url := cfg.Web1.Services.APIGW.Addr + "/api/v1/portal" - //log.Printf("URL: %s", url) - - //TODO: MS: vad är konceptet för att hantera/köra https client mot apigw? - //TODO: lägga in timeout - //client := http.Client{} - - jsonBody := map[string]string{ - //"authentic_source": "SUNET", - //"document_id": documentId, - //"document_type": "EHIC", - "authentic_source": "SUNET", - "authentic_source_person_id": documentId, - "validity_from": "1970-01-01", - "validity_to": "1970-01-01", - } - - /* - /api/v1/portal - - "authentic_source" - "authentic_source_person_id" - "validity_from" - "validity_to" - */ - - // Serialize 'jsonBody' to JSON-format - jsonData, err := json.Marshal(jsonBody) - if err != nil { - //log.Printf("Error marshalling jsonBody: %s", err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error marshalling jsonBody"}) - return - } - - // Create new HTTP POST reguest with jsonData as Body - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - //log.Printf("Error while preparing request to url: %s %s", url, err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"Error creating new http req": err.Error()}) - return - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - resp, err := client.Do(req) - //if resp != nil { - // log.Print("Respons header:", resp.Header) - //} - if err != nil { - //log.Printf("Error during reguest to url: %s %s", url, err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"Error req": err.Error()}) - return - } - - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - //log.Printf("Error during reguest to url: %s %s", url, err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"Error read resp": err.Error()}) - return - } - - var jsonResp map[string]interface{} - if err := json.Unmarshal(body, &jsonResp); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"Error Unmarshal resp Body": err.Error()}) - return - } - //log.Print("Response Body:", string(Body)) - - c.JSON(resp.StatusCode, jsonResp) - } -} - -/* TODO: remove before production */ -func getDevJsonArrayHandler(c *gin.Context) { - url := "http://jsonplaceholder.typicode.com/posts" //Just some random testserver on the internet that responds with a json array - //log.Printf("URL: %s", url) - - client := http.Client{} - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - //log.Printf("Error while preparing request to url: %s %s", url, err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"Error creating new http req": err.Error()}) - return - } - - resp, err := client.Do(req) - //if resp != nil { - // log.Print("Respons header:", resp.Header) - //} - if err != nil { - //log.Printf("Error during reguest to url: %s %s", url, err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"Error req": err.Error()}) - return - } - - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - //log.Printf("Error during reguest to url: %s %s", url, err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"Error read resp": err.Error()}) - return - } - - //log.Print("Response Body:", string(data)) - - c.Data(http.StatusOK, "application/json", data) -} - -/*TODO: remove before production */ -func getDevJsonObjHandler(c *gin.Context) { - - jsonData := gin.H{ - "message": "Dummy json object - hardcoded", - } - c.JSON(http.StatusOK, jsonData) -} diff --git a/adminweb/config.yaml b/adminweb/config.yaml index 69d902f..d302487 100644 --- a/adminweb/config.yaml +++ b/adminweb/config.yaml @@ -2,6 +2,8 @@ web1: username: "admin" password: "secret123" + session_cookie_authentication_key: "c61b0a82cd86d4f9ae1b2b366f9087139352dcbdbbbfa6541c6a82b30171dc8a4f8eca0eeccf9cc1640b96876779757663efb6d25eeb8422b1326aa61ff5444d" + session_store_encryption_key: "1841c012073c8b5da3f26ce709b4faf1" services: apigw: addr: "http://172.16.50.2:8080" diff --git a/adminweb/pkg/model/config.go b/adminweb/pkg/model/config.go index 7511f59..bcd76e2 100644 --- a/adminweb/pkg/model/config.go +++ b/adminweb/pkg/model/config.go @@ -14,9 +14,12 @@ type Common struct { } type Web1 struct { - Username string `yaml:"username" validate:"required"` - Password string `yaml:"password" validate:"required"` - Services struct { + // Login: user and password + 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 { APIGW struct { Addr string `yaml:"addr"` } `yaml:"apigw"` From e2dc98c86c3bc9e52a4d9af35de3b73adf843457 Mon Sep 17 00:00:00 2001 From: Mats Kramer Date: Mon, 22 Apr 2024 09:43:22 +0200 Subject: [PATCH 4/6] Added some helper comments about config values --- adminweb/config.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/adminweb/config.yaml b/adminweb/config.yaml index d302487..aa7f25f 100644 --- a/adminweb/config.yaml +++ b/adminweb/config.yaml @@ -2,8 +2,10 @@ web1: username: "admin" password: "secret123" - session_cookie_authentication_key: "c61b0a82cd86d4f9ae1b2b366f9087139352dcbdbbbfa6541c6a82b30171dc8a4f8eca0eeccf9cc1640b96876779757663efb6d25eeb8422b1326aa61ff5444d" - session_store_encryption_key: "1841c012073c8b5da3f26ce709b4faf1" + #It is recommended to use an authentication key with 32 or 64 bytes. + 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" services: apigw: addr: "http://172.16.50.2:8080" From 5ab7534f2e5558ee179ccf3dbaf0576f82ff2df6 Mon Sep 17 00:00:00 2001 From: Mats Kramer Date: Mon, 22 Apr 2024 10:24:12 +0200 Subject: [PATCH 5/6] Made collapse/expand more discreet --- adminweb/assets/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adminweb/assets/index.html b/adminweb/assets/index.html index 1eb3b94..6c35379 100644 --- a/adminweb/assets/index.html +++ b/adminweb/assets/index.html @@ -281,9 +281,9 @@ // const expandCollapseButton = document.createElement('button'); expandCollapseButton.onclick = () => toggleExpandCollapseArticle(articleID); - expandCollapseButton.classList.add("button", "is-text", "is-medium"); + expandCollapseButton.classList.add("button", "is-dark"); expandCollapseButton.textContent = "Collapse/Expand" - expandCollapseButton.ariaLabel = "toggle expand/collapse" + expandCollapseButton.ariaLabel = "toggle collapse/expand" const removeButton = document.createElement('button'); removeButton.onclick = () => removeElementById(articleID); From ace94561a4e700df1d04342397f79a7eba338ce1 Mon Sep 17 00:00:00 2001 From: Mats Kramer Date: Mon, 22 Apr 2024 10:35:28 +0200 Subject: [PATCH 6/6] Removed unused const i js --- adminweb/assets/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adminweb/assets/index.html b/adminweb/assets/index.html index 6c35379..d24150a 100644 --- a/adminweb/assets/index.html +++ b/adminweb/assets/index.html @@ -132,7 +132,7 @@ function buildResponseMeta(response) { const status = response.status; // const statusText = response.statusText; - const respUrl = response.url; + //const respUrl = response.url; const contentType = response.headers.get('Content-Type'); return `Response status: ${status} (${statusText}), Content-Type: ${contentType}`; }