From 88422353726e9401c12c15e7e5b97a6ec7a6a292 Mon Sep 17 00:00:00 2001 From: efim Date: Mon, 9 Oct 2023 07:45:29 +0000 Subject: [PATCH] feat: middleware to return error as html --- auth-notes.org | 7 +- main.go | 1 + middleware/httpErrors.go | 54 ++++++++ pages/pageRoutes.go | 183 +++++++++++++++++----------- pages/templates/errors/401.gohtml | 10 ++ pages/templates/errors/404.gohtml | 9 ++ pages/templates/errors/error.gohtml | 10 ++ pages/templates/index.gohtml | 1 + 8 files changed, 202 insertions(+), 73 deletions(-) create mode 100644 middleware/httpErrors.go create mode 100644 pages/templates/errors/401.gohtml create mode 100644 pages/templates/errors/404.gohtml create mode 100644 pages/templates/errors/error.gohtml diff --git a/auth-notes.org b/auth-notes.org index fd5dd5c..1c22fac 100644 --- a/auth-notes.org +++ b/auth-notes.org @@ -242,11 +242,12 @@ https://stackoverflow.com/questions/62307431/firefox-sends-secure-cookies-to-loc see: except on localhost : https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie -** TODO prettying up server responses for "we show html" land +** DONE prettying up server responses for "we show html" land let's do this also, yes *** DONE logout should push root url in htmx -*** TODO lets make 404 page and return it -*** TODO lets make 401 page and return it +*** DONE lets make 404 page and return it +*** DONE lets make 401 page and return it +*** DONE and let's make NavInfo init common for reuse ** TODO get icons for the auth providers. surely they are accessible from the pocketbase itself? http://localhost:8090/_/images/oauth2/apple.svg yes. diff --git a/main.go b/main.go index a63b24f..ff64f67 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ func main() { app := pocketbase.New() middleware.AddCookieSessionMiddleware(app) + middleware.AddErrorsMiddleware(app) pages.AddPageRoutes(app) if err := app.Start(); err != nil { diff --git a/middleware/httpErrors.go b/middleware/httpErrors.go new file mode 100644 index 0000000..70c19e1 --- /dev/null +++ b/middleware/httpErrors.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "bytes" + "html/template" + "log" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" +) + +func AddErrorsMiddleware(app *pocketbase.PocketBase) { + app.OnBeforeApiError().Add(func(e *core.ApiErrorEvent) error { + log.Printf("in before api error with %+v with response %v and error %+v", e, e.HttpContext.Response(), e.Error) + // oh, i guess i could do redirect? + return renderErrorPage(e) + }) +} + +var redirectTemplate = ` + + + + + + +

Redirecting to error page

