From 999780b7bd6ef215f312016bd11a1de36608996a Mon Sep 17 00:00:00 2001
From: Bastien DUMONT <bdumont@grandlyon.com>
Date: Thu, 8 Aug 2024 12:25:14 +0000
Subject: [PATCH] feat(grdf): fetch access token every two hours

---
 .env.template                              |  4 +
 .gitlab-ci.yml                             | 18 ++--
 .vscode/settings.json                      |  1 +
 internal/models/grdfToken.go               | 99 ++++++++++++++++++++++
 internal/models/models.go                  | 12 +++
 internal/models/partnersInfo.go            |  1 -
 internal/rootmux/rootmux.go                |  1 +
 internal/tokens/tokens.go                  |  2 +-
 k8s/secrets/ecolyo-agent-server-config.yml |  2 +
 main.go                                    | 20 +++++
 10 files changed, 151 insertions(+), 9 deletions(-)
 create mode 100644 internal/models/grdfToken.go

diff --git a/.env.template b/.env.template
index 6eff757..f01896f 100644
--- a/.env.template
+++ b/.env.template
@@ -20,4 +20,8 @@ DATABASE_USER
 DATABASE_PASSWORD
 DATABASE_NAME
 
+# rename this to backoffice token ?
 SGE_API_TOKEN
+
+GRDF_CLIENT_ID
+GRDF_CLIENT_SECRET
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f4a4de4..9bb7369 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,7 +14,7 @@ services:
 
 variables:
   DOCKER_DRIVER: overlay2
-  DOCKER_TLS_CERTDIR: ""
+  DOCKER_TLS_CERTDIR: ''
   GIT_STRATEGY: clone
   GIT_DEPTH: 0
 
@@ -89,10 +89,10 @@ sonarqube:
     - dev
   image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/sonarsource/sonar-scanner-cli:4
   variables:
-    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
-    GIT_DEPTH: "0" # T
+    SONAR_USER_HOME: '${CI_PROJECT_DIR}/.sonar' # Defines the location of the analysis task cache
+    GIT_DEPTH: '0' # T
   cache:
-    key: "${CI_JOB_NAME}"
+    key: '${CI_JOB_NAME}'
     paths:
       - .sonar/cache
   script:
@@ -114,10 +114,10 @@ sonarqube-mr:
     - merge_requests
   image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/sonarsource/sonar-scanner-cli:4
   variables:
-    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
-    GIT_DEPTH: "0" # T
+    SONAR_USER_HOME: '${CI_PROJECT_DIR}/.sonar' # Defines the location of the analysis task cache
+    GIT_DEPTH: '0' # T
   cache:
-    key: "${CI_JOB_NAME}"
+    key: '${CI_JOB_NAME}'
     paths:
       - .sonar/cache
   script:
@@ -142,6 +142,8 @@ deploy_rec:
     - sed -i "s/{{CLIENT_ID}}/$REC_CLIENT_ID/" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s/{{CLIENT_SECRET}}/$REC_CLIENT_SECRET/" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s/{{SGE_API_TOKEN}}/$REC_SGE_API_TOKEN/" ./k8s/secrets/ecolyo-agent-server-config.yml
+    - sed -i "s/{{GRDF_CLIENT_ID}}/$GRDF_CLIENT_ID/" ./k8s/secrets/ecolyo-agent-server-config.yml
+    - sed -i "s/{{GRDF_CLIENT_SECRET}}/$GRDF_CLIENT_SECRET/" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s/{{HOSTNAME}}/ecolyo-agent-rec.apps.grandlyon.com/g" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s+{{AUTH_URL}}+https://connexion-rec.grandlyon.fr/IdPOAuth2/authorize/oidc-rec-2+" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s+{{USERINFO_URL}}+https://connexion-rec.grandlyon.fr/IdPOAuth2/userinfo/oidc-rec-2+" ./k8s/secrets/ecolyo-agent-server-config.yml
@@ -172,6 +174,8 @@ deploy_prod:
     - sed -i "s/{{CLIENT_ID}}/$PROD_CLIENT_ID/" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s/{{CLIENT_SECRET}}/$PROD_CLIENT_SECRET/" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s/{{SGE_API_TOKEN}}/$PROD_SGE_API_TOKEN/" ./k8s/secrets/ecolyo-agent-server-config.yml
