From d2f6fe1ab857ab813fa4d27484bb896314c00a88 Mon Sep 17 00:00:00 2001 From: Thomas Miceli Date: Mon, 16 Dec 2024 01:09:32 +0100 Subject: [PATCH] wip --- internal/web/handler/auth.go | 40 +++--- internal/web/handler/git_http.go | 2 +- internal/web/handler/healthcheck.go | 6 +- internal/web/handler/settings.go | 16 +-- internal/web/server/middlewares.go | 29 ++-- internal/web/server/router.go | 212 ++++++++++++++++++++++++++++ internal/web/server/server.go | 137 +----------------- internal/web/server/util.go | 29 ++-- 8 files changed, 283 insertions(+), 188 deletions(-) diff --git a/internal/web/handler/auth.go b/internal/web/handler/auth.go index 735b687..499d8a2 100644 --- a/internal/web/handler/auth.go +++ b/internal/web/handler/auth.go @@ -37,7 +37,7 @@ const ( OpenIDConnect = "openid-connect" ) -func register(ctx *context.OGContext) error { +func Register(ctx *context.OGContext) error { disableSignup := ctx.GetData("DisableSignup") disableForm := ctx.GetData("DisableLoginForm") @@ -58,7 +58,7 @@ func register(ctx *context.OGContext) error { return ctx.HTML_("auth_form.html") } -func processRegister(ctx *context.OGContext) error { +func ProcessRegister(ctx *context.OGContext) error { disableSignup := ctx.GetData("DisableSignup") code := ctx.QueryParam("code") @@ -127,7 +127,7 @@ func processRegister(ctx *context.OGContext) error { return ctx.RedirectTo("/") } -func login(ctx *context.OGContext) error { +func Login(ctx *context.OGContext) error { ctx.SetData("title", ctx.TrH("auth.login")) ctx.SetData("htmlTitle", ctx.TrH("auth.login")) ctx.SetData("disableForm", ctx.GetData("DisableLoginForm")) @@ -135,7 +135,7 @@ func login(ctx *context.OGContext) error { return ctx.HTML_("auth_form.html") } -func processLogin(ctx *context.OGContext) error { +func ProcessLogin(ctx *context.OGContext) error { if ctx.GetData("DisableLoginForm") == true { return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil) } @@ -189,7 +189,7 @@ func processLogin(ctx *context.OGContext) error { return ctx.RedirectTo("/") } -func mfa(ctx *context.OGContext) error { +func Mfa(ctx *context.OGContext) error { var err error user := db.User{ID: ctx.GetSession().Values["mfaID"].(uint)} @@ -205,7 +205,7 @@ func mfa(ctx *context.OGContext) error { return ctx.HTML_("mfa.html") } -func oauthCallback(ctx *context.OGContext) error { +func OauthCallback(ctx *context.OGContext) error { user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request()) if err != nil { return ctx.ErrorRes(400, ctx.Tr("error.complete-oauth-login", err.Error()), err) @@ -311,7 +311,7 @@ func oauthCallback(ctx *context.OGContext) error { return ctx.RedirectTo("/") } -func oauth(ctx *context.OGContext) error { +func Oauth(ctx *context.OGContext) error { provider := ctx.Param("provider") httpProtocol := "http" @@ -401,7 +401,7 @@ func oauth(ctx *context.OGContext) error { return nil } -func oauthUnlink(ctx *context.OGContext) error { +func OauthUnlink(ctx *context.OGContext) error { provider := ctx.Param("provider") currUser := ctx.User @@ -425,7 +425,7 @@ func oauthUnlink(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func beginWebAuthnBinding(ctx *context.OGContext) error { +func BeginWebAuthnBinding(ctx *context.OGContext) error { credsCreation, jsonWaSession, err := webauthn.BeginBinding(ctx.User) if err != nil { return ctx.ErrorRes(500, "Cannot begin WebAuthn registration", err) @@ -439,7 +439,7 @@ func beginWebAuthnBinding(ctx *context.OGContext) error { return ctx.JSON(200, credsCreation) } -func finishWebAuthnBinding(ctx *context.OGContext) error { +func FinishWebAuthnBinding(ctx *context.OGContext) error { sess := ctx.GetSession() jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte) if !ok { @@ -483,7 +483,7 @@ func finishWebAuthnBinding(ctx *context.OGContext) error { return ctx.JSON_([]string{"OK"}) } -func beginWebAuthnLogin(ctx *context.OGContext) error { +func BeginWebAuthnLogin(ctx *context.OGContext) error { credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin() if err != nil { return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err) @@ -497,7 +497,7 @@ func beginWebAuthnLogin(ctx *context.OGContext) error { return ctx.JSON_(credsCreation) } -func finishWebAuthnLogin(ctx *context.OGContext) error { +func FinishWebAuthnLogin(ctx *context.OGContext) error { sess := ctx.GetSession() sessionData, ok := sess.Values["webauthn_login_session"].([]byte) if !ok { @@ -518,7 +518,7 @@ func finishWebAuthnLogin(ctx *context.OGContext) error { return ctx.JSON_([]string{"OK"}) } -func beginWebAuthnAssertion(ctx *context.OGContext) error { +func BeginWebAuthnAssertion(ctx *context.OGContext) error { sess := ctx.GetSession() ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint)) @@ -538,7 +538,7 @@ func beginWebAuthnAssertion(ctx *context.OGContext) error { return ctx.JSON_(credsCreation) } -func finishWebAuthnAssertion(ctx *context.OGContext) error { +func FinishWebAuthnAssertion(ctx *context.OGContext) error { sess := ctx.GetSession() sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte) if !ok { @@ -566,7 +566,7 @@ func finishWebAuthnAssertion(ctx *context.OGContext) error { return ctx.JSON_([]string{"OK"}) } -func beginTotp(ctx *context.OGContext) error { +func BeginTotp(ctx *context.OGContext) error { user := ctx.User if _, hasTotp, err := user.HasMFA(); err != nil { @@ -599,7 +599,7 @@ func beginTotp(ctx *context.OGContext) error { } -func finishTotp(ctx *context.OGContext) error { +func FinishTotp(ctx *context.OGContext) error { user := ctx.User if _, hasTotp, err := user.HasMFA(); err != nil { @@ -656,7 +656,7 @@ func finishTotp(ctx *context.OGContext) error { return ctx.HTML_("totp.html") } -func assertTotp(ctx *context.OGContext) error { +func AssertTotp(ctx *context.OGContext) error { var err error dto := &db.TOTPDTO{} if err := ctx.Bind(dto); err != nil { @@ -704,7 +704,7 @@ func assertTotp(ctx *context.OGContext) error { return ctx.RedirectTo(redirectUrl) } -func disableTotp(ctx *context.OGContext) error { +func DisableTotp(ctx *context.OGContext) error { user := ctx.User userTotp, err := db.GetTOTPByUserID(user.ID) if err != nil { @@ -719,7 +719,7 @@ func disableTotp(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func regenerateTotpRecoveryCodes(ctx *context.OGContext) error { +func RegenerateTotpRecoveryCodes(ctx *context.OGContext) error { user := ctx.User userTotp, err := db.GetTOTPByUserID(user.ID) if err != nil { @@ -735,7 +735,7 @@ func regenerateTotpRecoveryCodes(ctx *context.OGContext) error { return ctx.HTML_("totp.html") } -func logout(ctx *context.OGContext) error { +func Logout(ctx *context.OGContext) error { ctx.DeleteSession() ctx.DeleteCsrfCookie() return ctx.RedirectTo("/all") diff --git a/internal/web/handler/git_http.go b/internal/web/handler/git_http.go index 278620d..ee29f8a 100644 --- a/internal/web/handler/git_http.go +++ b/internal/web/handler/git_http.go @@ -44,7 +44,7 @@ var routes = []struct { {"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$", "GET", idxFile}, } -func gitHttp(ctx *context.OGContext) error { +func GitHttp(ctx *context.OGContext) error { for _, route := range routes { matched, _ := regexp.MatchString(route.gitUrl, ctx.Request().URL.Path) if ctx.Request().Method == route.method && matched { diff --git a/internal/web/handler/healthcheck.go b/internal/web/handler/healthcheck.go index 0ae6953..31c69c0 100644 --- a/internal/web/handler/healthcheck.go +++ b/internal/web/handler/healthcheck.go @@ -6,7 +6,7 @@ import ( "time" ) -func healthcheck(ctx *context.OGContext) error { +func Healthcheck(ctx *context.OGContext) error { // Check database connection dbOk := "ok" httpStatus := 200 @@ -24,8 +24,8 @@ func healthcheck(ctx *context.OGContext) error { }) } -// metrics is a dummy handler to satisfy the /metrics endpoint (for Prometheus, Openmetrics, etc.) +// Metrics is a dummy handler to satisfy the /metrics endpoint (for Prometheus, Openmetrics, etc.) // until we have a proper metrics endpoint -func metrics(ctx *context.OGContext) error { +func Metrics(ctx *context.OGContext) error { return ctx.String(200, "") } diff --git a/internal/web/handler/settings.go b/internal/web/handler/settings.go index ea18541..03ad6a1 100644 --- a/internal/web/handler/settings.go +++ b/internal/web/handler/settings.go @@ -18,7 +18,7 @@ import ( "golang.org/x/crypto/ssh" ) -func userSettings(ctx *context.OGContext) error { +func UserSettings(ctx *context.OGContext) error { user := ctx.User keys, err := db.GetSSHKeysByUserID(user.ID) @@ -46,7 +46,7 @@ func userSettings(ctx *context.OGContext) error { return ctx.HTML_("settings.html") } -func emailProcess(ctx *context.OGContext) error { +func EmailProcess(ctx *context.OGContext) error { user := ctx.User email := ctx.FormValue("email") var hash string @@ -69,7 +69,7 @@ func emailProcess(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func accountDeleteProcess(ctx *context.OGContext) error { +func AccountDeleteProcess(ctx *context.OGContext) error { user := ctx.User if err := user.Delete(); err != nil { @@ -79,7 +79,7 @@ func accountDeleteProcess(ctx *context.OGContext) error { return ctx.RedirectTo("/all") } -func sshKeysProcess(ctx *context.OGContext) error { +func SshKeysProcess(ctx *context.OGContext) error { user := ctx.User dto := new(db.SSHKeyDTO) @@ -118,7 +118,7 @@ func sshKeysProcess(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func sshKeysDelete(ctx *context.OGContext) error { +func SshKeysDelete(ctx *context.OGContext) error { user := ctx.User keyId, err := strconv.Atoi(ctx.Param("id")) if err != nil { @@ -139,7 +139,7 @@ func sshKeysDelete(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func passkeyDelete(ctx *context.OGContext) error { +func PasskeyDelete(ctx *context.OGContext) error { user := ctx.User keyId, err := strconv.Atoi(ctx.Param("id")) if err != nil { @@ -159,7 +159,7 @@ func passkeyDelete(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func passwordProcess(ctx *context.OGContext) error { +func PasswordProcess(ctx *context.OGContext) error { user := ctx.User dto := new(db.UserDTO) @@ -187,7 +187,7 @@ func passwordProcess(ctx *context.OGContext) error { return ctx.RedirectTo("/settings") } -func usernameProcess(ctx *context.OGContext) error { +func UsernameProcess(ctx *context.OGContext) error { user := ctx.User dto := new(db.UserDTO) diff --git a/internal/web/server/middlewares.go b/internal/web/server/middlewares.go index 4544447..802e962 100644 --- a/internal/web/server/middlewares.go +++ b/internal/web/server/middlewares.go @@ -10,6 +10,7 @@ import ( "github.com/thomiceli/opengist/internal/web/context" "golang.org/x/text/cases" "golang.org/x/text/language" + "html/template" "net/http" "strings" "time" @@ -24,16 +25,16 @@ func (s *Server) useCustomContext() { }) } -func (s *Server) RegisterMiddlewares(e *echo.Echo) { - e.Use(Middleware(dataInit).ToEcho()) - e.Use(Middleware(locale).ToEcho()) +func (s *Server) RegisterMiddlewares() { + s.echo.Use(Middleware(dataInit).ToEcho()) + s.echo.Use(Middleware(locale).ToEcho()) - e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + s.echo.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ Getter: middleware.MethodFromForm("_method"), })) - e.Pre(middleware.RemoveTrailingSlash()) - e.Pre(middleware.CORS()) - e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + s.echo.Pre(middleware.RemoveTrailingSlash()) + s.echo.Pre(middleware.CORS()) + s.echo.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogURI: true, LogStatus: true, LogMethod: true, LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error { log.Info().Str("uri", v.URI).Int("status", v.Status).Str("method", v.Method). @@ -42,10 +43,10 @@ func (s *Server) RegisterMiddlewares(e *echo.Echo) { return nil }, })) - e.Use(middleware.Recover()) - e.Use(middleware.Secure()) + s.echo.Use(middleware.Recover()) + s.echo.Use(middleware.Secure()) - e.Use(Middleware(sessionInit).ToEcho()) + s.echo.Use(Middleware(sessionInit).ToEcho()) } func dataInit(next Handler) Handler { @@ -155,7 +156,13 @@ func sessionInit(next Handler) Handler { func csrfInit(next Handler) Handler { return func(ctx *context.OGContext) error { - setCsrfHtmlForm(ctx) + var csrf string + if csrfToken, ok := ctx.Get("csrf").(string); ok { + csrf = csrfToken + } + ctx.SetData("csrfHtml", template.HTML(``)) + ctx.SetData("csrfHtml", template.HTML(``)) + return next(ctx) } } diff --git a/internal/web/server/router.go b/internal/web/server/router.go index abb4e43..f81d7fb 100644 --- a/internal/web/server/router.go +++ b/internal/web/server/router.go @@ -1 +1,213 @@ package server + +import ( + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/thomiceli/opengist/internal/config" + "github.com/thomiceli/opengist/internal/index" + "github.com/thomiceli/opengist/internal/web/context" + "github.com/thomiceli/opengist/internal/web/handler" + "github.com/thomiceli/opengist/public" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" +) + +func (s *Server) setupRoutes() { + r := NewRouter(s.echo.Group("")) + + // Web based routes + { + if !s.ignoreCsrf { + r.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "form:_csrf,header:X-CSRF-Token", + CookiePath: "/", + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, + })) + r.Use(csrfInit) + } + + r.GET("/", handler.Create, logged) + r.POST("/", handler.ProcessCreate, logged) + r.POST("/preview", handler.Preview, logged) + + r.GET("/healthcheck", handler.Healthcheck) + r.GET("/metrics", handler.Metrics) + + r.GET("/register", handler.Register) + r.POST("/register", handler.ProcessRegister) + r.GET("/login", handler.Login) + r.POST("/login", handler.ProcessLogin) + r.GET("/logout", handler.Logout) + r.GET("/oauth/:provider", handler.Oauth) + r.GET("/oauth/:provider/callback", handler.OauthCallback) + r.GET("/oauth/:provider/unlink", handler.OauthUnlink, logged) + r.POST("/webauthn/bind", handler.BeginWebAuthnBinding, logged) + r.POST("/webauthn/bind/finish", handler.FinishWebAuthnBinding, logged) + r.POST("/webauthn/login", handler.BeginWebAuthnLogin) + r.POST("/webauthn/login/finish", handler.FinishWebAuthnLogin) + r.POST("/webauthn/assertion", handler.BeginWebAuthnAssertion, inMFASession) + r.POST("/webauthn/assertion/finish", handler.FinishWebAuthnAssertion, inMFASession) + r.GET("/mfa", handler.Mfa, inMFASession) + r.POST("/mfa/totp/assertion", handler.AssertTotp, inMFASession) + + r.GET("/settings", handler.UserSettings, logged) + r.POST("/settings/email", handler.EmailProcess, logged) + r.DELETE("/settings/account", handler.AccountDeleteProcess, logged) + r.POST("/settings/ssh-keys", handler.SshKeysProcess, logged) + r.DELETE("/settings/ssh-keys/:id", handler.SshKeysDelete, logged) + r.DELETE("/settings/passkeys/:id", handler.PasskeyDelete, logged) + r.PUT("/settings/password", handler.PasswordProcess, logged) + r.PUT("/settings/username", handler.UsernameProcess, logged) + r.GET("/settings/totp/generate", handler.BeginTotp, logged) + r.POST("/settings/totp/generate", handler.FinishTotp, logged) + r.DELETE("/settings/totp", handler.DisableTotp, logged) + r.POST("/settings/totp/regenerate", handler.RegenerateTotpRecoveryCodes, logged) + + g2 := g1.Group("/admin-panel") + { + g2.Use(adminPermission) + g2.GET("", adminIndex) + g2.GET("/users", Handler(adminUsers).ToEcho()) + g2.POST("/users/:user/delete", adminUserDelete) + g2.GET("/gists", adminGists) + g2.POST("/gists/:gist/delete", adminGistDelete) + g2.GET("/invitations", adminInvitations) + g2.POST("/invitations", adminInvitationsCreate) + g2.POST("/invitations/:id/delete", adminInvitationsDelete) + g2.POST("/sync-fs", adminSyncReposFromFS) + g2.POST("/sync-db", adminSyncReposFromDB) + g2.POST("/gc-repos", adminGcRepos) + g2.POST("/sync-previews", adminSyncGistPreviews) + g2.POST("/reset-hooks", adminResetHooks) + g2.POST("/index-gists", adminIndexGists) + g2.GET("/configuration", adminConfig) + g2.PUT("/set-config", adminSetConfig) + } + + if config.C.HttpGit { + e.Any("/init/*", gitHttp, gistNewPushSoftInit) + } + + g1.GET("/all", allGists, checkRequireLogin) + + if index.Enabled() { + g1.GET("/search", search, checkRequireLogin) + } else { + g1.GET("/search", allGists, checkRequireLogin) + } + + g1.GET("/:user", allGists, checkRequireLogin) + g1.GET("/:user/liked", allGists, checkRequireLogin) + g1.GET("/:user/forked", allGists, checkRequireLogin) + + g3 := g1.Group("/:user/:gistname") + { + g3.Use(makeCheckRequireLogin(true), gistInit) + g3.GET("", gistIndex) + g3.GET("/rev/:revision", gistIndex) + g3.GET("/revisions", revisions) + g3.GET("/archive/:revision", downloadZip) + g3.POST("/visibility", editVisibility, logged, writePermission) + g3.POST("/delete", deleteGist, logged, writePermission) + g3.GET("/raw/:revision/:file", rawFile) + g3.GET("/download/:revision/:file", downloadFile) + g3.GET("/edit", edit, logged, writePermission) + g3.POST("/edit", processCreate, logged, writePermission) + g3.POST("/like", like, logged) + g3.GET("/likes", likes, checkRequireLogin) + g3.POST("/fork", fork, logged) + g3.GET("/forks", forks, checkRequireLogin) + g3.PUT("/checkbox", checkbox, logged, writePermission) + } + } + + customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom")) + e.GET("/assets/*", func(ctx echo.Context) error { + if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil { + ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000") + ctx.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat)) + + return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(ctx) + } + + // if the custom file is an .html template, render it + if strings.HasSuffix(ctx.Param("*"), ".html") { + if err := html(ctx, ctx.Param("*")); err != nil { + return notFound("Page not found") + } + return nil + } + + return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(ctx) + }) + + // Git HTTP routes + if config.C.HttpGit { + e.Any("/:user/:gistname/*", gitHttp, gistSoftInit) + } + + e.Any("/*", noRouteFound) +} + +// Router wraps echo.Group to provide custom Handler support +type Router struct { + *echo.Group +} + +// NewRouter creates a new Router instance +func NewRouter(g *echo.Group) *Router { + return &Router{Group: g} +} + +// SubGroup returns a new Router group with the given prefix and middleware +func (r *Router) SubGroup(prefix string, m ...Middleware) *Router { + // Convert middleware only when creating group + echoMiddleware := make([]echo.MiddlewareFunc, len(m)) + for i, mw := range m { + mw := mw // capture for closure + echoMiddleware[i] = func(next echo.HandlerFunc) echo.HandlerFunc { + return Chain(func(c *context.OGContext) error { + return next(c) + }, mw).toEchoHandler() + } + } + return NewRouter(r.Group.Group(prefix, echoMiddleware...)) +} + +// Route registration methods +func (r *Router) GET(path string, h Handler, m ...Middleware) { + r.Group.GET(path, Chain(h, m...).toEchoHandler()) +} + +func (r *Router) POST(path string, h Handler, m ...Middleware) { + r.Group.POST(path, Chain(h, m...).toEchoHandler()) +} + +func (r *Router) PUT(path string, h Handler, m ...Middleware) { + r.Group.PUT(path, Chain(h, m...).toEchoHandler()) +} + +func (r *Router) DELETE(path string, h Handler, m ...Middleware) { + r.Group.DELETE(path, Chain(h, m...).toEchoHandler()) +} + +func (r *Router) PATCH(path string, h Handler, m ...Middleware) { + r.Group.PATCH(path, Chain(h, m...).toEchoHandler()) +} + +// Use registers middleware for the entire router group +func (r *Router) Use(middleware ...Middleware) { + for _, m := range middleware { + m := m // capture for closure + r.Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return Chain(func(c *context.OGContext) error { + return next(c) + }, m).toEchoHandler() + }) + } +} diff --git a/internal/web/server/server.go b/internal/web/server/server.go index 6066952..7cc6342 100644 --- a/internal/web/server/server.go +++ b/internal/web/server/server.go @@ -58,7 +58,7 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server { log.Fatal().Err(err).Msg("Failed to load locales") } - s.RegisterMiddlewares(e) + s.RegisterMiddlewares() s.setFuncMap() s.setHTTPErrorHandler() @@ -68,140 +68,7 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server { parseManifestEntries() } - // Web based routes - g1 := e.Group("") - { - if !ignoreCsrf { - g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ - TokenLookup: "form:_csrf,header:X-CSRF-Token", - CookiePath: "/", - CookieHTTPOnly: true, - CookieSameSite: http.SameSiteStrictMode, - })) - g1.Use(Middleware(csrfInit).ToEcho()) - } - - g1.GET("/", Handler(handler.Create).ToEcho(), logged) - g1.POST("/", Handler(handler.ProcessCreate).ToEcho(), logged) - g1.POST("/preview", preview, logged) - - g1.GET("/healthcheck", healthcheck) - g1.GET("/metrics", metrics) - - g1.GET("/register", register) - g1.POST("/register", processRegister) - g1.GET("/login", login) - g1.POST("/login", processLogin) - g1.GET("/logout", logout) - g1.GET("/oauth/:provider", oauth) - g1.GET("/oauth/:provider/callback", oauthCallback) - g1.GET("/oauth/:provider/unlink", oauthUnlink, logged) - g1.POST("/webauthn/bind", beginWebAuthnBinding, logged) - g1.POST("/webauthn/bind/finish", finishWebAuthnBinding, logged) - g1.POST("/webauthn/login", beginWebAuthnLogin) - g1.POST("/webauthn/login/finish", finishWebAuthnLogin) - g1.POST("/webauthn/assertion", beginWebAuthnAssertion, inMFASession) - g1.POST("/webauthn/assertion/finish", finishWebAuthnAssertion, inMFASession) - g1.GET("/mfa", mfa, inMFASession) - g1.POST("/mfa/totp/assertion", assertTotp, inMFASession) - - g1.GET("/settings", userSettings, logged) - g1.POST("/settings/email", emailProcess, logged) - g1.DELETE("/settings/account", accountDeleteProcess, logged) - g1.POST("/settings/ssh-keys", sshKeysProcess, logged) - g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged) - g1.DELETE("/settings/passkeys/:id", passkeyDelete, logged) - g1.PUT("/settings/password", passwordProcess, logged) - g1.PUT("/settings/username", usernameProcess, logged) - g1.GET("/settings/totp/generate", beginTotp, logged) - g1.POST("/settings/totp/generate", finishTotp, logged) - g1.DELETE("/settings/totp", disableTotp, logged) - g1.POST("/settings/totp/regenerate", regenerateTotpRecoveryCodes, logged) - - g2 := g1.Group("/admin-panel") - { - g2.Use(adminPermission) - g2.GET("", adminIndex) - g2.GET("/users", Handler(adminUsers).ToEcho()) - g2.POST("/users/:user/delete", adminUserDelete) - g2.GET("/gists", adminGists) - g2.POST("/gists/:gist/delete", adminGistDelete) - g2.GET("/invitations", adminInvitations) - g2.POST("/invitations", adminInvitationsCreate) - g2.POST("/invitations/:id/delete", adminInvitationsDelete) - g2.POST("/sync-fs", adminSyncReposFromFS) - g2.POST("/sync-db", adminSyncReposFromDB) - g2.POST("/gc-repos", adminGcRepos) - g2.POST("/sync-previews", adminSyncGistPreviews) - g2.POST("/reset-hooks", adminResetHooks) - g2.POST("/index-gists", adminIndexGists) - g2.GET("/configuration", adminConfig) - g2.PUT("/set-config", adminSetConfig) - } - - if config.C.HttpGit { - e.Any("/init/*", gitHttp, gistNewPushSoftInit) - } - - g1.GET("/all", allGists, checkRequireLogin) - - if index.Enabled() { - g1.GET("/search", search, checkRequireLogin) - } else { - g1.GET("/search", allGists, checkRequireLogin) - } - - g1.GET("/:user", allGists, checkRequireLogin) - g1.GET("/:user/liked", allGists, checkRequireLogin) - g1.GET("/:user/forked", allGists, checkRequireLogin) - - g3 := g1.Group("/:user/:gistname") - { - g3.Use(makeCheckRequireLogin(true), gistInit) - g3.GET("", gistIndex) - g3.GET("/rev/:revision", gistIndex) - g3.GET("/revisions", revisions) - g3.GET("/archive/:revision", downloadZip) - g3.POST("/visibility", editVisibility, logged, writePermission) - g3.POST("/delete", deleteGist, logged, writePermission) - g3.GET("/raw/:revision/:file", rawFile) - g3.GET("/download/:revision/:file", downloadFile) - g3.GET("/edit", edit, logged, writePermission) - g3.POST("/edit", processCreate, logged, writePermission) - g3.POST("/like", like, logged) - g3.GET("/likes", likes, checkRequireLogin) - g3.POST("/fork", fork, logged) - g3.GET("/forks", forks, checkRequireLogin) - g3.PUT("/checkbox", checkbox, logged, writePermission) - } - } - - customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom")) - e.GET("/assets/*", func(ctx echo.Context) error { - if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil { - ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000") - ctx.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat)) - - return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(ctx) - } - - // if the custom file is an .html template, render it - if strings.HasSuffix(ctx.Param("*"), ".html") { - if err := html(ctx, ctx.Param("*")); err != nil { - return notFound("Page not found") - } - return nil - } - - return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(ctx) - }) - - // Git HTTP routes - if config.C.HttpGit { - e.Any("/:user/:gistname/*", gitHttp, gistSoftInit) - } - - e.Any("/*", noRouteFound) + s.setupRoutes() return s } diff --git a/internal/web/server/util.go b/internal/web/server/util.go index e9872e5..6e2ce1f 100644 --- a/internal/web/server/util.go +++ b/internal/web/server/util.go @@ -3,18 +3,8 @@ package server import ( "github.com/labstack/echo/v4" "github.com/thomiceli/opengist/internal/web/context" - "html/template" ) -func setCsrfHtmlForm(ctx *context.OGContext) { - var csrf string - if csrfToken, ok := ctx.Get("csrf").(string); ok { - csrf = csrfToken - } - ctx.SetData("csrfHtml", template.HTML(``)) - ctx.SetData("csrfHtml", template.HTML(``)) -} - type Handler func(ctx *context.OGContext) error type Middleware func(next Handler) Handler @@ -31,3 +21,22 @@ func (m Middleware) ToEcho() echo.MiddlewareFunc { }).ToEcho() } } + +func (h Handler) toEchoHandler() echo.HandlerFunc { + return func(c echo.Context) error { + if ogc, ok := c.(*context.OGContext); ok { + return h(ogc) + } + // Could also add error handling for incorrect context type + return h(c.(*context.OGContext)) + } +} + +// Chain applies middleware to a handler without conversion to echo types +func Chain(h Handler, middleware ...Middleware) Handler { + // Apply middleware in reverse order + for i := len(middleware) - 1; i >= 0; i-- { + h = middleware[i](h) + } + return h +}