+ + +` +var tmpl = template.Must( template.New("redirect-to-error").Parse(redirectTemplate) ) + +func renderErrorPage(e *core.ApiErrorEvent) error { + errorMessage := e.Error.Error() + log.Printf("in error to html middleware for %s with status %+v", errorMessage, e) + + errorCode := 500 + switch errorMessage { + case "Not Found.": + // not authorized + errorCode = 404 + case "The request requires admin or record authorization token to be set.": + // not found + errorCode = 401 + } + + var instantiatedTemplate bytes.Buffer + if err := tmpl.Execute(&instantiatedTemplate, errorCode); err != nil { + // couldn't execute the template + return e.HttpContext.HTML(200, "Error 500") + } + + return e.HttpContext.HTML(200, instantiatedTemplate.String()) +} diff --git a/pages/pageRoutes.go b/pages/pageRoutes.go index ff1a90e..d592bb8 100644 --- a/pages/pageRoutes.go +++ b/pages/pageRoutes.go @@ -6,6 +6,7 @@ import ( "html/template" "math/rand" "net/http" + "strconv" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" @@ -21,7 +22,8 @@ var staticFilesFS embed.FS func AddPageRoutes(app *pocketbase.PocketBase) { app.OnBeforeServe().Add(getIndexPageRoute(app)) - app.OnBeforeServe().Add(somePageRoute) + app.OnBeforeServe().Add(getSomePageRoute(app)) + app.OnBeforeServe().Add(getErrorPageRoute(app)) app.OnBeforeServe().Add(func(e *core.ServeEvent) error { e.Router.StaticFS("/static", staticFilesFS) @@ -41,40 +43,13 @@ func getIndexPageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error return func(e *core.ServeEvent) error { e.Router.GET("/", func(c echo.Context) error { // first collect data - info := apis.RequestInfo(c) - admin := info.Admin // nil if not authenticated as admin - record := info.AuthRecord // nil if not authenticated as regular auth record - - isGuest := admin == nil && record == nil - - username := "" - switch { - case admin != nil: - username = admin.Email - case record != nil: - username = record.Username() - } - - oauthProviders := app.Settings().NamedAuthProviderConfigs() - oauthProviderNames := make([]string, 0, len(oauthProviders)) - for name, config := range oauthProviders { - if config.Enabled { - oauthProviderNames = append(oauthProviderNames, name) - } - } - + navInfoData := initNavInfoData(app, c) indexPageData := struct { - IsGuest, IsAdmin bool - Username string - EnabledOauthProviders []string - NavInfo navInfo + BackendMessage string + NavInfo navInfo }{ - IsAdmin: admin != nil, - NavInfo: navInfo{ - IsGuest: isGuest, - Username: username, - EnabledOauthProviders: oauthProviderNames, - }, + BackendMessage: "Hello from the backend!", + NavInfo: navInfoData, } // then render template with it @@ -101,44 +76,112 @@ func stringWithCharset(length int, charset string) string { } return string(b) } -func somePageRoute(e *core.ServeEvent) error { - e.Router.GET("/somepage", func(c echo.Context) error { - // get data - // and since i'm using 'base.gohtml' with Nav, i'll need Nav info +func getSomePageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error { + return func(e *core.ServeEvent) error { + e.Router.GET("/somepage", func(c echo.Context) error { + // get data + // and since i'm using 'base.gohtml' with Nav, i'll need Nav info + navInfoData := initNavInfoData(app, c) - info := apis.RequestInfo(c) - admin := info.Admin // nil if not authenticated as admin - record := info.AuthRecord // nil if not authenticated as regular auth record + somePageData := struct { + RandomNumber int + RandomString string + NavInfo navInfo + }{ + RandomNumber: rand.Int(), + RandomString: stringWithCharset(25, charset), + NavInfo: navInfoData, + } - username := "" - switch { - case admin != nil: - username = admin.Email - case record != nil: - username = record.Username() - } + // then render template with it + templateName := "templates/somepage.gohtml" + tmpl := template.Must(template.ParseFS(templatesFS, "templates/base.gohtml", templateName)) + var instantiatedTemplate bytes.Buffer + if err := tmpl.Execute(&instantiatedTemplate, somePageData); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"message": "error parsing template"}) + } - somePageData := struct { - RandomNumber int - RandomString string - NavInfo navInfo - }{ - RandomNumber: rand.Int(), - RandomString: stringWithCharset(25, charset), - NavInfo: navInfo{ - Username: username, - }, - } - - // then render template with it - templateName := "templates/somepage.gohtml" - tmpl := template.Must(template.ParseFS(templatesFS, "templates/base.gohtml", templateName)) - var instantiatedTemplate bytes.Buffer - if err := tmpl.Execute(&instantiatedTemplate, somePageData); err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"message": "error parsing template"}) - } - - return c.HTML(http.StatusOK, instantiatedTemplate.String()) - }, apis.RequireAdminOrRecordAuth()) - return nil + return c.HTML(http.StatusOK, instantiatedTemplate.String()) + }, apis.RequireAdminOrRecordAuth()) + return nil + } +} + +func getErrorPageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error { + return func(e *core.ServeEvent) error { + e.Router.GET("/error/:code", func(c echo.Context) error { + // get data + code := c.PathParam("code") + codeNum, err := strconv.ParseInt(code, 10, 64) + if err != nil { + codeNum = 500 + } + errorText := http.StatusText(int(codeNum)) + if errorText == "" { + codeNum = 500 + errorText = http.StatusText(500) + } + + // and since i'm using 'base.gohtml' with Nav, i'll need Nav info + navInfoData := initNavInfoData(app, c) + + somePageData := struct { + NavInfo navInfo + ErrorCode int64 + ErrorText string + }{ + NavInfo: navInfoData, + ErrorCode: codeNum, + ErrorText: errorText, + } + + // then render template with it + templateName := "templates/errors/error.gohtml" + switch codeNum { + case 404: + templateName = "templates/errors/404.gohtml" + case 401: + templateName = "templates/errors/401.gohtml" + } + tmpl := template.Must(template.ParseFS(templatesFS, "templates/base.gohtml", templateName)) + var instantiatedTemplate bytes.Buffer + if err := tmpl.Execute(&instantiatedTemplate, somePageData); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"message": "error parsing template"}) + } + + return c.HTML(int(codeNum), instantiatedTemplate.String()) + }) + return nil + } +} + +func initNavInfoData(app *pocketbase.PocketBase, c echo.Context) navInfo { + // first collect data + info := apis.RequestInfo(c) + admin := info.Admin // nil if not authenticated as admin + record := info.AuthRecord // nil if not authenticated as regular auth record + + isGuest := admin == nil && record == nil + + username := "" + switch { + case admin != nil: + username = admin.Email + case record != nil: + username = record.Username() + } + + oauthProviders := app.Settings().NamedAuthProviderConfigs() + oauthProviderNames := make([]string, 0, len(oauthProviders)) + for name, config := range oauthProviders { + if config.Enabled { + oauthProviderNames = append(oauthProviderNames, name) + } + } + + return navInfo{ + IsGuest: isGuest, + Username: username, + EnabledOauthProviders: oauthProviderNames, + } } diff --git a/pages/templates/errors/401.gohtml b/pages/templates/errors/401.gohtml new file mode 100644 index 0000000..1468db2 --- /dev/null +++ b/pages/templates/errors/401.gohtml @@ -0,0 +1,10 @@ +{{ define "title" }} + Not Authorized +{{ end }} + +{{ define "main" }} +
+ The page you are trying to access requires authorization. + Please log in. +
+{{ end }} diff --git a/pages/templates/errors/404.gohtml b/pages/templates/errors/404.gohtml new file mode 100644 index 0000000..d67304f --- /dev/null +++ b/pages/templates/errors/404.gohtml @@ -0,0 +1,9 @@ +{{ define "title" }} + Page Not Found +{{ end }} + +{{ define "main" }} +
+ Error 404 means the page was not found +
+{{ end }} diff --git a/pages/templates/errors/error.gohtml b/pages/templates/errors/error.gohtml new file mode 100644 index 0000000..756606b --- /dev/null +++ b/pages/templates/errors/error.gohtml @@ -0,0 +1,10 @@ +{{ define "title" }} + Error occurred +{{ end }} + +{{ define "main" }} +
+

Error {{ .ErrorCode }} occurred!

+

{{ .ErrorText }}

+
+{{ end }} diff --git a/pages/templates/index.gohtml b/pages/templates/index.gohtml index aab49e5..6f6f015 100644 --- a/pages/templates/index.gohtml +++ b/pages/templates/index.gohtml @@ -13,6 +13,7 @@ {{ else }} +

Rendering this on the backend, passing values from the code: {{ .BackendMessage }}

There will be some content only for authorized users

{{ end }}