+    - sed -i "s/{{GRDF_CLIENT_ID}}/$GRDF_CLIENT_ID/" ./k8s/secrets/ecolyo-agent-server-config.yml
+    - sed -i "s/{{GRDF_CLIENT_SECRET}}/$GRDF_CLIENT_SECRET/" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s/{{HOSTNAME}}/ecolyo-agent.apps.grandlyon.com/g" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s+{{AUTH_URL}}+https://connexion.grandlyon.fr/IdPOAuth2/authorize/oidc-2+" ./k8s/secrets/ecolyo-agent-server-config.yml
     - sed -i "s+{{USERINFO_URL}}+https://connexion.grandlyon.fr/IdPOAuth2/userinfo/oidc-2+" ./k8s/secrets/ecolyo-agent-server-config.yml
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c18971e..c32570c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -20,6 +20,7 @@
   },
   "peacock.color": "#32f0ff",
   "cSpell.words": [
+    "adict",
     "admininfo",
     "animatorinfo",
     "backoffice",
diff --git a/internal/models/grdfToken.go b/internal/models/grdfToken.go
new file mode 100644
index 0000000..7c531c6
--- /dev/null
+++ b/internal/models/grdfToken.go
@@ -0,0 +1,99 @@
+package models
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+	"time"
+
+	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/constants"
+	"gorm.io/gorm"
+)
+
+type GrdfAccessToken struct {
+	AccessToken string    `json:"access_token"`
+	FetchedAt   time.Time `json:"fetched_at"`
+}
+
+// Fetches GRDF auth API for an access token and save it in DB
+func FetchGRDFAuthAPI() {
+	log.Println("| Calling GRDF auth API")
+
+	dh := NewDataHandler()
+
+	reqBody := url.Values{
+		"scope":         {"/adict/v2"},
+		"grant_type":    {"client_credentials"},
+		"client_id":     {os.Getenv("GRDF_CLIENT_ID")},
+		"client_secret": {os.Getenv("GRDF_CLIENT_SECRET")},
+	}
+
+	client := &http.Client{}
+	req, err := http.NewRequest(http.MethodPost, "https://adict-connexion.grdf.fr/oauth2/aus5y2ta2uEHjCWIR417/v1/token", strings.NewReader(reqBody.Encode()))
+	if err != nil {
+		fmt.Println("Error creating request:", err)
+		return
+	}
+
+	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("Error sending request:", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("Error reading response body:", err)
+		return
+	}
+
+	var result map[string]interface{}
+	if err := json.Unmarshal(body, &result); err != nil {
+		fmt.Println("Error unmarshaling JSON:", err)
+		return
+	}
+
+	accessToken, ok := result["access_token"].(string)
+	if !ok {
+		fmt.Println("Access token not found in response")
+		return
+	}
+
+	dh.WriteAccessToken(accessToken)
+}
+
+// Writes a new access token to the database
+func (dh *DataHandler) WriteAccessToken(token string) {
+	result := dh.sqlClient.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&GrdfAccessToken{}).Updates(&GrdfAccessToken{
+		AccessToken: token,
+		FetchedAt:   time.Now(),
+	})
+
+	if result.Error != nil {
+		log.Println("| Error updating GRDF access token")
+		log.Println(result.Error)
+	} else {
+		log.Println("| New GRDF access token written")
+	}
+}
+
+func (dh *DataHandler) GetGrdfAccessToken(w http.ResponseWriter, r *http.Request) {
+	var token GrdfAccessToken
+	err := dh.sqlClient.First(&token).Error
+
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	w.Header().Set(constants.ContentType, constants.Json)
+	json.NewEncoder(w).Encode(token)
+	log.Printf("| get partnersInfo | %v", r.RemoteAddr)
+}
diff --git a/internal/models/models.go b/internal/models/models.go
index ca67395..22b3843 100644
--- a/internal/models/models.go
+++ b/internal/models/models.go
@@ -50,6 +50,7 @@ func NewDataHandler() *DataHandler {
 	sqlClient.AutoMigrate(&Price{})
 	sqlClient.AutoMigrate(&SgeConsent{})
 	sqlClient.AutoMigrate(&GrdfConsent{})
+	sqlClient.AutoMigrate(&GrdfAccessToken{})
 
 	// Check if partners info already exists
 	var partnersInfo PartnersInfo
@@ -78,5 +79,16 @@ func NewDataHandler() *DataHandler {
 		})
 	}
 
