From dccb0657b87670d79ee266cbfaf96f1376d2d373 Mon Sep 17 00:00:00 2001 From: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> Date: Wed, 29 May 2024 18:51:36 +0800 Subject: [PATCH 1/4] refactor: clean arch approach for router Signed-off-by: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> --- src/goapp/http/mux-router.go | 57 ++++++++++++++++++++++++++++++ src/goapp/http/router.go | 9 +++++ src/goapp/main.go | 56 +++-------------------------- src/goapp/middleware/middleware.go | 34 ++++++++++++++++++ src/goapp/pkg/session/session.go | 8 ++--- src/goapp/routes.go | 53 +++++++++++++++------------ 6 files changed, 136 insertions(+), 81 deletions(-) create mode 100644 src/goapp/http/mux-router.go create mode 100644 src/goapp/http/router.go create mode 100644 src/goapp/middleware/middleware.go diff --git a/src/goapp/http/mux-router.go b/src/goapp/http/mux-router.go new file mode 100644 index 0000000..f91ed0d --- /dev/null +++ b/src/goapp/http/mux-router.go @@ -0,0 +1,57 @@ +package router + +import ( + "fmt" + "net/http" + "os" + + rtPages "main/routes/pages" + + "github.com/gorilla/mux" + "github.com/unrolled/secure" +) + +type muxRouter struct{} + +func NewMuxRouter() Router { + return &muxRouter{} +} + +var ( + muxDispatcher = mux.NewRouter() +) + +func (*muxRouter) GET(uri string, f func(resp http.ResponseWriter, req *http.Request)) { + muxDispatcher.HandleFunc(uri, f).Methods("GET") +} + +func (*muxRouter) POST(uri string, f func(resp http.ResponseWriter, req *http.Request)) { + muxDispatcher.HandleFunc(uri, f).Methods("POST") +} + +func (*muxRouter) SERVE(port string) { + secureOptions := secure.Options{ + SSLRedirect: true, // Strict-Transport-Security + SSLHost: os.Getenv("SSL_HOST"), // Strict-Transport-Security + SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, // Strict-Transport-Security + FrameDeny: true, + ContentTypeNosniff: true, // X-Content-Type-Options + BrowserXssFilter: true, + ReferrerPolicy: "strict-origin", // Referrer-Policy + ContentSecurityPolicy: os.Getenv("CONTENT_SECURITY_POLICY"), + PermissionsPolicy: "fullscreen=(), geolocation=()", // Permissions-Policy + STSSeconds: 31536000, // Strict-Transport-Security + STSIncludeSubdomains: true, // Strict-Transport-Security + IsDevelopment: os.Getenv("IS_DEVELOPMENT") == "true", + } + + secureMiddleware := secure.New(secureOptions) + muxDispatcher.Use(secureMiddleware.Handler) + http.Handle("/", muxDispatcher) + + muxDispatcher.NotFoundHandler = http.HandlerFunc(rtPages.NotFoundHandler) + muxDispatcher.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir("./public/")))) + + fmt.Printf("Mux HTTP server running on port %v", port) + http.ListenAndServe(fmt.Sprintf(":%v", port), muxDispatcher) +} diff --git a/src/goapp/http/router.go b/src/goapp/http/router.go new file mode 100644 index 0000000..ec37ee3 --- /dev/null +++ b/src/goapp/http/router.go @@ -0,0 +1,9 @@ +package router + +import "net/http" + +type Router interface { + GET(uri string, f func(resp http.ResponseWriter, req *http.Request)) + POST(uri string, f func(resp http.ResponseWriter, req *http.Request)) + SERVE(port string) +} diff --git a/src/goapp/main.go b/src/goapp/main.go index a8445ad..7076a31 100644 --- a/src/goapp/main.go +++ b/src/goapp/main.go @@ -1,22 +1,14 @@ package main import ( - "fmt" "log" session "main/pkg/session" - rtPages "main/routes/pages" rtApprovals "main/routes/pages/approvals" - "net/http" - "os" "strconv" "time" - "github.com/gorilla/mux" - "github.com/unrolled/secure" - ev "main/pkg/envvar" - "github.com/codegangsta/negroni" "github.com/joho/godotenv" ) @@ -27,56 +19,16 @@ func main() { log.Print(err.Error()) } - secureMiddleware := secure.New(secure.Options{ - SSLRedirect: true, // Strict-Transport-Security - SSLHost: os.Getenv("SSL_HOST"), // Strict-Transport-Security - SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, // Strict-Transport-Security - FrameDeny: true, // X-FRAME-OPTIONS - ContentTypeNosniff: true, // X-Content-Type-Options - BrowserXssFilter: true, - ReferrerPolicy: "strict-origin", // Referrer-Policy - ContentSecurityPolicy: os.Getenv("CONTENT_SECURITY_POLICY"), - PermissionsPolicy: "fullscreen=(), geolocation=()", // Permissions-Policy - STSSeconds: 31536000, // Strict-Transport-Security - STSIncludeSubdomains: true, // Strict-Transport-Security, - IsDevelopment: os.Getenv("IS_DEVELOPMENT") == "true", - }) - // Create session and GitHubClient session.InitializeSession() - mux := mux.NewRouter() - - setPageRoutes(mux) - setApiRoutes(mux) - setUtilityRoutes(mux) - - mux.NotFoundHandler = loadAzAuthPage(rtPages.NotFoundHandler) - go checkFailedCallbacks() - mux.Use(secureMiddleware.Handler) - http.Handle("/", mux) - - port := ev.GetEnvVar("PORT", "8080") - fmt.Printf("Now listening on port %v\n", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), mux)) - -} - -// Verifies authentication before loading the page. -func loadAzAuthPage(f func(w http.ResponseWriter, r *http.Request)) *negroni.Negroni { - return negroni.New( - negroni.HandlerFunc(session.IsAuthenticated), - negroni.Wrap(http.HandlerFunc(f)), - ) -} + setPageRoutes() + setApiRoutes() + setUtilityRoutes() -func loadGuidAuthApi(f func(w http.ResponseWriter, r *http.Request)) *negroni.Negroni { - return negroni.New( - negroni.HandlerFunc(session.IsGuidAuthenticated), - negroni.Wrap(http.HandlerFunc(f)), - ) + serve() } func checkFailedCallbacks() { diff --git a/src/goapp/middleware/middleware.go b/src/goapp/middleware/middleware.go new file mode 100644 index 0000000..3b81fca --- /dev/null +++ b/src/goapp/middleware/middleware.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "main/pkg/session" + "net/http" +) + +type Middleware func(http.HandlerFunc) http.HandlerFunc + +func AzureAuth() Middleware { + return func(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session.IsAuthenticated(w, r) + f(w, r) + } + } +} + +func ManagedIdentityAuth() Middleware { + return func(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session.IsGuidAuthenticated(w, r) + f(w, r) + } + } +} + +// Chain applies middlewares to a http.HandlerFunc +func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc { + for _, m := range middlewares { + f = m(f) + } + return f +} diff --git a/src/goapp/pkg/session/session.go b/src/goapp/pkg/session/session.go index f2b0d1b..fce90e1 100644 --- a/src/goapp/pkg/session/session.go +++ b/src/goapp/pkg/session/session.go @@ -29,7 +29,7 @@ func InitializeSession() { gob.Register(map[string]interface{}{}) } -func IsAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { +func IsAuthenticated(w http.ResponseWriter, r *http.Request) { var url string url = fmt.Sprintf("%v", r.URL) if strings.HasPrefix(url, "/response/") { @@ -40,7 +40,6 @@ func IsAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFu authReq = IsAuthRequired(appModuleGuid) if authReq == false { - next(w, r) return } } @@ -109,11 +108,10 @@ func IsAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFu } } } - next(w, r) } } -func IsGuidAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { +func IsGuidAuthenticated(w http.ResponseWriter, r *http.Request) { // Check header if authenticated _, err := auth.VerifyAccessToken(r) // RETURN ERROR @@ -121,8 +119,6 @@ func IsGuidAuthenticated(w http.ResponseWriter, r *http.Request, next http.Handl http.Error(w, err.Error(), http.StatusInternalServerError) return } - // RETURN SUCCESS - next(w, r) } func GetState(w http.ResponseWriter, r *http.Request) (string, error) { diff --git a/src/goapp/routes.go b/src/goapp/routes.go index 5987d93..de06e83 100644 --- a/src/goapp/routes.go +++ b/src/goapp/routes.go @@ -1,38 +1,45 @@ package main import ( + router "main/http" + m "main/middleware" + ev "main/pkg/envvar" rtApi "main/routes/apis" rtAzure "main/routes/login/azure" rtPages "main/routes/pages" rtApprovals "main/routes/pages/approvals" - "net/http" +) - "github.com/gorilla/mux" +var ( + httpRouter router.Router = router.NewMuxRouter() ) -func setPageRoutes(mux *mux.Router) { - mux.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir("./public/")))) - mux.Handle("/", loadAzAuthPage(rtApprovals.MyRequestsHandler)) - mux.Handle("/myapprovals", loadAzAuthPage(rtApprovals.MyApprovalsHandler)) - mux.Handle("/response/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}", loadAzAuthPage(rtApprovals.ResponseHandler)) - mux.Handle("/responsereassigned/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}/{ApproveText}/{RejectText}", loadAzAuthPage(rtApprovals.ResponseReassignedeHandler)) - - mux.HandleFunc("/loginredirect", rtPages.LoginRedirectHandler) - mux.HandleFunc("/login/azure", rtAzure.LoginHandler) - mux.HandleFunc("/login/azure/callback", rtAzure.CallbackHandler) - mux.HandleFunc("/logout/azure", rtAzure.LogoutHandler) +func setPageRoutes() { + httpRouter.GET("/", m.Chain(rtApprovals.MyRequestsHandler, m.AzureAuth())) + httpRouter.GET("/myapprovals", m.Chain(rtApprovals.MyApprovalsHandler, m.AzureAuth())) + httpRouter.GET("/response/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}", m.Chain(rtApprovals.ResponseHandler, m.AzureAuth())) + httpRouter.GET("/responsereassigned/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}/{ApproveText}/{RejectText}", m.Chain(rtApprovals.ResponseReassignedeHandler, m.AzureAuth())) + + httpRouter.GET("/loginredirect", rtPages.LoginRedirectHandler) + httpRouter.GET("/login/azure", rtAzure.LoginHandler) + httpRouter.GET("/login/azure/callback", rtAzure.CallbackHandler) + httpRouter.GET("/logout/azure", rtAzure.LogoutHandler) +} + +func setApiRoutes() { + + httpRouter.POST("/api/request", rtApprovals.ApprovalRequestHandler) + httpRouter.POST("/api/process", rtApprovals.ProcessResponseHandler) + httpRouter.GET("/api/items/type/{type:[0-2]+}/status/{status:[0-3]+}", m.Chain(rtApi.GetItems, m.AzureAuth())) + httpRouter.GET("/api/search/users/{search}", m.Chain(rtApi.SearchUserFromActiveDirectory, m.AzureAuth())) + httpRouter.GET("/api/responsereassignedapi/{itemGuid}/{approver}/{ApplicationId}/{ApplicationModuleId}/{itemId}/{ApproveText}/{RejectText}", m.Chain(rtApprovals.ReAssignApproverHandler, m.AzureAuth())) } -func setApiRoutes(mux *mux.Router) { - muxApi := mux.PathPrefix("/api").Subrouter() - muxApi.HandleFunc("/request", rtApprovals.ApprovalRequestHandler) - muxApi.HandleFunc("/process", rtApprovals.ProcessResponseHandler) - muxApi.Handle("/items/type/{type:[0-2]+}/status/{status:[0-3]+}", loadAzAuthPage(rtApi.GetItems)) - muxApi.Handle("/search/users/{search}", loadAzAuthPage(rtApi.SearchUserFromActiveDirectory)) - muxApi.Handle("/responsereassignedapi/{itemGuid}/{approver}/{ApplicationId}/{ApplicationModuleId}/{itemId}/{ApproveText}/{RejectText}", loadAzAuthPage(rtApprovals.ReAssignApproverHandler)) +func setUtilityRoutes() { + httpRouter.GET("/utility/fillout-approvalrequest-approvers", m.Chain(rtApi.FillOutApprovalRequestApprovers, m.ManagedIdentityAuth())) } -func setUtilityRoutes(mux *mux.Router) { - muxUtility := mux.PathPrefix("/utility").Subrouter() - muxUtility.Handle("/fillout-approvalrequest-approvers", loadGuidAuthApi(rtApi.FillOutApprovalRequestApprovers)).Methods("GET") +func serve() { + port := ev.GetEnvVar("PORT", "8080") + httpRouter.SERVE(port) } From 7256c96293e4a3ec942bdd87d51ad8646e9ba906 Mon Sep 17 00:00:00 2001 From: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> Date: Wed, 29 May 2024 19:26:31 +0800 Subject: [PATCH 2/4] fix on error Signed-off-by: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> --- src/goapp/middleware/middleware.go | 6 ++++-- src/goapp/pkg/session/session.go | 12 +++++++----- src/goapp/routes.go | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/goapp/middleware/middleware.go b/src/goapp/middleware/middleware.go index 3b81fca..c6f8724 100644 --- a/src/goapp/middleware/middleware.go +++ b/src/goapp/middleware/middleware.go @@ -10,8 +10,10 @@ type Middleware func(http.HandlerFunc) http.HandlerFunc func AzureAuth() Middleware { return func(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - session.IsAuthenticated(w, r) - f(w, r) + isAuth := session.IsAuthenticated(w, r) + if isAuth { + f(w, r) + } } } } diff --git a/src/goapp/pkg/session/session.go b/src/goapp/pkg/session/session.go index fce90e1..cfd6087 100644 --- a/src/goapp/pkg/session/session.go +++ b/src/goapp/pkg/session/session.go @@ -29,7 +29,7 @@ func InitializeSession() { gob.Register(map[string]interface{}{}) } -func IsAuthenticated(w http.ResponseWriter, r *http.Request) { +func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool { var url string url = fmt.Sprintf("%v", r.URL) if strings.HasPrefix(url, "/response/") { @@ -40,7 +40,7 @@ func IsAuthenticated(w http.ResponseWriter, r *http.Request) { authReq = IsAuthRequired(appModuleGuid) if authReq == false { - return + return true } } @@ -53,13 +53,14 @@ func IsAuthenticated(w http.ResponseWriter, r *http.Request) { MaxAge: -1} http.SetCookie(w, &c) http.Redirect(w, r, url, http.StatusTemporaryRedirect) - return + return false } // fmt.Println(session) if _, ok := session.Values["profile"]; !ok { // Asks user to login if there is no saved user profile http.Redirect(w, r, url, http.StatusTemporaryRedirect) + return false } else { // If there is a user profile saved @@ -67,7 +68,7 @@ func IsAuthenticated(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return false } // Retrieve current token data @@ -104,10 +105,11 @@ func IsAuthenticated(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return false } } } + return true } } diff --git a/src/goapp/routes.go b/src/goapp/routes.go index de06e83..c781356 100644 --- a/src/goapp/routes.go +++ b/src/goapp/routes.go @@ -40,6 +40,6 @@ func setUtilityRoutes() { } func serve() { - port := ev.GetEnvVar("PORT", "8080") + port := ev.GetEnvVar("PORT", "8081") httpRouter.SERVE(port) } From 87688af37e5c13f0c9b099f5e8cab215adf43911 Mon Sep 17 00:00:00 2001 From: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> Date: Wed, 29 May 2024 19:38:55 +0800 Subject: [PATCH 3/4] fix: update port Signed-off-by: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> --- src/goapp/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goapp/routes.go b/src/goapp/routes.go index c781356..de06e83 100644 --- a/src/goapp/routes.go +++ b/src/goapp/routes.go @@ -40,6 +40,6 @@ func setUtilityRoutes() { } func serve() { - port := ev.GetEnvVar("PORT", "8081") + port := ev.GetEnvVar("PORT", "8080") httpRouter.SERVE(port) } From 82c4576590794e5bb010b4558d8adc3192b631e2 Mon Sep 17 00:00:00 2001 From: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:57:41 +0800 Subject: [PATCH 4/4] refactor: move links to config Signed-off-by: Ismael Ibuan <102030576+iibuan@users.noreply.github.com> --- .bicep/webapp/parameters.json | 4 +++- src/goapp/.env.template | 4 +++- src/goapp/models/data.go | 13 ++++++++--- src/goapp/pkg/template/template.go | 23 ++++++++++++++----- src/goapp/templates/master.html | 36 ++++++------------------------ 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/.bicep/webapp/parameters.json b/.bicep/webapp/parameters.json index f9b36b0..2efd726 100644 --- a/.bicep/webapp/parameters.json +++ b/.bicep/webapp/parameters.json @@ -38,7 +38,9 @@ "EMAIL_TENANT_ID" : "", "EMAIL_CLIENT_ID" : "", "EMAIL_CLIENT_SECRET" : "", - "EMAIL_USER_ID" : "" + "EMAIL_USER_ID" : "", + "LINK_FOOTERS": "", + "ORGANIZATION_NAME": "" } } } diff --git a/src/goapp/.env.template b/src/goapp/.env.template index 52d0d34..dcf136e 100644 --- a/src/goapp/.env.template +++ b/src/goapp/.env.template @@ -14,4 +14,6 @@ EMAIL_TENANT_ID= EMAIL_CLIENT_ID= EMAIL_CLIENT_SECRET= EMAIL_ENABLED= -EMAIL_USER_ID= \ No newline at end of file +EMAIL_USER_ID= +LINK_FOOTERS="" +ORGANIZATION_NAME="" \ No newline at end of file diff --git a/src/goapp/models/data.go b/src/goapp/models/data.go index 5cbea50..3dab167 100644 --- a/src/goapp/models/data.go +++ b/src/goapp/models/data.go @@ -1,9 +1,16 @@ package models type TypPageData struct { - Header interface{} - Profile interface{} - Content interface{} + Header interface{} + Profile interface{} + Content interface{} + Footers []Footer + OrganizationName string +} + +type Footer struct { + Text string + Url string } type TypHeaders struct { diff --git a/src/goapp/pkg/template/template.go b/src/goapp/pkg/template/template.go index 834fc63..08dfeb7 100644 --- a/src/goapp/pkg/template/template.go +++ b/src/goapp/pkg/template/template.go @@ -6,6 +6,7 @@ import ( "main/models" session "main/pkg/session" "net/http" + "os" "strings" ) @@ -21,8 +22,8 @@ func UseTemplate(w *http.ResponseWriter, r *http.Request, page string, pageData if profile == nil { profile = map[string]interface{}{ - "name": "", - "preferred_username":"", + "name": "", + "preferred_username": "", } } @@ -32,10 +33,22 @@ func UseTemplate(w *http.ResponseWriter, r *http.Request, page string, pageData menu = append(menu, models.TypMenu{Name: "My Approvals", Url: "/myapprovals", IconPath: "/public/icons/approvals.svg"}) masterPageData := models.TypHeaders{Menu: menu, Page: getUrlPath(r.URL.Path)} + //Footers + var footers []models.Footer + footerString := os.Getenv("LINK_FOOTERS") + res := strings.Split(footerString, ";") + for _, footer := range res { + f := strings.Split(footer, ">") + footers = append(footers, models.Footer{Text: f[0], Url: f[1]}) + } + data := models.TypPageData{ - Header: masterPageData, - Profile: profile, - Content: pageData} + Header: masterPageData, + Profile: profile, + Content: pageData, + Footers: footers, + OrganizationName: os.Getenv("ORGANIZATION_NAME"), + } tmpl := template.Must( template.ParseFiles("templates/master.html", "templates/buttons.html", diff --git a/src/goapp/templates/master.html b/src/goapp/templates/master.html index efee648..4b56d75 100644 --- a/src/goapp/templates/master.html +++ b/src/goapp/templates/master.html @@ -117,7 +117,7 @@ Avanade

- © 2022 Avanade, Inc. All rights reserved. + © 2022 {{ .OrganizationName }}, Inc. All rights reserved.