From 1a7961ec6cad59a6d59d63715d25cf34c7538f7c Mon Sep 17 00:00:00 2001 From: Bastien DUMONT <bdumont@grandlyon.com> Date: Tue, 27 Sep 2022 08:03:22 +0000 Subject: [PATCH] feat: custom alert --- .env.template | 24 +++++++++ .vscode/launch.json | 8 +-- README.md | 16 ++++-- docker-compose.yml | 2 +- internal/models/customPopup.go | 83 ++++++++++++++++++++++++++++++++ internal/models/models.go | 9 ++++ internal/rootmux/rootmux.go | 2 + internal/rootmux/rootmux_test.go | 32 +++++++----- template.env | 20 -------- 9 files changed, 155 insertions(+), 41 deletions(-) create mode 100644 .env.template create mode 100644 internal/models/customPopup.go delete mode 100644 template.env diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..3857e75 --- /dev/null +++ b/.env.template @@ -0,0 +1,24 @@ +# Common settings +HOSTNAME=localhost +ADMIN_ROLE +DEBUG_MODE +MOCK_OAUTH2 +HTTPS_PORT +IMAGE_FOLDER + +# Needed to user OAuth2 authentication : +REDIRECT_URL +CLIENT_ID +CLIENT_SECRET +AUTH_URL +TOKEN_URL +USERINFO_URL + +# Access to the database +DATABASE_USER +DATABASE_PASSWORD +DATABASE_NAME + +SGE_API_TOKEN +MEILI_MASTER_KEY +MEILI_HOST \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index da3a8c4..785de51 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,19 +12,19 @@ "program": "${workspaceFolder}/main.go", "env": { "HOSTNAME": "localhost", - "ADMIN_ROLE" : "ADMINS", + "ADMIN_ROLE": "ADMINS", "DEBUG_MODE": "true", "MOCK_OAUTH2": "true", "HTTPS_PORT": "1443", "IMAGE_FOLDER": "image-lib", - "REDIRECT_URL": "http://localhost:1443/OAuth2Callback", + "REDIRECT_URL": "https://localhost:1443/OAuth2Callback", "CLIENT_ID": "foo", "CLIENT_SECRET": "bar", "AUTH_URL": "http://localhost:8090/auth", "TOKEN_URL": "http://localhost:8090/token", "USERINFO_URL": "http://localhost:8090/admininfo", - + "DATABASE_USER": "root", "DATABASE_PASSWORD": "password", "DATABASE_NAME": "backoffice", @@ -58,4 +58,4 @@ "showLog": true } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 7b6ca2b..e530d50 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ This repository contains the backend part of this backoffice. # How to setup : -This backend should be deployed with the frontend from [this repo](https://forge.grandlyon.com/web-et-numerique/llle_project/backoffice-client) +This backend should be deployed with the frontend from [this repo](https://forge.grandlyon.com/web-et-numerique/llle_project/backoffice-client) However this backend can be run in standalone : - Clone the repository -- Set a .env file at the root and add all variables declared in the [template.env](https://forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/-/blob/dev/template.env) file +- Set a .env file at the root and add all variables declared in the [template.env](https://forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/-/blob/dev/.env.template) - (**Optionnal**) you can create new certificates by running `cd dev_certificates && ./generate-certificates.sh` @@ -30,7 +30,8 @@ Once deployed, you can access to a Swagger documentation of the API on https://$ ## Launch locally -To launch it locally : +To launch it locally : + - Run `docker-compose up -d` In local you can access phpmyadmin interface to manage the database : http://localhost:8008 @@ -39,11 +40,16 @@ In local you can access phpmyadmin interface to manage the database : http://loc To launch the backend for development goal: -- edit *main.go* file, uncomment the last line "log.Fatal(http.ListenAndServe(":"+strconv.Itoa(httpsPort), rootMux.Router))" and comment the line above "log.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(httpsPort), "./dev_certificates/localhost.crt", "./dev_certificates/localhost.key", rootMux.Router))" +- edit _main.go_ file, uncomment the last line "log.Fatal(http.ListenAndServe(":"+strconv.Itoa(httpsPort), rootMux.Router))" and comment the line above "log.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(httpsPort), "./dev_certificates/localhost.crt", "./dev_certificates/localhost.key", rootMux.Router))" - This way you disable https so make sure your requests goes on http adresses -- in *vscode/launch.json* set "REDIRECT_URL" to "http://localhost:1443/OAuth2Callback", +- in _vscode/launch.json_ set "REDIRECT_URL" to "http://localhost:1443/OAuth2Callback", - also comment the port and host values - if you comment the DATABASE_USER line, it will launches with a sqlite database instead of mysql - launch the app by pressing F5 on vscode, you will see the logs on the debug console. - this way you won't have the login every time you relaunch the backend for development +## Build image for local + +``` +docker build . -t backoffice-server +``` diff --git a/docker-compose.yml b/docker-compose.yml index 1f0cb5c..3242f25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,7 @@ services: PMA_HOST: database backoffice-container: - image: backoffice + image: backoffice depends_on: database: condition: service_healthy diff --git a/internal/models/customPopup.go b/internal/models/customPopup.go new file mode 100644 index 0000000..db4aa73 --- /dev/null +++ b/internal/models/customPopup.go @@ -0,0 +1,83 @@ +package models + +import ( + "encoding/json" + "errors" + "log" + "net/http" + + "gorm.io/gorm" +) + +type CustomPopup struct { + ID uint `gorm:"<-:create"` + PopupEnabled bool `json:"popupEnabled"` + Title string `json:"title"` + Description string `json:"description"` +} + +// GetCustomPopup godoc +// @Summary Give status of custom popup +// @Description Give status of custom poup +// @Tags customPopup +// @Produce json +// @Success 200 {object} CustomPopup +// @Failure 404 {string} string "Not found" +// @Router /api/common/customPopup [get] +func (dh *DataHandler) GetCustomPopup(w http.ResponseWriter, r *http.Request) { + var popupInfo CustomPopup + err := dh.sqlClient.First(&popupInfo).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + http.Error(w, "custom popup status not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(popupInfo) + log.Printf("| get customPopup | %v", r.RemoteAddr) +} + +// SaveCustomPopup godoc +// @Summary Update custom popup content +// @Description Update custom popup content +// @Tags customPopup +// @Accept json +// @Produce json +// @Success 200 {object} CustomPopup "Updated successfully" +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal server error" +// @Param customPopup body CustomPopup true "CustomPopup to create/update with new content" +// @Router /api/admin/customPopup [put] +func (dh *DataHandler) SaveCustomPopup(w http.ResponseWriter, r *http.Request) { + if r.Body == http.NoBody { + http.Error(w, "request body is empty", http.StatusBadRequest) + return + } + + decoder := json.NewDecoder(r.Body) + var customPopup CustomPopup + err := decoder.Decode(&customPopup) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + var updatedCustomPopup CustomPopup + + err = dh.sqlClient.First(&updatedCustomPopup).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + updatedCustomPopup.PopupEnabled = customPopup.PopupEnabled + updatedCustomPopup.Title = customPopup.Title + updatedCustomPopup.Description = customPopup.Description + + dh.sqlClient.Save(&updatedCustomPopup) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(customPopup) + log.Printf("| updated customPopup | %v", r.RemoteAddr) +} diff --git a/internal/models/models.go b/internal/models/models.go index 607f8f8..35d828b 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -46,9 +46,11 @@ func NewDataHandler() *DataHandler { sqlClient.AutoMigrate(&MonthlyNews{}) sqlClient.AutoMigrate(&Poll{}) sqlClient.AutoMigrate(&PartnersInfo{}) + sqlClient.AutoMigrate(&CustomPopup{}) sqlClient.AutoMigrate(&Price{}) sqlClient.AutoMigrate(&Consent{}) + //TODO fix recreate on server init // Create default partner status sqlClient.Create(&PartnersInfo{ GRDFFailure: false, @@ -57,6 +59,13 @@ func NewDataHandler() *DataHandler { NotificationActivated: false, }) + // Create default custom popup + sqlClient.Create(&CustomPopup{ + PopupEnabled: false, + Title: "", + Description: "", + }) + // Meilisearch setup meiliClient := meilisearch.NewClient(meilisearch.ClientConfig{ Host: common.StringValueFromEnv("MEILI_HOST", ""), diff --git a/internal/rootmux/rootmux.go b/internal/rootmux/rootmux.go index a3d236f..a7907c0 100644 --- a/internal/rootmux/rootmux.go +++ b/internal/rootmux/rootmux.go @@ -43,6 +43,7 @@ func CreateRootMux() RootMux { r.HandleFunc("/api/common/monthlyReport", dh.GetMonthlyReport).Methods(http.MethodGet) r.HandleFunc("/api/common/monthlyReport/{year}/{month}", dh.GetMonthlyReport).Methods(http.MethodGet) r.HandleFunc("/api/common/partnersInfo", dh.GetPartnersInfo).Methods(http.MethodGet) + r.HandleFunc("/api/common/customPopup", dh.GetCustomPopup).Methods(http.MethodGet) r.HandleFunc("/api/common/prices", dh.GetAllPrices).Methods(http.MethodGet) r.HandleFunc("/api/common/prices/{fluidtype}", dh.GetPricesByFluid).Methods(http.MethodGet) @@ -69,6 +70,7 @@ func CreateRootMux() RootMux { apiAdmin.HandleFunc("/poll/{year}/{month}", dh.DeletePoll).Methods(http.MethodDelete) apiAdmin.HandleFunc("/partnersInfo", dh.SavePartnersInfo).Methods(http.MethodPut) + apiAdmin.HandleFunc("/customPopup", dh.SaveCustomPopup).Methods(http.MethodPut) apiAdmin.HandleFunc("/imageNames", file.GetEcogestureImages).Methods(http.MethodGet) diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go index f22c2cf..a215fc1 100644 --- a/internal/rootmux/rootmux_test.go +++ b/internal/rootmux/rootmux_test.go @@ -29,6 +29,8 @@ var ( newPollStr string partnersInfo = models.PartnersInfo{ID: 1, GRDFFailure: false, EnedisFailure: false, EGLFailure: true, NotificationActivated: true} partnersInfoStr string + customPopupInfo = models.CustomPopup{ID: 1, PopupEnabled: false, Title: "Alerte personnalisée", Description: "Ecolyo 4ever"} + customPopupStr string consent = models.Consent{Firstname: "Foo", Lastname: "Bar", PointID: 123456} consentStr string noH map[string]string @@ -65,6 +67,8 @@ func TestMain(m *testing.M) { monthlyInfoStr = string(monthlyInfoBytes) partnersInfoBytes, _ := json.Marshal(partnersInfo) partnersInfoStr = string(partnersInfoBytes) + customPopupBytes, _ := json.Marshal(customPopupInfo) + customPopupStr = string(customPopupBytes) newPollBytes, _ := json.Marshal(newPoll) newPollStr = string(newPollBytes) consentBytes, _ := json.Marshal(consent) @@ -90,9 +94,11 @@ func TestAll(t *testing.T) { sgeTests(t) } -/** +/* +* SECURITY TESTS (this tests are to check that the security protections works) -**/ +* +*/ func oauth2Tests(t *testing.T) { // Create the tester ts, do, _ := createTester(t) @@ -101,9 +107,11 @@ func oauth2Tests(t *testing.T) { do("GET", "/OAuth2Login", noH, "", http.StatusInternalServerError, "invalid oauth state") } -/** +/* +* UNLOGGED USER TESTS (this tests are to check that the security protections works) -**/ +* +*/ func unloggedTests(t *testing.T) { // Create the tester ts, do, _ := createTester(t) @@ -119,9 +127,11 @@ func unloggedTests(t *testing.T) { do("GET", "/api/common/partnersInfo", noH, "", http.StatusOK, `{"ID":1,"grdf_failure":false,"enedis_failure":false,"egl_failure":false,"notification_activated":false}`) } -/** +/* +* ADMIN TESTS (this tests are to check that an administrator can edit a newsletter's content) -**/ +* +*/ func adminTests(t *testing.T) { // Create the tester ts, do, _ := createTester(t) @@ -179,13 +189,13 @@ func adminTests(t *testing.T) { // Try to update the partnersInfo (must pass) do("PUT", "/api/admin/partnersInfo", xsrfHeader, partnersInfoStr, http.StatusOK, partnersInfoStr) - // Try to get the monthlyInfo created (must pass) + // Try to get the partnersInfo created (must pass) do("GET", "/api/common/partnersInfo", xsrfHeader, "", http.StatusOK, partnersInfoStr) - // Try to update the partnersInfo (must pass) - do("PUT", "/api/admin/partnersInfo", xsrfHeader, partnersInfoStr, http.StatusOK, partnersInfoStr) - // Try to get the monthlyInfo created (must pass) - do("GET", "/api/common/partnersInfo", xsrfHeader, "", http.StatusOK, partnersInfoStr) + // Try to update the customPopup (must pass) + do("PUT", "/api/admin/customPopup", xsrfHeader, customPopupStr, http.StatusOK, customPopupStr) + // Try to get the partnersInfo created (must pass) + do("GET", "/api/common/customPopup", xsrfHeader, "", http.StatusOK, customPopupStr) // Try to delete the monthlyNews created (must pass) do("DELETE", "/api/admin/monthlyNews/2021/1", xsrfHeader, "", http.StatusOK, "successful delete") diff --git a/template.env b/template.env deleted file mode 100644 index 46c5957..0000000 --- a/template.env +++ /dev/null @@ -1,20 +0,0 @@ -# Common settings -HOSTNAME= -ADMIN_ROLE= -DEBUG_MODE= -MOCK_OAUTH2= -HTTPS_PORT= -IMAGE_FOLDER= - -# Needed to user OAuth2 authentication : -REDIRECT_URL= -CLIENT_ID= -CLIENT_SECRET= -AUTH_URL= -TOKEN_URL= -USERINFO_URL= - -# Access to the database -DATABASE_USER= -DATABASE_PASSWORD= -DATABASE_NAME= \ No newline at end of file -- GitLab