+	// check if access token already exists
+	var accessToken GrdfAccessToken
+	err = sqlClient.First(&accessToken).Error
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		// Create default access token
+		sqlClient.Create(&GrdfAccessToken{
+			AccessToken: "",
+			FetchedAt:   time.Now(),
+		})
+	}
+
 	return &DataHandler{sqlClient: sqlClient}
 }
diff --git a/internal/models/partnersInfo.go b/internal/models/partnersInfo.go
index 9237a30..7494b1f 100644
--- a/internal/models/partnersInfo.go
+++ b/internal/models/partnersInfo.go
@@ -64,5 +64,4 @@ func (dh *DataHandler) SavePartnersInfo(w http.ResponseWriter, r *http.Request)
 	w.Header().Set(constants.ContentType, constants.Json)
 	json.NewEncoder(w).Encode(partnersInfo)
 	log.Printf("| updated partnersInfo | %v", r.RemoteAddr)
-
 }
diff --git a/internal/rootmux/rootmux.go b/internal/rootmux/rootmux.go
index 962169f..7603056 100644
--- a/internal/rootmux/rootmux.go
+++ b/internal/rootmux/rootmux.go
@@ -90,6 +90,7 @@ func CreateRootMux() RootMux {
 		r.Post("/consent", dh.PostGrdfConsent)
 		r.Get("/consent/{id}", dh.GetGrdfConsentById)
 		r.Delete("/consent/{id}", dh.DeleteGrdfConsentById)
+		r.Get("/access-token", dh.GetGrdfAccessToken)
 	})
 
 	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { /** handles Oauth2 redirection */ })
diff --git a/internal/tokens/tokens.go b/internal/tokens/tokens.go
index 86a5bf8..9a940cc 100644
--- a/internal/tokens/tokens.go
+++ b/internal/tokens/tokens.go
@@ -60,7 +60,7 @@ func newManager(keyfile string, debug bool) manager {
 	}
 }
 
-// Token represents a token containting data
+// Token represents a token containing data
 type Token struct {
 	ExpiresAt int64
 	IssuedAt  int64 `json:"iat,omitempty"`
diff --git a/k8s/secrets/ecolyo-agent-server-config.yml b/k8s/secrets/ecolyo-agent-server-config.yml
index 9503f60..8974c88 100644
--- a/k8s/secrets/ecolyo-agent-server-config.yml
+++ b/k8s/secrets/ecolyo-agent-server-config.yml
@@ -20,4 +20,6 @@ stringData:
   SGE_API_TOKEN: {{SGE_API_TOKEN}}
   TOKEN_URL: {{TOKEN_URL}}
   USERINFO_URL: {{USERINFO_URL}}
+  GRDF_CLIENT_ID: {{GRDF_CLIENT_ID}}
+  GRDF_CLIENT_SECRET: {{GRDF_CLIENT_SECRET}}
 type: Opaque
diff --git a/main.go b/main.go
index 137ea8d..c8dbfb5 100644
--- a/main.go
+++ b/main.go
@@ -4,11 +4,13 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
+	"time"
 
 	"log"
 
 	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/common"
 	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/mocks"
+	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/models"
 	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/rootmux"
 	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/tokens"
 )
@@ -37,6 +39,24 @@ func main() {
 		mockOAuth2Port := ":8090"
 		go http.ListenAndServe(mockOAuth2Port, mocks.CreateMockOAuth2())
 		fmt.Println("Mock OAuth2 server Listening on: http://localhost" + mockOAuth2Port)
+
+		// Call the function immediately when the server starts
+		models.FetchGRDFAuthAPI()
+
+		// then call GRDF auth api every two hours
+		ticker := time.NewTicker(time.Hour * 2)
+		quit := make(chan struct{})
+		go func() {
+			for {
+				select {
+				case <-ticker.C:
+					models.FetchGRDFAuthAPI()
+				case <-quit:
+					ticker.Stop()
+					return
+				}
+			}
+		}()
 	}
 
 	// Serve locally with https
-- 
GitLab