This commit is contained in:
Thomas Miceli 2024-12-16 01:09:32 +01:00
parent 60027e4ab8
commit d2f6fe1ab8
8 changed files with 283 additions and 188 deletions

View file

@ -37,7 +37,7 @@ const (
OpenIDConnect = "openid-connect" OpenIDConnect = "openid-connect"
) )
func register(ctx *context.OGContext) error { func Register(ctx *context.OGContext) error {
disableSignup := ctx.GetData("DisableSignup") disableSignup := ctx.GetData("DisableSignup")
disableForm := ctx.GetData("DisableLoginForm") disableForm := ctx.GetData("DisableLoginForm")
@ -58,7 +58,7 @@ func register(ctx *context.OGContext) error {
return ctx.HTML_("auth_form.html") return ctx.HTML_("auth_form.html")
} }
func processRegister(ctx *context.OGContext) error { func ProcessRegister(ctx *context.OGContext) error {
disableSignup := ctx.GetData("DisableSignup") disableSignup := ctx.GetData("DisableSignup")
code := ctx.QueryParam("code") code := ctx.QueryParam("code")
@ -127,7 +127,7 @@ func processRegister(ctx *context.OGContext) error {
return ctx.RedirectTo("/") return ctx.RedirectTo("/")
} }
func login(ctx *context.OGContext) error { func Login(ctx *context.OGContext) error {
ctx.SetData("title", ctx.TrH("auth.login")) ctx.SetData("title", ctx.TrH("auth.login"))
ctx.SetData("htmlTitle", ctx.TrH("auth.login")) ctx.SetData("htmlTitle", ctx.TrH("auth.login"))
ctx.SetData("disableForm", ctx.GetData("DisableLoginForm")) ctx.SetData("disableForm", ctx.GetData("DisableLoginForm"))
@ -135,7 +135,7 @@ func login(ctx *context.OGContext) error {
return ctx.HTML_("auth_form.html") return ctx.HTML_("auth_form.html")
} }
func processLogin(ctx *context.OGContext) error { func ProcessLogin(ctx *context.OGContext) error {
if ctx.GetData("DisableLoginForm") == true { if ctx.GetData("DisableLoginForm") == true {
return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil) return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil)
} }
@ -189,7 +189,7 @@ func processLogin(ctx *context.OGContext) error {
return ctx.RedirectTo("/") return ctx.RedirectTo("/")
} }
func mfa(ctx *context.OGContext) error { func Mfa(ctx *context.OGContext) error {
var err error var err error
user := db.User{ID: ctx.GetSession().Values["mfaID"].(uint)} user := db.User{ID: ctx.GetSession().Values["mfaID"].(uint)}
@ -205,7 +205,7 @@ func mfa(ctx *context.OGContext) error {
return ctx.HTML_("mfa.html") 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()) user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil { if err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.complete-oauth-login", err.Error()), err) 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("/") return ctx.RedirectTo("/")
} }
func oauth(ctx *context.OGContext) error { func Oauth(ctx *context.OGContext) error {
provider := ctx.Param("provider") provider := ctx.Param("provider")
httpProtocol := "http" httpProtocol := "http"
@ -401,7 +401,7 @@ func oauth(ctx *context.OGContext) error {
return nil return nil
} }
func oauthUnlink(ctx *context.OGContext) error { func OauthUnlink(ctx *context.OGContext) error {
provider := ctx.Param("provider") provider := ctx.Param("provider")
currUser := ctx.User currUser := ctx.User
@ -425,7 +425,7 @@ func oauthUnlink(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func beginWebAuthnBinding(ctx *context.OGContext) error { func BeginWebAuthnBinding(ctx *context.OGContext) error {
credsCreation, jsonWaSession, err := webauthn.BeginBinding(ctx.User) credsCreation, jsonWaSession, err := webauthn.BeginBinding(ctx.User)
if err != nil { if err != nil {
return ctx.ErrorRes(500, "Cannot begin WebAuthn registration", err) return ctx.ErrorRes(500, "Cannot begin WebAuthn registration", err)
@ -439,7 +439,7 @@ func beginWebAuthnBinding(ctx *context.OGContext) error {
return ctx.JSON(200, credsCreation) return ctx.JSON(200, credsCreation)
} }
func finishWebAuthnBinding(ctx *context.OGContext) error { func FinishWebAuthnBinding(ctx *context.OGContext) error {
sess := ctx.GetSession() sess := ctx.GetSession()
jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte) jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte)
if !ok { if !ok {
@ -483,7 +483,7 @@ func finishWebAuthnBinding(ctx *context.OGContext) error {
return ctx.JSON_([]string{"OK"}) return ctx.JSON_([]string{"OK"})
} }
func beginWebAuthnLogin(ctx *context.OGContext) error { func BeginWebAuthnLogin(ctx *context.OGContext) error {
credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin() credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin()
if err != nil { if err != nil {
return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err) return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err)
@ -497,7 +497,7 @@ func beginWebAuthnLogin(ctx *context.OGContext) error {
return ctx.JSON_(credsCreation) return ctx.JSON_(credsCreation)
} }
func finishWebAuthnLogin(ctx *context.OGContext) error { func FinishWebAuthnLogin(ctx *context.OGContext) error {
sess := ctx.GetSession() sess := ctx.GetSession()
sessionData, ok := sess.Values["webauthn_login_session"].([]byte) sessionData, ok := sess.Values["webauthn_login_session"].([]byte)
if !ok { if !ok {
@ -518,7 +518,7 @@ func finishWebAuthnLogin(ctx *context.OGContext) error {
return ctx.JSON_([]string{"OK"}) return ctx.JSON_([]string{"OK"})
} }
func beginWebAuthnAssertion(ctx *context.OGContext) error { func BeginWebAuthnAssertion(ctx *context.OGContext) error {
sess := ctx.GetSession() sess := ctx.GetSession()
ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint)) ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint))
@ -538,7 +538,7 @@ func beginWebAuthnAssertion(ctx *context.OGContext) error {
return ctx.JSON_(credsCreation) return ctx.JSON_(credsCreation)
} }
func finishWebAuthnAssertion(ctx *context.OGContext) error { func FinishWebAuthnAssertion(ctx *context.OGContext) error {
sess := ctx.GetSession() sess := ctx.GetSession()
sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte) sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte)
if !ok { if !ok {
@ -566,7 +566,7 @@ func finishWebAuthnAssertion(ctx *context.OGContext) error {
return ctx.JSON_([]string{"OK"}) return ctx.JSON_([]string{"OK"})
} }
func beginTotp(ctx *context.OGContext) error { func BeginTotp(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
if _, hasTotp, err := user.HasMFA(); err != nil { 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 user := ctx.User
if _, hasTotp, err := user.HasMFA(); err != nil { if _, hasTotp, err := user.HasMFA(); err != nil {
@ -656,7 +656,7 @@ func finishTotp(ctx *context.OGContext) error {
return ctx.HTML_("totp.html") return ctx.HTML_("totp.html")
} }
func assertTotp(ctx *context.OGContext) error { func AssertTotp(ctx *context.OGContext) error {
var err error var err error
dto := &db.TOTPDTO{} dto := &db.TOTPDTO{}
if err := ctx.Bind(dto); err != nil { if err := ctx.Bind(dto); err != nil {
@ -704,7 +704,7 @@ func assertTotp(ctx *context.OGContext) error {
return ctx.RedirectTo(redirectUrl) return ctx.RedirectTo(redirectUrl)
} }
func disableTotp(ctx *context.OGContext) error { func DisableTotp(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
userTotp, err := db.GetTOTPByUserID(user.ID) userTotp, err := db.GetTOTPByUserID(user.ID)
if err != nil { if err != nil {
@ -719,7 +719,7 @@ func disableTotp(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func regenerateTotpRecoveryCodes(ctx *context.OGContext) error { func RegenerateTotpRecoveryCodes(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
userTotp, err := db.GetTOTPByUserID(user.ID) userTotp, err := db.GetTOTPByUserID(user.ID)
if err != nil { if err != nil {
@ -735,7 +735,7 @@ func regenerateTotpRecoveryCodes(ctx *context.OGContext) error {
return ctx.HTML_("totp.html") return ctx.HTML_("totp.html")
} }
func logout(ctx *context.OGContext) error { func Logout(ctx *context.OGContext) error {
ctx.DeleteSession() ctx.DeleteSession()
ctx.DeleteCsrfCookie() ctx.DeleteCsrfCookie()
return ctx.RedirectTo("/all") return ctx.RedirectTo("/all")

View file

@ -44,7 +44,7 @@ var routes = []struct {
{"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$", "GET", idxFile}, {"(.*?)/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 { for _, route := range routes {
matched, _ := regexp.MatchString(route.gitUrl, ctx.Request().URL.Path) matched, _ := regexp.MatchString(route.gitUrl, ctx.Request().URL.Path)
if ctx.Request().Method == route.method && matched { if ctx.Request().Method == route.method && matched {

View file

@ -6,7 +6,7 @@ import (
"time" "time"
) )
func healthcheck(ctx *context.OGContext) error { func Healthcheck(ctx *context.OGContext) error {
// Check database connection // Check database connection
dbOk := "ok" dbOk := "ok"
httpStatus := 200 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 // until we have a proper metrics endpoint
func metrics(ctx *context.OGContext) error { func Metrics(ctx *context.OGContext) error {
return ctx.String(200, "") return ctx.String(200, "")
} }

View file

@ -18,7 +18,7 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func userSettings(ctx *context.OGContext) error { func UserSettings(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
keys, err := db.GetSSHKeysByUserID(user.ID) keys, err := db.GetSSHKeysByUserID(user.ID)
@ -46,7 +46,7 @@ func userSettings(ctx *context.OGContext) error {
return ctx.HTML_("settings.html") return ctx.HTML_("settings.html")
} }
func emailProcess(ctx *context.OGContext) error { func EmailProcess(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
email := ctx.FormValue("email") email := ctx.FormValue("email")
var hash string var hash string
@ -69,7 +69,7 @@ func emailProcess(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func accountDeleteProcess(ctx *context.OGContext) error { func AccountDeleteProcess(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
if err := user.Delete(); err != nil { if err := user.Delete(); err != nil {
@ -79,7 +79,7 @@ func accountDeleteProcess(ctx *context.OGContext) error {
return ctx.RedirectTo("/all") return ctx.RedirectTo("/all")
} }
func sshKeysProcess(ctx *context.OGContext) error { func SshKeysProcess(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
dto := new(db.SSHKeyDTO) dto := new(db.SSHKeyDTO)
@ -118,7 +118,7 @@ func sshKeysProcess(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func sshKeysDelete(ctx *context.OGContext) error { func SshKeysDelete(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
keyId, err := strconv.Atoi(ctx.Param("id")) keyId, err := strconv.Atoi(ctx.Param("id"))
if err != nil { if err != nil {
@ -139,7 +139,7 @@ func sshKeysDelete(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func passkeyDelete(ctx *context.OGContext) error { func PasskeyDelete(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
keyId, err := strconv.Atoi(ctx.Param("id")) keyId, err := strconv.Atoi(ctx.Param("id"))
if err != nil { if err != nil {
@ -159,7 +159,7 @@ func passkeyDelete(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func passwordProcess(ctx *context.OGContext) error { func PasswordProcess(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
dto := new(db.UserDTO) dto := new(db.UserDTO)
@ -187,7 +187,7 @@ func passwordProcess(ctx *context.OGContext) error {
return ctx.RedirectTo("/settings") return ctx.RedirectTo("/settings")
} }
func usernameProcess(ctx *context.OGContext) error { func UsernameProcess(ctx *context.OGContext) error {
user := ctx.User user := ctx.User
dto := new(db.UserDTO) dto := new(db.UserDTO)

View file

@ -10,6 +10,7 @@ import (
"github.com/thomiceli/opengist/internal/web/context" "github.com/thomiceli/opengist/internal/web/context"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"html/template"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -24,16 +25,16 @@ func (s *Server) useCustomContext() {
}) })
} }
func (s *Server) RegisterMiddlewares(e *echo.Echo) { func (s *Server) RegisterMiddlewares() {
e.Use(Middleware(dataInit).ToEcho()) s.echo.Use(Middleware(dataInit).ToEcho())
e.Use(Middleware(locale).ToEcho()) s.echo.Use(Middleware(locale).ToEcho())
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ s.echo.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
Getter: middleware.MethodFromForm("_method"), Getter: middleware.MethodFromForm("_method"),
})) }))
e.Pre(middleware.RemoveTrailingSlash()) s.echo.Pre(middleware.RemoveTrailingSlash())
e.Pre(middleware.CORS()) s.echo.Pre(middleware.CORS())
e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ s.echo.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogURI: true, LogStatus: true, LogMethod: true, LogURI: true, LogStatus: true, LogMethod: true,
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error { LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {
log.Info().Str("uri", v.URI).Int("status", v.Status).Str("method", v.Method). 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 return nil
}, },
})) }))
e.Use(middleware.Recover()) s.echo.Use(middleware.Recover())
e.Use(middleware.Secure()) s.echo.Use(middleware.Secure())
e.Use(Middleware(sessionInit).ToEcho()) s.echo.Use(Middleware(sessionInit).ToEcho())
} }
func dataInit(next Handler) Handler { func dataInit(next Handler) Handler {
@ -155,7 +156,13 @@ func sessionInit(next Handler) Handler {
func csrfInit(next Handler) Handler { func csrfInit(next Handler) Handler {
return func(ctx *context.OGContext) error { 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(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
ctx.SetData("csrfHtml", template.HTML(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
return next(ctx) return next(ctx)
} }
} }

View file

@ -1 +1,213 @@
package server 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()
})
}
}

View file

@ -58,7 +58,7 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server {
log.Fatal().Err(err).Msg("Failed to load locales") log.Fatal().Err(err).Msg("Failed to load locales")
} }
s.RegisterMiddlewares(e) s.RegisterMiddlewares()
s.setFuncMap() s.setFuncMap()
s.setHTTPErrorHandler() s.setHTTPErrorHandler()
@ -68,140 +68,7 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server {
parseManifestEntries() parseManifestEntries()
} }
// Web based routes s.setupRoutes()
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)
return s return s
} }

View file

@ -3,18 +3,8 @@ package server
import ( import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/web/context" "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(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
ctx.SetData("csrfHtml", template.HTML(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
}
type Handler func(ctx *context.OGContext) error type Handler func(ctx *context.OGContext) error
type Middleware func(next Handler) Handler type Middleware func(next Handler) Handler
@ -31,3 +21,22 @@ func (m Middleware) ToEcho() echo.MiddlewareFunc {
}).ToEcho() }).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
}