diff --git a/internal/web/admin.go b/internal/web/admin.go
deleted file mode 100644
index c49d778..0000000
--- a/internal/web/admin.go
+++ /dev/null
@@ -1,236 +0,0 @@
-package web
-
-import (
- "github.com/labstack/echo/v4"
- "github.com/thomiceli/opengist/internal/actions"
- "github.com/thomiceli/opengist/internal/config"
- "github.com/thomiceli/opengist/internal/db"
- "github.com/thomiceli/opengist/internal/git"
- "runtime"
- "strconv"
- "time"
-)
-
-func adminIndex(ctx echo.Context) error {
- setData(ctx, "htmlTitle", trH(ctx, "admin.admin_panel"))
- setData(ctx, "adminHeaderPage", "index")
-
- setData(ctx, "opengistVersion", config.OpengistVersion)
- setData(ctx, "goVersion", runtime.Version())
- gitVersion, err := git.GetGitVersion()
- if err != nil {
- return errorRes(500, "Cannot get git version", err)
- }
- setData(ctx, "gitVersion", gitVersion)
-
- countUsers, err := db.CountAll(&db.User{})
- if err != nil {
- return errorRes(500, "Cannot count users", err)
- }
- setData(ctx, "countUsers", countUsers)
-
- countGists, err := db.CountAll(&db.Gist{})
- if err != nil {
- return errorRes(500, "Cannot count gists", err)
- }
- setData(ctx, "countGists", countGists)
-
- countKeys, err := db.CountAll(&db.SSHKey{})
- if err != nil {
- return errorRes(500, "Cannot count SSH keys", err)
- }
- setData(ctx, "countKeys", countKeys)
-
- setData(ctx, "syncReposFromFS", actions.IsRunning(actions.SyncReposFromFS))
- setData(ctx, "syncReposFromDB", actions.IsRunning(actions.SyncReposFromDB))
- setData(ctx, "gitGcRepos", actions.IsRunning(actions.GitGcRepos))
- setData(ctx, "syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
- setData(ctx, "resetHooks", actions.IsRunning(actions.ResetHooks))
- setData(ctx, "indexGists", actions.IsRunning(actions.IndexGists))
- return html(ctx, "admin_index.html")
-}
-
-func adminUsers(ctx echo.Context) error {
- setData(ctx, "htmlTitle", trH(ctx, "admin.users")+" - "+trH(ctx, "admin.admin_panel"))
- setData(ctx, "adminHeaderPage", "users")
- pageInt := getPage(ctx)
-
- var data []*db.User
- var err error
- if data, err = db.GetAllUsers(pageInt - 1); err != nil {
- return errorRes(500, "Cannot get users", err)
- }
-
- if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
- return errorRes(404, tr(ctx, "error.page-not-found"), nil)
- }
-
- return html(ctx, "admin_users.html")
-}
-
-func adminGists(ctx echo.Context) error {
- setData(ctx, "htmlTitle", trH(ctx, "admin.gists")+" - "+trH(ctx, "admin.admin_panel"))
- setData(ctx, "adminHeaderPage", "gists")
- pageInt := getPage(ctx)
-
- var data []*db.Gist
- var err error
- if data, err = db.GetAllGists(pageInt - 1); err != nil {
- return errorRes(500, "Cannot get gists", err)
- }
-
- if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
- return errorRes(404, tr(ctx, "error.page-not-found"), nil)
- }
-
- return html(ctx, "admin_gists.html")
-}
-
-func adminUserDelete(ctx echo.Context) error {
- userId, _ := strconv.ParseUint(ctx.Param("user"), 10, 64)
- user, err := db.GetUserById(uint(userId))
- if err != nil {
- return errorRes(500, "Cannot retrieve user", err)
- }
-
- if err := user.Delete(); err != nil {
- return errorRes(500, "Cannot delete this user", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.admin.user-deleted"), "success")
- return redirect(ctx, "/admin-panel/users")
-}
-
-func adminGistDelete(ctx echo.Context) error {
- gist, err := db.GetGistByID(ctx.Param("gist"))
- if err != nil {
- return errorRes(500, "Cannot retrieve gist", err)
- }
-
- if err = gist.DeleteRepository(); err != nil {
- return errorRes(500, "Cannot delete the repository", err)
- }
-
- if err = gist.Delete(); err != nil {
- return errorRes(500, "Cannot delete this gist", err)
- }
-
- gist.RemoveFromIndex()
-
- addFlash(ctx, tr(ctx, "flash.admin.gist-deleted"), "success")
- return redirect(ctx, "/admin-panel/gists")
-}
-
-func adminSyncReposFromFS(ctx echo.Context) error {
- addFlash(ctx, tr(ctx, "flash.admin.sync-fs"), "success")
- go actions.Run(actions.SyncReposFromFS)
- return redirect(ctx, "/admin-panel")
-}
-
-func adminSyncReposFromDB(ctx echo.Context) error {
- addFlash(ctx, tr(ctx, "flash.admin.sync-db"), "success")
- go actions.Run(actions.SyncReposFromDB)
- return redirect(ctx, "/admin-panel")
-}
-
-func adminGcRepos(ctx echo.Context) error {
- addFlash(ctx, tr(ctx, "flash.admin.git-gc"), "success")
- go actions.Run(actions.GitGcRepos)
- return redirect(ctx, "/admin-panel")
-}
-
-func adminSyncGistPreviews(ctx echo.Context) error {
- addFlash(ctx, tr(ctx, "flash.admin.sync-previews"), "success")
- go actions.Run(actions.SyncGistPreviews)
- return redirect(ctx, "/admin-panel")
-}
-
-func adminResetHooks(ctx echo.Context) error {
- addFlash(ctx, tr(ctx, "flash.admin.reset-hooks"), "success")
- go actions.Run(actions.ResetHooks)
- return redirect(ctx, "/admin-panel")
-}
-
-func adminIndexGists(ctx echo.Context) error {
- addFlash(ctx, tr(ctx, "flash.admin.index-gists"), "success")
- go actions.Run(actions.IndexGists)
- return redirect(ctx, "/admin-panel")
-}
-
-func adminConfig(ctx echo.Context) error {
- setData(ctx, "htmlTitle", trH(ctx, "admin.configuration")+" - "+trH(ctx, "admin.admin_panel"))
- setData(ctx, "adminHeaderPage", "config")
-
- setData(ctx, "dbtype", db.DatabaseInfo.Type.String())
- setData(ctx, "dbname", db.DatabaseInfo.Database)
-
- return html(ctx, "admin_config.html")
-}
-
-func adminSetConfig(ctx echo.Context) error {
- key := ctx.FormValue("key")
- value := ctx.FormValue("value")
-
- if err := db.UpdateSetting(key, value); err != nil {
- return errorRes(500, "Cannot set setting", err)
- }
-
- return ctx.JSON(200, map[string]interface{}{
- "success": true,
- })
-}
-
-func adminInvitations(ctx echo.Context) error {
- setData(ctx, "htmlTitle", trH(ctx, "admin.invitations")+" - "+trH(ctx, "admin.admin_panel"))
- setData(ctx, "adminHeaderPage", "invitations")
-
- var invitations []*db.Invitation
- var err error
- if invitations, err = db.GetAllInvitations(); err != nil {
- return errorRes(500, "Cannot get invites", err)
- }
-
- setData(ctx, "invitations", invitations)
- return html(ctx, "admin_invitations.html")
-}
-
-func adminInvitationsCreate(ctx echo.Context) error {
- code := ctx.FormValue("code")
- nbMax, err := strconv.ParseUint(ctx.FormValue("nbMax"), 10, 64)
- if err != nil {
- nbMax = 10
- }
-
- expiresAtUnix, err := strconv.ParseInt(ctx.FormValue("expiredAtUnix"), 10, 64)
- if err != nil {
- expiresAtUnix = time.Now().Unix() + 604800 // 1 week
- }
-
- invitation := &db.Invitation{
- Code: code,
- ExpiresAt: expiresAtUnix,
- NbMax: uint(nbMax),
- }
-
- if err := invitation.Create(); err != nil {
- return errorRes(500, "Cannot create invitation", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.admin.invitation-created"), "success")
- return redirect(ctx, "/admin-panel/invitations")
-}
-
-func adminInvitationsDelete(ctx echo.Context) error {
- id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
- invitation, err := db.GetInvitationByID(uint(id))
- if err != nil {
- return errorRes(500, "Cannot retrieve invitation", err)
- }
-
- if err := invitation.Delete(); err != nil {
- return errorRes(500, "Cannot delete this invitation", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.admin.invitation-deleted"), "success")
- return redirect(ctx, "/admin-panel/invitations")
-}
diff --git a/internal/web/context/context.go b/internal/web/context/context.go
new file mode 100644
index 0000000..9c3559c
--- /dev/null
+++ b/internal/web/context/context.go
@@ -0,0 +1,140 @@
+package context
+
+import (
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+ "github.com/rs/zerolog/log"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/db"
+ "github.com/thomiceli/opengist/internal/i18n"
+ "html/template"
+ "net/http"
+)
+
+type OGContext struct {
+ echo.Context
+
+ data echo.Map
+
+ store *Store
+ User *db.User
+}
+
+func NewContext(c echo.Context) *OGContext {
+ return &OGContext{
+ Context: c,
+ data: make(echo.Map),
+ }
+}
+
+func (ctx *OGContext) SetData(key string, value any) {
+ ctx.data[key] = value
+}
+
+func (ctx *OGContext) GetData(key string) any {
+ return ctx.data[key]
+}
+
+func (ctx *OGContext) DataMap() echo.Map {
+ return ctx.data
+}
+
+func (ctx *OGContext) ErrorRes(code int, message string, err error) error {
+ if code >= 500 {
+ var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger()
+ skipLogger.Error().Err(err).Msg(message)
+ }
+
+ return &echo.HTTPError{Code: code, Message: message, Internal: err}
+}
+
+func (ctx *OGContext) JsonErrorRes(code int, message string, err error) error {
+ if code >= 500 {
+ var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger()
+ skipLogger.Error().Err(err).Msg(message)
+ }
+
+ return &echo.HTTPError{Code: code, Message: message, Internal: err}
+}
+
+func (ctx *OGContext) RedirectTo(location string) error {
+ return ctx.Context.Redirect(302, config.C.ExternalUrl+location)
+}
+
+func (ctx *OGContext) HTML_(template string) error {
+ return ctx.HtmlWithCode(200, template)
+}
+
+func (ctx *OGContext) HtmlWithCode(code int, template string) error {
+ ctx.setErrorFlashes()
+ return ctx.Render(code, template, ctx.DataMap())
+}
+
+func (ctx *OGContext) JSON_(data any) error {
+ return ctx.JsonWithCode(200, data)
+}
+
+func (ctx *OGContext) JsonWithCode(code int, data any) error {
+ return ctx.JSON(code, data)
+}
+
+func (ctx *OGContext) PlainText(code int, message string) error {
+ return ctx.String(code, message)
+}
+
+func (ctx *OGContext) NotFound(message string) error {
+ return ctx.ErrorRes(404, message, nil)
+}
+
+func (ctx *OGContext) setErrorFlashes() {
+ sess, _ := ctx.store.flashStore.Get(ctx.Request(), "flash")
+
+ ctx.SetData("flashErrors", sess.Flashes("error"))
+ ctx.SetData("flashSuccess", sess.Flashes("success"))
+ ctx.SetData("flashWarnings", sess.Flashes("warning"))
+
+ _ = sess.Save(ctx.Request(), ctx.Response())
+}
+
+func (ctx *OGContext) GetSession() *sessions.Session {
+ sess, _ := ctx.store.UserStore.Get(ctx.Request(), "session")
+ return sess
+}
+
+func (ctx *OGContext) SaveSession(sess *sessions.Session) {
+ _ = sess.Save(ctx.Request(), ctx.Response())
+}
+
+func (ctx *OGContext) DeleteSession() {
+ sess := ctx.GetSession()
+ sess.Options.MaxAge = -1
+ ctx.SaveSession(sess)
+}
+
+func (ctx *OGContext) AddFlash(flashMessage string, flashType string) {
+ sess, _ := ctx.store.flashStore.Get(ctx.Request(), "flash")
+ sess.AddFlash(flashMessage, flashType)
+ _ = sess.Save(ctx.Request(), ctx.Response())
+}
+
+func (ctx *OGContext) getUserLogged() *db.User {
+ user := ctx.GetData("userLogged")
+ if user != nil {
+ return user.(*db.User)
+ }
+ return nil
+}
+
+func (ctx *OGContext) DeleteCsrfCookie() {
+ ctx.SetCookie(&http.Cookie{Name: "_csrf", Path: "/", MaxAge: -1})
+}
+
+func (ctx *OGContext) TrH(key string, args ...any) template.HTML {
+ l := ctx.GetData("locale").(*i18n.Locale)
+ return l.Tr(key, args...)
+}
+
+func (ctx *OGContext) Tr(key string, args ...any) string {
+ l := ctx.GetData("locale").(*i18n.Locale)
+ return l.String(key, args...)
+}
diff --git a/internal/web/context/store.go b/internal/web/context/store.go
new file mode 100644
index 0000000..01a0ef0
--- /dev/null
+++ b/internal/web/context/store.go
@@ -0,0 +1,28 @@
+package context
+
+import (
+ "github.com/gorilla/sessions"
+ "github.com/markbates/goth/gothic"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/utils"
+ "path/filepath"
+)
+
+type Store struct {
+ sessionsPath string
+
+ flashStore *sessions.CookieStore
+ UserStore *sessions.FilesystemStore
+}
+
+func NewStore(sessionsPath string) *Store {
+ return &Store{sessionsPath: sessionsPath}
+}
+
+func (s *Store) setupSessionStore() {
+ s.flashStore = sessions.NewCookieStore([]byte("opengist"))
+ encryptKey, _ := utils.GenerateSecretKey(filepath.Join(s.sessionsPath, "session-encrypt.key"))
+ s.UserStore = sessions.NewFilesystemStore(s.sessionsPath, config.SecretKey, encryptKey)
+ s.UserStore.MaxLength(10 * 1024)
+ gothic.Store = s.UserStore
+}
diff --git a/internal/web/handler/admin.go b/internal/web/handler/admin.go
new file mode 100644
index 0000000..30b2991
--- /dev/null
+++ b/internal/web/handler/admin.go
@@ -0,0 +1,238 @@
+package handler
+
+import (
+ "github.com/thomiceli/opengist/internal/actions"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/db"
+ "github.com/thomiceli/opengist/internal/git"
+ "github.com/thomiceli/opengist/internal/web/context"
+ "runtime"
+ "strconv"
+ "time"
+)
+
+func AdminIndex(ctx *context.OGContext) error {
+ ctx.SetData("htmlTitle", ctx.TrH("admin.admin_panel"))
+ ctx.SetData("adminHeaderPage", "index")
+
+ ctx.SetData("opengistVersion", config.OpengistVersion)
+ ctx.SetData("goVersion", runtime.Version())
+ gitVersion, err := git.GetGitVersion()
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot get git version", err)
+ }
+ ctx.SetData("gitVersion", gitVersion)
+
+ countUsers, err := db.CountAll(&db.User{})
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot count users", err)
+ }
+ ctx.SetData("countUsers", countUsers)
+
+ countGists, err := db.CountAll(&db.Gist{})
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot count gists", err)
+ }
+ ctx.SetData("countGists", countGists)
+
+ countKeys, err := db.CountAll(&db.SSHKey{})
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot count SSH keys", err)
+ }
+ ctx.SetData("countKeys", countKeys)
+
+ ctx.SetData("syncReposFromFS", actions.IsRunning(actions.SyncReposFromFS))
+ ctx.SetData("syncReposFromDB", actions.IsRunning(actions.SyncReposFromDB))
+ ctx.SetData("gitGcRepos", actions.IsRunning(actions.GitGcRepos))
+ ctx.SetData("syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
+ ctx.SetData("resetHooks", actions.IsRunning(actions.ResetHooks))
+ ctx.SetData("indexGists", actions.IsRunning(actions.IndexGists))
+ return ctx.HTML_("admin_index.html")
+}
+
+func AdminUsers(ctx *context.OGContext) error {
+ ctx.SetData("htmlTitle", ctx.TrH("admin.users")+" - "+ctx.TrH("admin.admin_panel"))
+ ctx.SetData("adminHeaderPage", "users")
+ ctx.SetData("loadStartTime", time.Now())
+
+ pageInt := getPage(ctx)
+
+ var data []*db.User
+ var err error
+ if data, err = db.GetAllUsers(pageInt - 1); err != nil {
+ return ctx.ErrorRes(500, "Cannot get users", err)
+ }
+
+ if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
+ return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
+ }
+
+ return ctx.HTML_("admin_users.html")
+}
+
+func AdminGists(ctx *context.OGContext) error {
+ ctx.SetData("htmlTitle", ctx.TrH("admin.gists")+" - "+ctx.TrH("admin.admin_panel"))
+ ctx.SetData("adminHeaderPage", "gists")
+ pageInt := getPage(ctx)
+
+ var data []*db.Gist
+ var err error
+ if data, err = db.GetAllGists(pageInt - 1); err != nil {
+ return ctx.ErrorRes(500, "Cannot get gists", err)
+ }
+
+ if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
+ return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
+ }
+
+ return ctx.HTML_("admin_gists.html")
+}
+
+func AdminUserDelete(ctx *context.OGContext) error {
+ userId, _ := strconv.ParseUint(ctx.Param("user"), 10, 64)
+ user, err := db.GetUserById(uint(userId))
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot retrieve user", err)
+ }
+
+ if err := user.Delete(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete this user", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.admin.user-deleted"), "success")
+ return ctx.RedirectTo("/admin-panel/users")
+}
+
+func AdminGistDelete(ctx *context.OGContext) error {
+ gist, err := db.GetGistByID(ctx.Param("gist"))
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot retrieve gist", err)
+ }
+
+ if err = gist.DeleteRepository(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete the repository", err)
+ }
+
+ if err = gist.Delete(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete this gist", err)
+ }
+
+ gist.RemoveFromIndex()
+
+ ctx.AddFlash(ctx.Tr("flash.admin.gist-deleted"), "success")
+ return ctx.RedirectTo("/admin-panel/gists")
+}
+
+func AdminSyncReposFromFS(ctx *context.OGContext) error {
+ ctx.AddFlash(ctx.Tr("flash.admin.sync-fs"), "success")
+ go actions.Run(actions.SyncReposFromFS)
+ return ctx.RedirectTo("/admin-panel")
+}
+
+func AdminSyncReposFromDB(ctx *context.OGContext) error {
+ ctx.AddFlash(ctx.Tr("flash.admin.sync-db"), "success")
+ go actions.Run(actions.SyncReposFromDB)
+ return ctx.RedirectTo("/admin-panel")
+}
+
+func AdminGcRepos(ctx *context.OGContext) error {
+ ctx.AddFlash(ctx.Tr("flash.admin.git-gc"), "success")
+ go actions.Run(actions.GitGcRepos)
+ return ctx.RedirectTo("/admin-panel")
+}
+
+func AdminSyncGistPreviews(ctx *context.OGContext) error {
+ ctx.AddFlash(ctx.Tr("flash.admin.sync-previews"), "success")
+ go actions.Run(actions.SyncGistPreviews)
+ return ctx.RedirectTo("/admin-panel")
+}
+
+func AdminResetHooks(ctx *context.OGContext) error {
+ ctx.AddFlash(ctx.Tr("flash.admin.reset-hooks"), "success")
+ go actions.Run(actions.ResetHooks)
+ return ctx.RedirectTo("/admin-panel")
+}
+
+func AdminIndexGists(ctx *context.OGContext) error {
+ ctx.AddFlash(ctx.Tr("flash.admin.index-gists"), "success")
+ go actions.Run(actions.IndexGists)
+ return ctx.RedirectTo("/admin-panel")
+}
+
+func AdminConfig(ctx *context.OGContext) error {
+ ctx.SetData("htmlTitle", ctx.TrH("admin.configuration")+" - "+ctx.TrH("admin.admin_panel"))
+ ctx.SetData("adminHeaderPage", "config")
+
+ ctx.SetData("dbtype", db.DatabaseInfo.Type.String())
+ ctx.SetData("dbname", db.DatabaseInfo.Database)
+
+ return ctx.HTML_("admin_config.html")
+}
+
+func AdminSetConfig(ctx *context.OGContext) error {
+ key := ctx.FormValue("key")
+ value := ctx.FormValue("value")
+
+ if err := db.UpdateSetting(key, value); err != nil {
+ return ctx.ErrorRes(500, "Cannot set setting", err)
+ }
+
+ return ctx.JSON(200, map[string]interface{}{
+ "success": true,
+ })
+}
+
+func AdminInvitations(ctx *context.OGContext) error {
+ ctx.SetData("htmlTitle", ctx.TrH("admin.invitations")+" - "+ctx.TrH("admin.admin_panel"))
+ ctx.SetData("adminHeaderPage", "invitations")
+
+ var invitations []*db.Invitation
+ var err error
+ if invitations, err = db.GetAllInvitations(); err != nil {
+ return ctx.ErrorRes(500, "Cannot get invites", err)
+ }
+
+ ctx.SetData("invitations", invitations)
+ return ctx.HTML_("admin_invitations.html")
+}
+
+func AdminInvitationsCreate(ctx *context.OGContext) error {
+ code := ctx.FormValue("code")
+ nbMax, err := strconv.ParseUint(ctx.FormValue("nbMax"), 10, 64)
+ if err != nil {
+ nbMax = 10
+ }
+
+ expiresAtUnix, err := strconv.ParseInt(ctx.FormValue("expiredAtUnix"), 10, 64)
+ if err != nil {
+ expiresAtUnix = time.Now().Unix() + 604800 // 1 week
+ }
+
+ invitation := &db.Invitation{
+ Code: code,
+ ExpiresAt: expiresAtUnix,
+ NbMax: uint(nbMax),
+ }
+
+ if err := invitation.Create(); err != nil {
+ return ctx.ErrorRes(500, "Cannot create invitation", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.admin.invitation-created"), "success")
+ return ctx.RedirectTo("/admin-panel/invitations")
+}
+
+func AdminInvitationsDelete(ctx *context.OGContext) error {
+ id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
+ invitation, err := db.GetInvitationByID(uint(id))
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot retrieve invitation", err)
+ }
+
+ if err := invitation.Delete(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete this invitation", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.admin.invitation-deleted"), "success")
+ return ctx.RedirectTo("/admin-panel/invitations")
+}
diff --git a/internal/web/auth.go b/internal/web/handler/auth.go
similarity index 58%
rename from internal/web/auth.go
rename to internal/web/handler/auth.go
index 39d3d28..735b687 100644
--- a/internal/web/auth.go
+++ b/internal/web/handler/auth.go
@@ -1,13 +1,12 @@
-package web
+package handler
import (
"bytes"
- "context"
+ gocontext "context"
"crypto/md5"
gojson "encoding/json"
"errors"
"fmt"
- "github.com/labstack/echo/v4"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
@@ -21,6 +20,7 @@ import (
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/utils"
+ "github.com/thomiceli/opengist/internal/web/context"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gorm.io/gorm"
@@ -37,115 +37,115 @@ const (
OpenIDConnect = "openid-connect"
)
-func register(ctx echo.Context) error {
- disableSignup := getData(ctx, "DisableSignup")
- disableForm := getData(ctx, "DisableLoginForm")
+func register(ctx *context.OGContext) error {
+ disableSignup := ctx.GetData("DisableSignup")
+ disableForm := ctx.GetData("DisableLoginForm")
code := ctx.QueryParam("code")
if code != "" {
if invitation, err := db.GetInvitationByCode(code); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- return errorRes(500, "Cannot check for invitation code", err)
+ return ctx.ErrorRes(500, "Cannot check for invitation code", err)
} else if invitation != nil && invitation.IsUsable() {
disableSignup = false
}
}
- setData(ctx, "title", trH(ctx, "auth.new-account"))
- setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
- setData(ctx, "disableForm", disableForm)
- setData(ctx, "disableSignup", disableSignup)
- setData(ctx, "isLoginPage", false)
- return html(ctx, "auth_form.html")
+ ctx.SetData("title", ctx.TrH("auth.new-account"))
+ ctx.SetData("htmlTitle", ctx.TrH("auth.new-account"))
+ ctx.SetData("disableForm", disableForm)
+ ctx.SetData("disableSignup", disableSignup)
+ ctx.SetData("isLoginPage", false)
+ return ctx.HTML_("auth_form.html")
}
-func processRegister(ctx echo.Context) error {
- disableSignup := getData(ctx, "DisableSignup")
+func processRegister(ctx *context.OGContext) error {
+ disableSignup := ctx.GetData("DisableSignup")
code := ctx.QueryParam("code")
invitation, err := db.GetInvitationByCode(code)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- return errorRes(500, "Cannot check for invitation code", err)
+ return ctx.ErrorRes(500, "Cannot check for invitation code", err)
} else if invitation.ID != 0 && invitation.IsUsable() {
disableSignup = false
}
if disableSignup == true {
- return errorRes(403, tr(ctx, "error.signup-disabled"), nil)
+ return ctx.ErrorRes(403, ctx.Tr("error.signup-disabled"), nil)
}
- if getData(ctx, "DisableLoginForm") == true {
- return errorRes(403, tr(ctx, "error.signup-disabled-form"), nil)
+ if ctx.GetData("DisableLoginForm") == true {
+ return ctx.ErrorRes(403, ctx.Tr("error.signup-disabled-form"), nil)
}
- setData(ctx, "title", trH(ctx, "auth.new-account"))
- setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
+ ctx.SetData("title", ctx.TrH("auth.new-account"))
+ ctx.SetData("htmlTitle", ctx.TrH("auth.new-account"))
- sess := getSession(ctx)
+ sess := ctx.GetSession()
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
if err := ctx.Validate(dto); err != nil {
- addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
- return html(ctx, "auth_form.html")
+ ctx.AddFlash(utils.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
+ return ctx.HTML_("auth_form.html")
}
if exists, err := db.UserExists(dto.Username); err != nil || exists {
- addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
- return html(ctx, "auth_form.html")
+ ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
+ return ctx.HTML_("auth_form.html")
}
user := dto.ToUser()
password, err := utils.Argon2id.Hash(user.Password)
if err != nil {
- return errorRes(500, "Cannot hash password", err)
+ return ctx.ErrorRes(500, "Cannot hash password", err)
}
user.Password = password
if err = user.Create(); err != nil {
- return errorRes(500, "Cannot create user", err)
+ return ctx.ErrorRes(500, "Cannot create user", err)
}
if user.ID == 1 {
if err = user.SetAdmin(); err != nil {
- return errorRes(500, "Cannot set user admin", err)
+ return ctx.ErrorRes(500, "Cannot set user admin", err)
}
}
if invitation.ID != 0 {
if err := invitation.Use(); err != nil {
- return errorRes(500, "Cannot use invitation", err)
+ return ctx.ErrorRes(500, "Cannot use invitation", err)
}
}
sess.Values["user"] = user.ID
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- return redirect(ctx, "/")
+ return ctx.RedirectTo("/")
}
-func login(ctx echo.Context) error {
- setData(ctx, "title", trH(ctx, "auth.login"))
- setData(ctx, "htmlTitle", trH(ctx, "auth.login"))
- setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
- setData(ctx, "isLoginPage", true)
- return html(ctx, "auth_form.html")
+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"))
+ ctx.SetData("isLoginPage", true)
+ return ctx.HTML_("auth_form.html")
}
-func processLogin(ctx echo.Context) error {
- if getData(ctx, "DisableLoginForm") == true {
- return errorRes(403, tr(ctx, "error.login-disabled-form"), nil)
+func processLogin(ctx *context.OGContext) error {
+ if ctx.GetData("DisableLoginForm") == true {
+ return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil)
}
var err error
- sess := getSession(ctx)
+ sess := ctx.GetSession()
dto := &db.UserDTO{}
if err = ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
password := dto.Password
@@ -153,86 +153,86 @@ func processLogin(ctx echo.Context) error {
if user, err = db.GetUserByUsername(dto.Username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
- return errorRes(500, "Cannot get user", err)
+ return ctx.ErrorRes(500, "Cannot get user", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
- addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error")
- return redirect(ctx, "/login")
+ ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
+ return ctx.RedirectTo("/login")
}
if ok, err := utils.Argon2id.Verify(password, user.Password); !ok {
if err != nil {
- return errorRes(500, "Cannot check for password", err)
+ return ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
- addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error")
- return redirect(ctx, "/login")
+ ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
+ return ctx.RedirectTo("/login")
}
// handle MFA
var hasWebauthn, hasTotp bool
if hasWebauthn, hasTotp, err = user.HasMFA(); err != nil {
- return errorRes(500, "Cannot check for user MFA", err)
+ return ctx.ErrorRes(500, "Cannot check for user MFA", err)
}
if hasWebauthn || hasTotp {
sess.Values["mfaID"] = user.ID
sess.Options.MaxAge = 5 * 60 // 5 minutes
- saveSession(sess, ctx)
- return redirect(ctx, "/mfa")
+ ctx.SaveSession(sess)
+ return ctx.RedirectTo("/mfa")
}
sess.Values["user"] = user.ID
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
- saveSession(sess, ctx)
- deleteCsrfCookie(ctx)
+ ctx.SaveSession(sess)
+ ctx.DeleteCsrfCookie()
- return redirect(ctx, "/")
+ return ctx.RedirectTo("/")
}
-func mfa(ctx echo.Context) error {
+func mfa(ctx *context.OGContext) error {
var err error
- user := db.User{ID: getSession(ctx).Values["mfaID"].(uint)}
+ user := db.User{ID: ctx.GetSession().Values["mfaID"].(uint)}
var hasWebauthn, hasTotp bool
if hasWebauthn, hasTotp, err = user.HasMFA(); err != nil {
- return errorRes(500, "Cannot check for user MFA", err)
+ return ctx.ErrorRes(500, "Cannot check for user MFA", err)
}
- setData(ctx, "hasWebauthn", hasWebauthn)
- setData(ctx, "hasTotp", hasTotp)
+ ctx.SetData("hasWebauthn", hasWebauthn)
+ ctx.SetData("hasTotp", hasTotp)
- return html(ctx, "mfa.html")
+ return ctx.HTML_("mfa.html")
}
-func oauthCallback(ctx echo.Context) error {
+func oauthCallback(ctx *context.OGContext) error {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
- return errorRes(400, tr(ctx, "error.complete-oauth-login", err.Error()), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.complete-oauth-login", err.Error()), err)
}
- currUser := getUserLogged(ctx)
+ currUser := ctx.User
if currUser != nil {
// if user is logged in, link account to user and update its avatar URL
updateUserProviderInfo(currUser, user.Provider, user)
if err = currUser.Update(); err != nil {
- return errorRes(500, "Cannot update user "+cases.Title(language.English).String(user.Provider)+" id", err)
+ return ctx.ErrorRes(500, "Cannot update user "+cases.Title(language.English).String(user.Provider)+" id", err)
}
- addFlash(ctx, tr(ctx, "flash.auth.account-linked-oauth", cases.Title(language.English).String(user.Provider)), "success")
- return redirect(ctx, "/settings")
+ ctx.AddFlash(ctx.Tr("flash.auth.account-linked-oauth", cases.Title(language.English).String(user.Provider)), "success")
+ return ctx.RedirectTo("/settings")
}
// if user is not in database, create it
userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
if err != nil {
- if getData(ctx, "DisableSignup") == true {
- return errorRes(403, tr(ctx, "error.signup-disabled"), nil)
+ if ctx.GetData("DisableSignup") == true {
+ return ctx.ErrorRes(403, ctx.Tr("error.signup-disabled"), nil)
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
- return errorRes(500, "Cannot get user", err)
+ return ctx.ErrorRes(500, "Cannot get user", err)
}
if user.NickName == "" {
@@ -250,16 +250,16 @@ func oauthCallback(ctx echo.Context) error {
if err = userDB.Create(); err != nil {
if db.IsUniqueConstraintViolation(err) {
- addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
- return redirect(ctx, "/login")
+ ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
+ return ctx.RedirectTo("/login")
}
- return errorRes(500, "Cannot create user", err)
+ return ctx.ErrorRes(500, "Cannot create user", err)
}
if userDB.ID == 1 {
if err = userDB.SetAdmin(); err != nil {
- return errorRes(500, "Cannot set user admin", err)
+ return ctx.ErrorRes(500, "Cannot set user admin", err)
}
}
@@ -280,7 +280,7 @@ func oauthCallback(ctx echo.Context) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
- addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-retrievable"), "error")
+ ctx.AddFlash(ctx.Tr("flash.auth.user-sshkeys-not-retrievable"), "error")
log.Error().Err(err).Msg("Could not get user keys")
}
@@ -296,22 +296,22 @@ func oauthCallback(ctx echo.Context) error {
}
if err = sshKey.Create(); err != nil {
- addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-created"), "error")
+ ctx.AddFlash(ctx.Tr("flash.auth.user-sshkeys-not-created"), "error")
log.Error().Err(err).Msg("Could not create ssh key")
}
}
}
}
- sess := getSession(ctx)
+ sess := ctx.GetSession()
sess.Values["user"] = userDB.ID
- saveSession(sess, ctx)
- deleteCsrfCookie(ctx)
+ ctx.SaveSession(sess)
+ ctx.DeleteCsrfCookie()
- return redirect(ctx, "/")
+ return ctx.RedirectTo("/")
}
-func oauth(ctx echo.Context) error {
+func oauth(ctx *context.OGContext) error {
provider := ctx.Param("provider")
httpProtocol := "http"
@@ -385,26 +385,26 @@ func oauth(ctx echo.Context) error {
)
if err != nil {
- return errorRes(500, "Cannot create OIDC provider", err)
+ return ctx.ErrorRes(500, "Cannot create OIDC provider", err)
}
goth.UseProviders(oidcProvider)
}
- ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
+ ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect {
- return errorRes(400, tr(ctx, "error.oauth-unsupported"), nil)
+ return ctx.ErrorRes(400, ctx.Tr("error.oauth-unsupported"), nil)
}
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
return nil
}
-func oauthUnlink(ctx echo.Context) error {
+func oauthUnlink(ctx *context.OGContext) error {
provider := ctx.Param("provider")
- currUser := getUserLogged(ctx)
+ currUser := ctx.User
// Map each provider to a function that checks the relevant ID in currUser
providerIDCheckMap := map[string]func() bool{
GitHubProvider: func() bool { return currUser.GithubID != "" },
@@ -415,43 +415,43 @@ func oauthUnlink(ctx echo.Context) error {
if checkFunc, exists := providerIDCheckMap[provider]; exists && checkFunc() {
if err := currUser.DeleteProviderID(provider); err != nil {
- return errorRes(500, "Cannot unlink account from "+cases.Title(language.English).String(provider), err)
+ return ctx.ErrorRes(500, "Cannot unlink account from "+cases.Title(language.English).String(provider), err)
}
- addFlash(ctx, tr(ctx, "flash.auth.account-unlinked-oauth", cases.Title(language.English).String(provider)), "success")
- return redirect(ctx, "/settings")
+ ctx.AddFlash(ctx.Tr("flash.auth.account-unlinked-oauth", cases.Title(language.English).String(provider)), "success")
+ return ctx.RedirectTo("/settings")
}
- return redirect(ctx, "/settings")
+ return ctx.RedirectTo("/settings")
}
-func beginWebAuthnBinding(ctx echo.Context) error {
- credsCreation, jsonWaSession, err := webauthn.BeginBinding(getUserLogged(ctx))
+func beginWebAuthnBinding(ctx *context.OGContext) error {
+ credsCreation, jsonWaSession, err := webauthn.BeginBinding(ctx.User)
if err != nil {
- return errorRes(500, "Cannot begin WebAuthn registration", err)
+ return ctx.ErrorRes(500, "Cannot begin WebAuthn registration", err)
}
- sess := getSession(ctx)
+ sess := ctx.GetSession()
sess.Values["webauthn_registration_session"] = jsonWaSession
sess.Options.MaxAge = 5 * 60 // 5 minutes
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
return ctx.JSON(200, credsCreation)
}
-func finishWebAuthnBinding(ctx echo.Context) error {
- sess := getSession(ctx)
+func finishWebAuthnBinding(ctx *context.OGContext) error {
+ sess := ctx.GetSession()
jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte)
if !ok {
- return jsonErrorRes(401, "Cannot get WebAuthn registration session", nil)
+ return ctx.JsonErrorRes(401, "Cannot get WebAuthn registration session", nil)
}
- user := getUserLogged(ctx)
+ user := ctx.User
// extract passkey name from request
body, err := io.ReadAll(ctx.Request().Body)
if err != nil {
- return jsonErrorRes(400, "Failed to read request body", err)
+ return ctx.JsonErrorRes(400, "Failed to read request body", err)
}
ctx.Request().Body.Close()
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(body))
@@ -460,7 +460,7 @@ func finishWebAuthnBinding(ctx echo.Context) error {
_ = gojson.Unmarshal(body, &dto)
if err = ctx.Validate(dto); err != nil {
- return jsonErrorRes(400, "Invalid request", err)
+ return ctx.JsonErrorRes(400, "Invalid request", err)
}
passkeyName := dto.PasskeyName
if passkeyName == "" {
@@ -469,91 +469,91 @@ func finishWebAuthnBinding(ctx echo.Context) error {
waCredential, err := webauthn.FinishBinding(user, jsonWaSession, ctx.Request())
if err != nil {
- return jsonErrorRes(403, "Failed binding attempt for passkey", err)
+ return ctx.JsonErrorRes(403, "Failed binding attempt for passkey", err)
}
if _, err = db.CreateFromCrendential(user.ID, passkeyName, waCredential); err != nil {
- return jsonErrorRes(500, "Cannot create WebAuthn credential on database", err)
+ return ctx.JsonErrorRes(500, "Cannot create WebAuthn credential on database", err)
}
delete(sess.Values, "webauthn_registration_session")
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- addFlash(ctx, tr(ctx, "flash.auth.passkey-registred", passkeyName), "success")
- return json(ctx, []string{"OK"})
+ ctx.AddFlash(ctx.Tr("flash.auth.passkey-registred", passkeyName), "success")
+ return ctx.JSON_([]string{"OK"})
}
-func beginWebAuthnLogin(ctx echo.Context) error {
+func beginWebAuthnLogin(ctx *context.OGContext) error {
credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin()
if err != nil {
- return jsonErrorRes(401, "Cannot begin WebAuthn login", err)
+ return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err)
}
- sess := getSession(ctx)
+ sess := ctx.GetSession()
sess.Values["webauthn_login_session"] = jsonWaSession
sess.Options.MaxAge = 5 * 60 // 5 minutes
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- return json(ctx, credsCreation)
+ return ctx.JSON_(credsCreation)
}
-func finishWebAuthnLogin(ctx echo.Context) error {
- sess := getSession(ctx)
+func finishWebAuthnLogin(ctx *context.OGContext) error {
+ sess := ctx.GetSession()
sessionData, ok := sess.Values["webauthn_login_session"].([]byte)
if !ok {
- return jsonErrorRes(401, "Cannot get WebAuthn login session", nil)
+ return ctx.JsonErrorRes(401, "Cannot get WebAuthn login session", nil)
}
userID, err := webauthn.FinishDiscoverableLogin(sessionData, ctx.Request())
if err != nil {
- return jsonErrorRes(403, "Failed authentication attempt for passkey", err)
+ return ctx.JsonErrorRes(403, "Failed authentication attempt for passkey", err)
}
sess.Values["user"] = userID
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
delete(sess.Values, "webauthn_login_session")
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- return json(ctx, []string{"OK"})
+ return ctx.JSON_([]string{"OK"})
}
-func beginWebAuthnAssertion(ctx echo.Context) error {
- sess := getSession(ctx)
+func beginWebAuthnAssertion(ctx *context.OGContext) error {
+ sess := ctx.GetSession()
ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint))
if err != nil {
- return jsonErrorRes(500, "Cannot get user", err)
+ return ctx.JsonErrorRes(500, "Cannot get user", err)
}
credsCreation, jsonWaSession, err := webauthn.BeginLogin(ogUser)
if err != nil {
- return jsonErrorRes(401, "Cannot begin WebAuthn login", err)
+ return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err)
}
sess.Values["webauthn_assertion_session"] = jsonWaSession
sess.Options.MaxAge = 5 * 60 // 5 minutes
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- return json(ctx, credsCreation)
+ return ctx.JSON_(credsCreation)
}
-func finishWebAuthnAssertion(ctx echo.Context) error {
- sess := getSession(ctx)
+func finishWebAuthnAssertion(ctx *context.OGContext) error {
+ sess := ctx.GetSession()
sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte)
if !ok {
- return jsonErrorRes(401, "Cannot get WebAuthn assertion session", nil)
+ return ctx.JsonErrorRes(401, "Cannot get WebAuthn assertion session", nil)
}
userId := sess.Values["mfaID"].(uint)
ogUser, err := db.GetUserById(userId)
if err != nil {
- return jsonErrorRes(500, "Cannot get user", err)
+ return ctx.JsonErrorRes(500, "Cannot get user", err)
}
if err = webauthn.FinishLogin(ogUser, sessionData, ctx.Request()); err != nil {
- return jsonErrorRes(403, "Failed authentication attempt for passkey", err)
+ return ctx.JsonErrorRes(403, "Failed authentication attempt for passkey", err)
}
sess.Values["user"] = userId
@@ -561,184 +561,184 @@ func finishWebAuthnAssertion(ctx echo.Context) error {
delete(sess.Values, "webauthn_assertion_session")
delete(sess.Values, "mfaID")
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- return json(ctx, []string{"OK"})
+ return ctx.JSON_([]string{"OK"})
}
-func beginTotp(ctx echo.Context) error {
- user := getUserLogged(ctx)
+func beginTotp(ctx *context.OGContext) error {
+ user := ctx.User
if _, hasTotp, err := user.HasMFA(); err != nil {
- return errorRes(500, "Cannot check for user MFA", err)
+ return ctx.ErrorRes(500, "Cannot check for user MFA", err)
} else if hasTotp {
- addFlash(ctx, tr(ctx, "auth.totp.already-enabled"), "error")
- return redirect(ctx, "/settings")
+ ctx.AddFlash(ctx.Tr("auth.totp.already-enabled"), "error")
+ return ctx.RedirectTo("/settings")
}
- ogUrl, err := url.Parse(getData(ctx, "baseHttpUrl").(string))
+ ogUrl, err := url.Parse(ctx.GetData("baseHttpUrl").(string))
if err != nil {
- return errorRes(500, "Cannot parse base URL", err)
+ return ctx.ErrorRes(500, "Cannot parse base URL", err)
}
- sess := getSession(ctx)
+ sess := ctx.GetSession()
generatedSecret, _ := sess.Values["generatedSecret"].([]byte)
- totpSecret, qrcode, err, generatedSecret := totp.GenerateQRCode(getUserLogged(ctx).Username, ogUrl.Hostname(), generatedSecret)
+ totpSecret, qrcode, err, generatedSecret := totp.GenerateQRCode(ctx.User.Username, ogUrl.Hostname(), generatedSecret)
if err != nil {
- return errorRes(500, "Cannot generate TOTP QR code", err)
+ return ctx.ErrorRes(500, "Cannot generate TOTP QR code", err)
}
sess.Values["totpSecret"] = totpSecret
sess.Values["generatedSecret"] = generatedSecret
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- setData(ctx, "totpSecret", totpSecret)
- setData(ctx, "totpQrcode", qrcode)
+ ctx.SetData("totpSecret", totpSecret)
+ ctx.SetData("totpQrcode", qrcode)
- return html(ctx, "totp.html")
+ return ctx.HTML_("totp.html")
}
-func finishTotp(ctx echo.Context) error {
- user := getUserLogged(ctx)
+func finishTotp(ctx *context.OGContext) error {
+ user := ctx.User
if _, hasTotp, err := user.HasMFA(); err != nil {
- return errorRes(500, "Cannot check for user MFA", err)
+ return ctx.ErrorRes(500, "Cannot check for user MFA", err)
} else if hasTotp {
- addFlash(ctx, tr(ctx, "auth.totp.already-enabled"), "error")
- return redirect(ctx, "/settings")
+ ctx.AddFlash(ctx.Tr("auth.totp.already-enabled"), "error")
+ return ctx.RedirectTo("/settings")
}
dto := &db.TOTPDTO{}
if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
if err := ctx.Validate(dto); err != nil {
- addFlash(ctx, "Invalid secret", "error")
- return redirect(ctx, "/settings/totp/generate")
+ ctx.AddFlash("Invalid secret", "error")
+ return ctx.RedirectTo("/settings/totp/generate")
}
- sess := getSession(ctx)
+ sess := ctx.GetSession()
secret, ok := sess.Values["totpSecret"].(string)
if !ok {
- return errorRes(500, "Cannot get TOTP secret from session", nil)
+ return ctx.ErrorRes(500, "Cannot get TOTP secret from session", nil)
}
if !totp.Validate(dto.Code, secret) {
- addFlash(ctx, tr(ctx, "auth.totp.invalid-code"), "error")
+ ctx.AddFlash(ctx.Tr("auth.totp.invalid-code"), "error")
- return redirect(ctx, "/settings/totp/generate")
+ return ctx.RedirectTo("/settings/totp/generate")
}
userTotp := &db.TOTP{
- UserID: getUserLogged(ctx).ID,
+ UserID: ctx.User.ID,
}
if err := userTotp.StoreSecret(secret); err != nil {
- return errorRes(500, "Cannot store TOTP secret", err)
+ return ctx.ErrorRes(500, "Cannot store TOTP secret", err)
}
if err := userTotp.Create(); err != nil {
- return errorRes(500, "Cannot create TOTP", err)
+ return ctx.ErrorRes(500, "Cannot create TOTP", err)
}
- addFlash(ctx, "TOTP successfully enabled", "success")
+ ctx.AddFlash("TOTP successfully enabled", "success")
codes, err := userTotp.GenerateRecoveryCodes()
if err != nil {
- return errorRes(500, "Cannot generate recovery codes", err)
+ return ctx.ErrorRes(500, "Cannot generate recovery codes", err)
}
delete(sess.Values, "totpSecret")
delete(sess.Values, "generatedSecret")
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- setData(ctx, "recoveryCodes", codes)
- return html(ctx, "totp.html")
+ ctx.SetData("recoveryCodes", codes)
+ return ctx.HTML_("totp.html")
}
-func assertTotp(ctx echo.Context) error {
+func assertTotp(ctx *context.OGContext) error {
var err error
dto := &db.TOTPDTO{}
if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
if err := ctx.Validate(dto); err != nil {
- addFlash(ctx, tr(ctx, "auth.totp.invalid-code"), "error")
- return redirect(ctx, "/mfa")
+ ctx.AddFlash(ctx.Tr("auth.totp.invalid-code"), "error")
+ return ctx.RedirectTo("/mfa")
}
- sess := getSession(ctx)
+ sess := ctx.GetSession()
userId := sess.Values["mfaID"].(uint)
var userTotp *db.TOTP
if userTotp, err = db.GetTOTPByUserID(userId); err != nil {
- return errorRes(500, "Cannot get TOTP by UID", err)
+ return ctx.ErrorRes(500, "Cannot get TOTP by UID", err)
}
redirectUrl := "/"
var validCode, validRecoveryCode bool
if validCode, err = userTotp.ValidateCode(dto.Code); err != nil {
- return errorRes(500, "Cannot validate TOTP code", err)
+ return ctx.ErrorRes(500, "Cannot validate TOTP code", err)
}
if !validCode {
validRecoveryCode, err = userTotp.ValidateRecoveryCode(dto.Code)
if err != nil {
- return errorRes(500, "Cannot validate TOTP code", err)
+ return ctx.ErrorRes(500, "Cannot validate TOTP code", err)
}
if !validRecoveryCode {
- addFlash(ctx, tr(ctx, "auth.totp.invalid-code"), "error")
- return redirect(ctx, "/mfa")
+ ctx.AddFlash(ctx.Tr("auth.totp.invalid-code"), "error")
+ return ctx.RedirectTo("/mfa")
}
- addFlash(ctx, tr(ctx, "auth.totp.code-used", dto.Code), "warning")
+ ctx.AddFlash(ctx.Tr("auth.totp.code-used", dto.Code), "warning")
redirectUrl = "/settings"
}
sess.Values["user"] = userId
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
delete(sess.Values, "mfaID")
- saveSession(sess, ctx)
+ ctx.SaveSession(sess)
- return redirect(ctx, redirectUrl)
+ return ctx.RedirectTo(redirectUrl)
}
-func disableTotp(ctx echo.Context) error {
- user := getUserLogged(ctx)
+func disableTotp(ctx *context.OGContext) error {
+ user := ctx.User
userTotp, err := db.GetTOTPByUserID(user.ID)
if err != nil {
- return errorRes(500, "Cannot get TOTP by UID", err)
+ return ctx.ErrorRes(500, "Cannot get TOTP by UID", err)
}
if err = userTotp.Delete(); err != nil {
- return errorRes(500, "Cannot delete TOTP", err)
+ return ctx.ErrorRes(500, "Cannot delete TOTP", err)
}
- addFlash(ctx, tr(ctx, "auth.totp.disabled"), "success")
- return redirect(ctx, "/settings")
+ ctx.AddFlash(ctx.Tr("auth.totp.disabled"), "success")
+ return ctx.RedirectTo("/settings")
}
-func regenerateTotpRecoveryCodes(ctx echo.Context) error {
- user := getUserLogged(ctx)
+func regenerateTotpRecoveryCodes(ctx *context.OGContext) error {
+ user := ctx.User
userTotp, err := db.GetTOTPByUserID(user.ID)
if err != nil {
- return errorRes(500, "Cannot get TOTP by UID", err)
+ return ctx.ErrorRes(500, "Cannot get TOTP by UID", err)
}
codes, err := userTotp.GenerateRecoveryCodes()
if err != nil {
- return errorRes(500, "Cannot generate recovery codes", err)
+ return ctx.ErrorRes(500, "Cannot generate recovery codes", err)
}
- setData(ctx, "recoveryCodes", codes)
- return html(ctx, "totp.html")
+ ctx.SetData("recoveryCodes", codes)
+ return ctx.HTML_("totp.html")
}
-func logout(ctx echo.Context) error {
- deleteSession(ctx)
- deleteCsrfCookie(ctx)
- return redirect(ctx, "/all")
+func logout(ctx *context.OGContext) error {
+ ctx.DeleteSession()
+ ctx.DeleteCsrfCookie()
+ return ctx.RedirectTo("/all")
}
func urlJoin(base string, elem ...string) string {
@@ -803,13 +803,13 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
}
type ContextAuthInfo struct {
- context echo.Context
+ context *context.OGContext
}
func (auth ContextAuthInfo) RequireLogin() (bool, error) {
- return getData(auth.context, "RequireLogin") == true, nil
+ return auth.context.GetData("RequireLogin") == true, nil
}
func (auth ContextAuthInfo) AllowGistsWithoutLogin() (bool, error) {
- return getData(auth.context, "AllowGistsWithoutLogin") == true, nil
+ return auth.context.GetData("AllowGistsWithoutLogin") == true, nil
}
diff --git a/internal/web/gist.go b/internal/web/handler/gist.go
similarity index 53%
rename from internal/web/gist.go
rename to internal/web/handler/gist.go
index f7d3564..8ec6334 100644
--- a/internal/web/gist.go
+++ b/internal/web/handler/gist.go
@@ -1,4 +1,4 @@
-package web
+package handler
import (
"archive/zip"
@@ -7,6 +7,8 @@ import (
gojson "encoding/json"
"errors"
"fmt"
+ "github.com/thomiceli/opengist/internal/web/context"
+ "github.com/thomiceli/opengist/internal/web/server"
"html/template"
"net/url"
"path/filepath"
@@ -29,37 +31,37 @@ import (
"gorm.io/gorm"
)
-func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- currUser := getUserLogged(ctx)
+func GistInit(next context.Handler) context.Handler {
+ return func(ctx *context.OGContext) error {
+ currUser := ctx.User
userName := ctx.Param("user")
gistName := ctx.Param("gistname")
switch filepath.Ext(gistName) {
case ".js":
- setData(ctx, "gistpage", "js")
+ ctx.SetData("gistpage", "js")
gistName = strings.TrimSuffix(gistName, ".js")
case ".json":
- setData(ctx, "gistpage", "json")
+ ctx.SetData("gistpage", "json")
gistName = strings.TrimSuffix(gistName, ".json")
case ".git":
- setData(ctx, "gistpage", "git")
+ ctx.SetData("gistpage", "git")
gistName = strings.TrimSuffix(gistName, ".git")
}
gist, err := db.GetGist(userName, gistName)
if err != nil {
- return notFound("Gist not found")
+ return ctx.NotFound("Gist not found")
}
if gist.Private == db.PrivateVisibility {
if currUser == nil || currUser.ID != gist.UserID {
- return notFound("Gist not found")
+ return ctx.NotFound("Gist not found")
}
}
- setData(ctx, "gist", gist)
+ ctx.SetData("gist", gist)
if config.C.SshGit {
var sshDomain string
@@ -71,93 +73,93 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
}
if config.C.SshPort == "22" {
- setData(ctx, "sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git")
+ ctx.SetData("sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git")
} else {
- setData(ctx, "sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git")
+ ctx.SetData("sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git")
}
}
- baseHttpUrl := getData(ctx, "baseHttpUrl").(string)
+ baseHttpUrl := ctx.GetData("baseHttpUrl").(string)
if config.C.HttpGit {
- setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
+ ctx.SetData("httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
}
- setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
- setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))
- setData(ctx, "embedScript", fmt.Sprintf(``, baseHttpUrl+"/"+userName+"/"+gistName+".js"))
+ ctx.SetData("httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
+ ctx.SetData("currentUrl", template.URL(ctx.Request().URL.Path))
+ ctx.SetData("embedScript", fmt.Sprintf(``, baseHttpUrl+"/"+userName+"/"+gistName+".js"))
nbCommits, err := gist.NbCommits()
if err != nil {
- return errorRes(500, "Error fetching number of commits", err)
+ return ctx.ErrorRes(500, "Error fetching number of commits", err)
}
- setData(ctx, "nbCommits", nbCommits)
+ ctx.SetData("nbCommits", nbCommits)
if currUser != nil {
hasLiked, err := currUser.HasLiked(gist)
if err != nil {
- return errorRes(500, "Cannot get user like status", err)
+ return ctx.ErrorRes(500, "Cannot get user like status", err)
}
- setData(ctx, "hasLiked", hasLiked)
+ ctx.SetData("hasLiked", hasLiked)
}
if gist.Private > 0 {
- setData(ctx, "NoIndex", true)
+ ctx.SetData("NoIndex", true)
}
return next(ctx)
}
}
-// gistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found
+// GistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found
// useful for git clients using HTTP to obfuscate the existence of a private gist
-func gistSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
+func GistSoftInit(next echo.HandlerFunc) context.Handler {
+ return func(ctx *context.OGContext) error {
userName := ctx.Param("user")
gistName := ctx.Param("gistname")
gistName = strings.TrimSuffix(gistName, ".git")
gist, _ := db.GetGist(userName, gistName)
- setData(ctx, "gist", gist)
+ ctx.SetData("gist", gist)
return next(ctx)
}
}
-// gistNewPushInit has the same behavior as gistSoftInit but create a new gist empty instead
-func gistNewPushSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- setData(c, "gist", new(db.Gist))
- return next(c)
+// GistNewPushSoftInit has the same behavior as gistSoftInit but create a new gist empty instead
+func GistNewPushSoftInit(next context.Handler) context.Handler {
+ return func(ctx *context.OGContext) error {
+ ctx.SetData("gist", new(db.Gist))
+ return next(ctx)
}
}
-func allGists(ctx echo.Context) error {
+func AllGists(ctx *context.OGContext) error {
var err error
var urlPage string
fromUserStr := ctx.Param("user")
- userLogged := getUserLogged(ctx)
+ userLogged := ctx.User
pageInt := getPage(ctx)
sort := "created"
- sortText := trH(ctx, "gist.list.sort-by-created")
+ sortText := ctx.TrH("gist.list.sort-by-created")
order := "desc"
- orderText := trH(ctx, "gist.list.order-by-desc")
+ orderText := ctx.TrH("gist.list.order-by-desc")
if ctx.QueryParam("sort") == "updated" {
sort = "updated"
- sortText = trH(ctx, "gist.list.sort-by-updated")
+ sortText = ctx.TrH("gist.list.sort-by-updated")
}
if ctx.QueryParam("order") == "asc" {
order = "asc"
- orderText = trH(ctx, "gist.list.order-by-asc")
+ orderText = ctx.TrH("gist.list.order-by-asc")
}
- setData(ctx, "sort", sortText)
- setData(ctx, "order", orderText)
+ ctx.SetData("sort", sortText)
+ ctx.SetData("order", orderText)
var gists []*db.Gist
var currentUserId uint
@@ -170,15 +172,15 @@ func allGists(ctx echo.Context) error {
if fromUserStr == "" {
urlctx := ctx.Request().URL.Path
if strings.HasSuffix(urlctx, "search") {
- setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results"))
- setData(ctx, "mode", "search")
- setData(ctx, "searchQuery", ctx.QueryParam("q"))
- setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
+ ctx.SetData("htmlTitle", ctx.TrH("gist.list.search-results"))
+ ctx.SetData("mode", "search")
+ ctx.SetData("searchQuery", ctx.QueryParam("q"))
+ ctx.SetData("searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
urlPage = "search"
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
} else if strings.HasSuffix(urlctx, "all") {
- setData(ctx, "htmlTitle", trH(ctx, "gist.list.all"))
- setData(ctx, "mode", "all")
+ ctx.SetData("htmlTitle", ctx.TrH("gist.list.all"))
+ ctx.SetData("mode", "all")
urlPage = "all"
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
}
@@ -188,12 +190,12 @@ func allGists(ctx echo.Context) error {
liked, err = regexp.MatchString(`/[^/]*/liked`, ctx.Request().URL.Path)
if err != nil {
- return errorRes(500, "Error matching regexp", err)
+ return ctx.ErrorRes(500, "Error matching regexp", err)
}
forked, err = regexp.MatchString(`/[^/]*/forked`, ctx.Request().URL.Path)
if err != nil {
- return errorRes(500, "Error matching regexp", err)
+ return ctx.ErrorRes(500, "Error matching regexp", err)
}
var fromUser *db.User
@@ -201,44 +203,44 @@ func allGists(ctx echo.Context) error {
fromUser, err = db.GetUserByUsername(fromUserStr)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
- return notFound("User not found")
+ return ctx.NotFound("User not found")
}
- return errorRes(500, "Error fetching user", err)
+ return ctx.ErrorRes(500, "Error fetching user", err)
}
- setData(ctx, "fromUser", fromUser)
+ ctx.SetData("fromUser", fromUser)
if countFromUser, err := db.CountAllGistsFromUser(fromUser.ID, currentUserId); err != nil {
- return errorRes(500, "Error counting gists", err)
+ return ctx.ErrorRes(500, "Error counting gists", err)
} else {
- setData(ctx, "countFromUser", countFromUser)
+ ctx.SetData("countFromUser", countFromUser)
}
if countLiked, err := db.CountAllGistsLikedByUser(fromUser.ID, currentUserId); err != nil {
- return errorRes(500, "Error counting liked gists", err)
+ return ctx.ErrorRes(500, "Error counting liked gists", err)
} else {
- setData(ctx, "countLiked", countLiked)
+ ctx.SetData("countLiked", countLiked)
}
if countForked, err := db.CountAllGistsForkedByUser(fromUser.ID, currentUserId); err != nil {
- return errorRes(500, "Error counting forked gists", err)
+ return ctx.ErrorRes(500, "Error counting forked gists", err)
} else {
- setData(ctx, "countForked", countForked)
+ ctx.SetData("countForked", countForked)
}
if liked {
urlPage = fromUserStr + "/liked"
- setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-liked-by", fromUserStr))
- setData(ctx, "mode", "liked")
+ ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-liked-by", fromUserStr))
+ ctx.SetData("mode", "liked")
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else if forked {
urlPage = fromUserStr + "/forked"
- setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-forked-by", fromUserStr))
- setData(ctx, "mode", "forked")
+ ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-forked-by", fromUserStr))
+ ctx.SetData("mode", "forked")
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else {
urlPage = fromUserStr
- setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-from", fromUserStr))
- setData(ctx, "mode", "fromUser")
+ ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-from", fromUserStr))
+ ctx.SetData("mode", "fromUser")
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
}
}
@@ -253,25 +255,25 @@ func allGists(ctx echo.Context) error {
}
if err != nil {
- return errorRes(500, "Error fetching gists", err)
+ return ctx.ErrorRes(500, "Error fetching gists", err)
}
if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
- return errorRes(404, tr(ctx, "error.page-not-found"), nil)
+ return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
}
- setData(ctx, "urlPage", urlPage)
- return html(ctx, "all.html")
+ ctx.SetData("urlPage", urlPage)
+ return ctx.HTML_("all.html")
}
-func search(ctx echo.Context) error {
+func Search(ctx *context.OGContext) error {
var err error
- content, meta := parseSearchQueryStr(ctx.QueryParam("q"))
+ content, meta := ParseSearchQueryStr(ctx.QueryParam("q"))
pageInt := getPage(ctx)
var currentUserId uint
- userLogged := getUserLogged(ctx)
+ userLogged := ctx.User
if userLogged != nil {
currentUserId = userLogged.ID
} else {
@@ -281,7 +283,7 @@ func search(ctx echo.Context) error {
var visibleGistsIds []uint
visibleGistsIds, err = db.GetAllGistsVisibleByUser(currentUserId)
if err != nil {
- return errorRes(500, "Error fetching gists", err)
+ return ctx.ErrorRes(500, "Error fetching gists", err)
}
gistsIds, nbHits, langs, err := index.SearchGists(content, index.SearchGistMetadata{
@@ -292,12 +294,12 @@ func search(ctx echo.Context) error {
Language: meta["language"],
}, visibleGistsIds, pageInt)
if err != nil {
- return errorRes(500, "Error searching gists", err)
+ return ctx.ErrorRes(500, "Error searching gists", err)
}
gists, err := db.GetAllGistsByIds(gistsIds)
if err != nil {
- return errorRes(500, "Error fetching gists", err)
+ return ctx.ErrorRes(500, "Error fetching gists", err)
}
renderedGists := make([]*render.RenderedGist, 0, len(gists))
@@ -310,31 +312,31 @@ func search(ctx echo.Context) error {
}
if pageInt > 1 && len(renderedGists) != 0 {
- setData(ctx, "prevPage", pageInt-1)
+ ctx.SetData("prevPage", pageInt-1)
}
if 10*pageInt < int(nbHits) {
- setData(ctx, "nextPage", pageInt+1)
+ ctx.SetData("nextPage", pageInt+1)
}
- setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
- setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
- setData(ctx, "urlPage", "search")
- setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q")))
- setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results"))
- setData(ctx, "nbHits", nbHits)
- setData(ctx, "gists", renderedGists)
- setData(ctx, "langs", langs)
- setData(ctx, "searchQuery", ctx.QueryParam("q"))
- return html(ctx, "search.html")
+ ctx.SetData("prevLabel", ctx.TrH("pagination.previous"))
+ ctx.SetData("nextLabel", ctx.TrH("pagination.next"))
+ ctx.SetData("urlPage", "search")
+ ctx.SetData("urlParams", template.URL("&q="+ctx.QueryParam("q")))
+ ctx.SetData("htmlTitle", ctx.TrH("gist.list.search-results"))
+ ctx.SetData("nbHits", nbHits)
+ ctx.SetData("gists", renderedGists)
+ ctx.SetData("langs", langs)
+ ctx.SetData("searchQuery", ctx.QueryParam("q"))
+ return ctx.HTML_("search.html")
}
-func gistIndex(ctx echo.Context) error {
- if getData(ctx, "gistpage") == "js" {
- return gistJs(ctx)
- } else if getData(ctx, "gistpage") == "json" {
- return gistJson(ctx)
+func GistIndex(ctx *context.OGContext) error {
+ if ctx.GetData("gistpage") == "js" {
+ return GistJs(ctx)
+ } else if ctx.GetData("gistpage") == "json" {
+ return GistJson(ctx)
}
- gist := getData(ctx, "gist").(*db.Gist)
+ gist := ctx.GetData("gist").(*db.Gist)
revision := ctx.Param("revision")
if revision == "" {
@@ -343,46 +345,46 @@ func gistIndex(ctx echo.Context) error {
files, err := gist.Files(revision, true)
if _, ok := err.(*git.RevisionNotFoundError); ok {
- return notFound("Revision not found")
+ return ctx.NotFound("Revision not found")
} else if err != nil {
- return errorRes(500, "Error fetching files", err)
+ return ctx.ErrorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
- setData(ctx, "page", "code")
- setData(ctx, "commit", revision)
- setData(ctx, "files", renderedFiles)
- setData(ctx, "revision", revision)
- setData(ctx, "htmlTitle", gist.Title)
- return html(ctx, "gist.html")
+ ctx.SetData("page", "code")
+ ctx.SetData("commit", revision)
+ ctx.SetData("files", renderedFiles)
+ ctx.SetData("revision", revision)
+ ctx.SetData("htmlTitle", gist.Title)
+ return ctx.HTML_("gist.html")
}
-func gistJson(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func GistJson(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
files, err := gist.Files("HEAD", true)
if err != nil {
- return errorRes(500, "Error fetching files", err)
+ return ctx.ErrorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
- setData(ctx, "files", renderedFiles)
+ ctx.SetData("files", renderedFiles)
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
- if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
+ if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", ctx.DataMap(), ctx); err != nil {
return err
}
_ = w.Flush()
- jsUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
+ jsUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
if err != nil {
- return errorRes(500, "Error joining js url", err)
+ return ctx.ErrorRes(500, "Error joining js url", err)
}
- cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
+ cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), server.ManifestEntries["embed.css"].File)
if err != nil {
- return errorRes(500, "Error joining css url", err)
+ return ctx.ErrorRes(500, "Error joining css url", err)
}
return ctx.JSON(200, map[string]interface{}{
@@ -403,42 +405,42 @@ func gistJson(ctx echo.Context) error {
})
}
-func gistJs(ctx echo.Context) error {
+func GistJs(ctx *context.OGContext) error {
if _, exists := ctx.QueryParams()["dark"]; exists {
- setData(ctx, "dark", "dark")
+ ctx.SetData("dark", "dark")
}
- gist := getData(ctx, "gist").(*db.Gist)
+ gist := ctx.GetData("gist").(*db.Gist)
files, err := gist.Files("HEAD", true)
if err != nil {
- return errorRes(500, "Error fetching files", err)
+ return ctx.ErrorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
- setData(ctx, "files", renderedFiles)
+ ctx.SetData("files", renderedFiles)
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
- if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
+ if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", ctx.DataMap(), ctx); err != nil {
return err
}
_ = w.Flush()
- cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
+ cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), server.ManifestEntries["embed.css"].File)
if err != nil {
- return errorRes(500, "Error joining css url", err)
+ return ctx.ErrorRes(500, "Error joining css url", err)
}
js, err := escapeJavaScriptContent(htmlbuf.String(), cssUrl)
if err != nil {
- return errorRes(500, "Error escaping JavaScript content", err)
+ return ctx.ErrorRes(500, "Error escaping JavaScript content", err)
}
ctx.Response().Header().Set("Content-Type", "application/javascript")
- return plainText(ctx, 200, js)
+ return ctx.PlainText(200, js)
}
-func revisions(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func Revisions(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
userName := gist.User.Username
gistName := gist.Identifier()
@@ -446,11 +448,11 @@ func revisions(ctx echo.Context) error {
commits, err := gist.Log((pageInt - 1) * 10)
if err != nil {
- return errorRes(500, "Error fetching commits log", err)
+ return ctx.ErrorRes(500, "Error fetching commits log", err)
}
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
- return errorRes(404, tr(ctx, "error.page-not-found"), nil)
+ return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
}
emailsSet := map[string]struct{}{}
@@ -463,23 +465,23 @@ func revisions(ctx echo.Context) error {
emailsUsers, err := db.GetUsersFromEmails(emailsSet)
if err != nil {
- return errorRes(500, "Error fetching users emails", err)
+ return ctx.ErrorRes(500, "Error fetching users emails", err)
}
- setData(ctx, "page", "revisions")
- setData(ctx, "revision", "HEAD")
- setData(ctx, "emails", emailsUsers)
- setData(ctx, "htmlTitle", trH(ctx, "gist.revision-of", gist.Title))
+ ctx.SetData("page", "revisions")
+ ctx.SetData("revision", "HEAD")
+ ctx.SetData("emails", emailsUsers)
+ ctx.SetData("htmlTitle", ctx.TrH("gist.revision-of", gist.Title))
- return html(ctx, "revisions.html")
+ return ctx.HTML_("revisions.html")
}
-func create(ctx echo.Context) error {
- setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
- return html(ctx, "create.html")
+func Create(ctx *context.OGContext) error {
+ ctx.SetData("htmlTitle", ctx.TrH("gist.new.create-a-new-gist"))
+ return ctx.HTML_("create.html")
}
-func processCreate(ctx echo.Context) error {
+func ProcessCreate(ctx *context.OGContext) error {
isCreate := false
if ctx.Request().URL.Path == "/" {
isCreate = true
@@ -487,21 +489,21 @@ func processCreate(ctx echo.Context) error {
err := ctx.Request().ParseForm()
if err != nil {
- return errorRes(400, tr(ctx, "error.bad-request"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.bad-request"), err)
}
dto := new(db.GistDTO)
var gist *db.Gist
if isCreate {
- setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
+ ctx.SetData("htmlTitle", ctx.TrH("gist.new.create-a-new-gist"))
} else {
- gist = getData(ctx, "gist").(*db.Gist)
- setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title))
+ gist = ctx.GetData("gist").(*db.Gist)
+ ctx.SetData("htmlTitle", ctx.TrH("gist.edit.edit-gist", gist.Title))
}
if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
dto.Files = make([]db.FileDTO, 0)
@@ -517,7 +519,7 @@ func processCreate(ctx echo.Context) error {
escapedValue, err := url.QueryUnescape(content)
if err != nil {
- return errorRes(400, tr(ctx, "error.invalid-character-unescaped"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.invalid-character-unescaped"), err)
}
dto.Files = append(dto.Files, db.FileDTO{
@@ -528,16 +530,16 @@ func processCreate(ctx echo.Context) error {
err = ctx.Validate(dto)
if err != nil {
- addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
+ ctx.AddFlash(utils.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
if isCreate {
- return html(ctx, "create.html")
+ return ctx.HTML_("create.html")
} else {
files, err := gist.Files("HEAD", false)
if err != nil {
- return errorRes(500, "Error fetching files", err)
+ return ctx.ErrorRes(500, "Error fetching files", err)
}
- setData(ctx, "files", files)
- return html(ctx, "edit.html")
+ ctx.SetData("files", files)
+ return ctx.HTML_("edit.html")
}
}
@@ -547,13 +549,13 @@ func processCreate(ctx echo.Context) error {
gist = dto.ToExistingGist(gist)
}
- user := getUserLogged(ctx)
+ user := ctx.User
gist.NbFiles = len(dto.Files)
if isCreate {
uuidGist, err := uuid.NewRandom()
if err != nil {
- return errorRes(500, "Error creating an UUID", err)
+ return ctx.ErrorRes(500, "Error creating an UUID", err)
}
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
@@ -581,104 +583,104 @@ func processCreate(ctx echo.Context) error {
}
if err = gist.InitRepository(); err != nil {
- return errorRes(500, "Error creating the repository", err)
+ return ctx.ErrorRes(500, "Error creating the repository", err)
}
if err = gist.AddAndCommitFiles(&dto.Files); err != nil {
- return errorRes(500, "Error adding and committing files", err)
+ return ctx.ErrorRes(500, "Error adding and committing files", err)
}
if isCreate {
if err = gist.Create(); err != nil {
- return errorRes(500, "Error creating the gist", err)
+ return ctx.ErrorRes(500, "Error creating the gist", err)
}
} else {
if err = gist.Update(); err != nil {
- return errorRes(500, "Error updating the gist", err)
+ return ctx.ErrorRes(500, "Error updating the gist", err)
}
}
gist.AddInIndex()
- return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
+ return ctx.RedirectTo("/" + user.Username + "/" + gist.Identifier())
}
-func editVisibility(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func EditVisibility(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
dto := new(db.VisibilityDTO)
if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
gist.Private = dto.Private
if err := gist.UpdateNoTimestamps(); err != nil {
- return errorRes(500, "Error updating this gist", err)
+ return ctx.ErrorRes(500, "Error updating this gist", err)
}
- addFlash(ctx, tr(ctx, "flash.gist.visibility-changed"), "success")
- return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
+ ctx.AddFlash(ctx.Tr("flash.gist.visibility-changed"), "success")
+ return ctx.RedirectTo("/" + gist.User.Username + "/" + gist.Identifier())
}
-func deleteGist(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func DeleteGist(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
if err := gist.Delete(); err != nil {
- return errorRes(500, "Error deleting this gist", err)
+ return ctx.ErrorRes(500, "Error deleting this gist", err)
}
gist.RemoveFromIndex()
- addFlash(ctx, tr(ctx, "flash.gist.deleted"), "success")
- return redirect(ctx, "/")
+ ctx.AddFlash(ctx.Tr("flash.gist.deleted"), "success")
+ return ctx.RedirectTo("/")
}
-func like(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
- currentUser := getUserLogged(ctx)
+func Like(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
+ currentUser := ctx.User
hasLiked, err := currentUser.HasLiked(gist)
if err != nil {
- return errorRes(500, "Error checking if user has liked a gist", err)
+ return ctx.ErrorRes(500, "Error checking if user has liked a gist", err)
}
if hasLiked {
- err = gist.RemoveUserLike(getUserLogged(ctx))
+ err = gist.RemoveUserLike(ctx.User)
} else {
- err = gist.AppendUserLike(getUserLogged(ctx))
+ err = gist.AppendUserLike(ctx.User)
}
if err != nil {
- return errorRes(500, "Error liking/dislking this gist", err)
+ return ctx.ErrorRes(500, "Error liking/dislking this gist", err)
}
redirectTo := "/" + gist.User.Username + "/" + gist.Identifier()
if r := ctx.QueryParam("redirecturl"); r != "" {
redirectTo = r
}
- return redirect(ctx, redirectTo)
+ return ctx.RedirectTo(redirectTo)
}
-func fork(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
- currentUser := getUserLogged(ctx)
+func Fork(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
+ currentUser := ctx.User
alreadyForked, err := gist.GetForkParent(currentUser)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- return errorRes(500, "Error checking if gist is already forked", err)
+ return ctx.ErrorRes(500, "Error checking if gist is already forked", err)
}
if gist.User.ID == currentUser.ID {
- addFlash(ctx, tr(ctx, "flash.gist.fork-own-gist"), "error")
- return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
+ ctx.AddFlash(ctx.Tr("flash.gist.fork-own-gist"), "error")
+ return ctx.RedirectTo("/" + gist.User.Username + "/" + gist.Identifier())
}
if alreadyForked.ID != 0 {
- return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Identifier())
+ return ctx.RedirectTo("/" + alreadyForked.User.Username + "/" + alreadyForked.Identifier())
}
uuidGist, err := uuid.NewRandom()
if err != nil {
- return errorRes(500, "Error creating an UUID", err)
+ return ctx.ErrorRes(500, "Error creating an UUID", err)
}
newGist := &db.Gist{
@@ -694,44 +696,44 @@ func fork(ctx echo.Context) error {
}
if err = newGist.CreateForked(); err != nil {
- return errorRes(500, "Error forking the gist in database", err)
+ return ctx.ErrorRes(500, "Error forking the gist in database", err)
}
if err = gist.ForkClone(currentUser.Username, newGist.Uuid); err != nil {
- return errorRes(500, "Error cloning the repository while forking", err)
+ return ctx.ErrorRes(500, "Error cloning the repository while forking", err)
}
if err = gist.IncrementForkCount(); err != nil {
- return errorRes(500, "Error incrementing the fork count", err)
+ return ctx.ErrorRes(500, "Error incrementing the fork count", err)
}
- addFlash(ctx, tr(ctx, "flash.gist.forked"), "success")
+ ctx.AddFlash(ctx.Tr("flash.gist.forked"), "success")
- return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
+ return ctx.RedirectTo("/" + currentUser.Username + "/" + newGist.Identifier())
}
-func rawFile(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func RawFile(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
if err != nil {
- return errorRes(500, "Error getting file content", err)
+ return ctx.ErrorRes(500, "Error getting file content", err)
}
if file == nil {
- return notFound("File not found")
+ return ctx.NotFound("File not found")
}
- return plainText(ctx, 200, file.Content)
+ return ctx.PlainText(200, file.Content)
}
-func downloadFile(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func DownloadFile(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
if err != nil {
- return errorRes(500, "Error getting file content", err)
+ return ctx.ErrorRes(500, "Error getting file content", err)
}
if file == nil {
- return notFound("File not found")
+ return ctx.NotFound("File not found")
}
ctx.Response().Header().Set("Content-Type", "text/plain")
@@ -739,36 +741,36 @@ func downloadFile(ctx echo.Context) error {
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
_, err = ctx.Response().Write([]byte(file.Content))
if err != nil {
- return errorRes(500, "Error downloading the file", err)
+ return ctx.ErrorRes(500, "Error downloading the file", err)
}
return nil
}
-func edit(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func Edit(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
files, err := gist.Files("HEAD", false)
if err != nil {
- return errorRes(500, "Error fetching files from repository", err)
+ return ctx.ErrorRes(500, "Error fetching files from repository", err)
}
- setData(ctx, "files", files)
- setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title))
+ ctx.SetData("files", files)
+ ctx.SetData("htmlTitle", ctx.TrH("gist.edit.edit-gist", gist.Title))
- return html(ctx, "edit.html")
+ return ctx.HTML_("edit.html")
}
-func downloadZip(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func DownloadZip(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
revision := ctx.Param("revision")
files, err := gist.Files(revision, false)
if err != nil {
- return errorRes(500, "Error fetching files from repository", err)
+ return ctx.ErrorRes(500, "Error fetching files from repository", err)
}
if len(files) == 0 {
- return notFound("No files found in this revision")
+ return ctx.NotFound("No files found in this revision")
}
zipFile := new(bytes.Buffer)
@@ -782,16 +784,16 @@ func downloadZip(ctx echo.Context) error {
}
f, err := zipWriter.CreateHeader(fh)
if err != nil {
- return errorRes(500, "Error adding a file the to the zip archive", err)
+ return ctx.ErrorRes(500, "Error adding a file the to the zip archive", err)
}
_, err = f.Write([]byte(file.Content))
if err != nil {
- return errorRes(500, "Error adding file content the to the zip archive", err)
+ return ctx.ErrorRes(500, "Error adding file content the to the zip archive", err)
}
}
err = zipWriter.Close()
if err != nil {
- return errorRes(500, "Error closing the zip archive", err)
+ return ctx.ErrorRes(500, "Error closing the zip archive", err)
}
ctx.Response().Header().Set("Content-Type", "application/zip")
@@ -799,35 +801,35 @@ func downloadZip(ctx echo.Context) error {
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
_, err = ctx.Response().Write(zipFile.Bytes())
if err != nil {
- return errorRes(500, "Error writing the zip archive", err)
+ return ctx.ErrorRes(500, "Error writing the zip archive", err)
}
return nil
}
-func likes(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func Likes(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
pageInt := getPage(ctx)
likers, err := gist.GetUsersLikes(pageInt - 1)
if err != nil {
- return errorRes(500, "Error getting users who liked this gist", err)
+ return ctx.ErrorRes(500, "Error getting users who liked this gist", err)
}
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil {
- return errorRes(404, tr(ctx, "error.page-not-found"), nil)
+ return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
}
- setData(ctx, "htmlTitle", trH(ctx, "gist.likes.for", gist.Title))
- setData(ctx, "revision", "HEAD")
- return html(ctx, "likes.html")
+ ctx.SetData("htmlTitle", ctx.TrH("gist.likes.for", gist.Title))
+ ctx.SetData("revision", "HEAD")
+ return ctx.HTML_("likes.html")
}
-func forks(ctx echo.Context) error {
- gist := getData(ctx, "gist").(*db.Gist)
+func Forks(ctx *context.OGContext) error {
+ gist := ctx.GetData("gist").(*db.Gist)
pageInt := getPage(ctx)
- currentUser := getUserLogged(ctx)
+ currentUser := ctx.User
var fromUserID uint = 0
if currentUser != nil {
fromUserID = currentUser.ID
@@ -835,63 +837,63 @@ func forks(ctx echo.Context) error {
forks, err := gist.GetForks(fromUserID, pageInt-1)
if err != nil {
- return errorRes(500, "Error getting users who liked this gist", err)
+ return ctx.ErrorRes(500, "Error getting users who liked this gist", err)
}
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil {
- return errorRes(404, tr(ctx, "error.page-not-found"), nil)
+ return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
}
- setData(ctx, "htmlTitle", trH(ctx, "gist.forks.for", gist.Title))
- setData(ctx, "revision", "HEAD")
- return html(ctx, "forks.html")
+ ctx.SetData("htmlTitle", ctx.TrH("gist.forks.for", gist.Title))
+ ctx.SetData("revision", "HEAD")
+ return ctx.HTML_("forks.html")
}
-func checkbox(ctx echo.Context) error {
+func Checkbox(ctx *context.OGContext) error {
filename := ctx.FormValue("file")
checkboxNb := ctx.FormValue("checkbox")
i, err := strconv.Atoi(checkboxNb)
if err != nil {
- return errorRes(400, tr(ctx, "error.invalid-number"), nil)
+ return ctx.ErrorRes(400, ctx.Tr("error.invalid-number"), nil)
}
- gist := getData(ctx, "gist").(*db.Gist)
+ gist := ctx.GetData("gist").(*db.Gist)
file, err := gist.File("HEAD", filename, false)
if err != nil {
- return errorRes(500, "Error getting file content", err)
+ return ctx.ErrorRes(500, "Error getting file content", err)
} else if file == nil {
- return notFound("File not found")
+ return ctx.NotFound("File not found")
}
markdown, err := render.Checkbox(file.Content, i)
if err != nil {
- return errorRes(500, "Error checking checkbox", err)
+ return ctx.ErrorRes(500, "Error checking checkbox", err)
}
if err = gist.AddAndCommitFile(&db.FileDTO{
Filename: filename,
Content: markdown,
}); err != nil {
- return errorRes(500, "Error adding and committing files", err)
+ return ctx.ErrorRes(500, "Error adding and committing files", err)
}
if err = gist.UpdatePreviewAndCount(true); err != nil {
- return errorRes(500, "Error updating the gist", err)
+ return ctx.ErrorRes(500, "Error updating the gist", err)
}
- return plainText(ctx, 200, "ok")
+ return ctx.PlainText(200, "ok")
}
-func preview(ctx echo.Context) error {
+func Preview(ctx *context.OGContext) error {
content := ctx.FormValue("content")
previewStr, err := render.MarkdownString(content)
if err != nil {
- return errorRes(500, "Error rendering markdown", err)
+ return ctx.ErrorRes(500, "Error rendering markdown", err)
}
- return plainText(ctx, 200, previewStr)
+ return ctx.PlainText(200, previewStr)
}
func escapeJavaScriptContent(htmlContent, cssUrl string) (string, error) {
diff --git a/internal/web/git_http.go b/internal/web/handler/git_http.go
similarity index 74%
rename from internal/web/git_http.go
rename to internal/web/handler/git_http.go
index a0341ff..278620d 100644
--- a/internal/web/git_http.go
+++ b/internal/web/handler/git_http.go
@@ -1,4 +1,4 @@
-package web
+package handler
import (
"bytes"
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/utils"
+ "github.com/thomiceli/opengist/internal/web/context"
"net/http"
"os"
"os/exec"
@@ -17,7 +18,6 @@ import (
"time"
"github.com/google/uuid"
- "github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/db"
@@ -29,7 +29,7 @@ import (
var routes = []struct {
gitUrl string
method string
- handler func(ctx echo.Context) error
+ handler func(ctx *context.OGContext) error
}{
{"(.*?)/git-upload-pack$", "POST", uploadPack},
{"(.*?)/git-receive-pack$", "POST", receivePack},
@@ -44,7 +44,7 @@ var routes = []struct {
{"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$", "GET", idxFile},
}
-func gitHttp(ctx echo.Context) 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 {
@@ -52,7 +52,7 @@ func gitHttp(ctx echo.Context) error {
continue
}
- gist := getData(ctx, "gist").(*db.Gist)
+ gist := ctx.GetData("gist").(*db.Gist)
isInit := strings.HasPrefix(ctx.Request().URL.Path, "/init/info/refs")
isInitReceive := strings.HasPrefix(ctx.Request().URL.Path, "/init/git-receive-pack")
@@ -65,11 +65,11 @@ func gitHttp(ctx echo.Context) error {
if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
if err != nil {
log.Info().Err(err).Msg("Repository directory does not exist")
- return errorRes(404, "Repository directory does not exist", err)
+ return ctx.ErrorRes(404, "Repository directory does not exist", err)
}
}
- setData(ctx, "repositoryPath", repositoryPath)
+ ctx.SetData("repositoryPath", repositoryPath)
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
if err != nil {
@@ -102,7 +102,7 @@ func gitHttp(ctx echo.Context) error {
if !isInit && !isInitReceive {
if gist.ID == 0 {
- return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
+ return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
}
var userToCheckPermissions *db.User
@@ -114,27 +114,27 @@ func gitHttp(ctx echo.Context) error {
if ok, err := utils.Argon2id.Verify(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
- return errorRes(500, "Cannot verify password", err)
+ return ctx.ErrorRes(500, "Cannot verify password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
- return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
+ return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
}
} else {
var user *db.User
if user, err = db.GetUserByUsername(authUsername); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
- return errorRes(500, "Cannot get user", err)
+ return ctx.ErrorRes(500, "Cannot get user", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
- return errorRes(401, "Invalid credentials", nil)
+ return ctx.ErrorRes(401, "Invalid credentials", nil)
}
if ok, err := utils.Argon2id.Verify(authPassword, user.Password); !ok {
if err != nil {
- return errorRes(500, "Cannot check for password", err)
+ return ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
- return errorRes(401, "Invalid credentials", nil)
+ return ctx.ErrorRes(401, "Invalid credentials", nil)
}
if isInit {
@@ -143,56 +143,56 @@ func gitHttp(ctx echo.Context) error {
gist.User = *user
uuidGist, err := uuid.NewRandom()
if err != nil {
- return errorRes(500, "Error creating an UUID", err)
+ return ctx.ErrorRes(500, "Error creating an UUID", err)
}
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
gist.Title = "gist:" + gist.Uuid
if err = gist.InitRepository(); err != nil {
- return errorRes(500, "Cannot init repository in the file system", err)
+ return ctx.ErrorRes(500, "Cannot init repository in the file system", err)
}
if err = gist.Create(); err != nil {
- return errorRes(500, "Cannot init repository in database", err)
+ return ctx.ErrorRes(500, "Cannot init repository in database", err)
}
if err := memdb.InsertGistInit(user.ID, gist); err != nil {
- return errorRes(500, "Cannot save the URL for the new Gist", err)
+ return ctx.ErrorRes(500, "Cannot save the URL for the new Gist", err)
}
- setData(ctx, "gist", gist)
+ ctx.SetData("gist", gist)
} else {
gistFromMemdb, err := memdb.GetGistInitAndDelete(user.ID)
if err != nil {
- return errorRes(500, "Cannot get the gist link from the in memory database", err)
+ return ctx.ErrorRes(500, "Cannot get the gist link from the in memory database", err)
}
gist := gistFromMemdb.Gist
- setData(ctx, "gist", gist)
- setData(ctx, "repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
+ ctx.SetData("gist", gist)
+ ctx.SetData("repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
}
}
return route.handler(ctx)
}
}
- return notFound("Gist not found")
+ return ctx.NotFound("Gist not found")
}
-func uploadPack(ctx echo.Context) error {
+func uploadPack(ctx *context.OGContext) error {
return pack(ctx, "upload-pack")
}
-func receivePack(ctx echo.Context) error {
+func receivePack(ctx *context.OGContext) error {
return pack(ctx, "receive-pack")
}
-func pack(ctx echo.Context, serviceType string) error {
+func pack(ctx *context.OGContext, serviceType string) error {
noCacheHeaders(ctx)
defer ctx.Request().Body.Close()
if ctx.Request().Header.Get("Content-Type") != "application/x-git-"+serviceType+"-request" {
- return errorRes(401, "Git client unsupported", nil)
+ return ctx.ErrorRes(401, "Git client unsupported", nil)
}
ctx.Response().Header().Set("Content-Type", "application/x-git-"+serviceType+"-result")
@@ -202,12 +202,12 @@ func pack(ctx echo.Context, serviceType string) error {
if ctx.Request().Header.Get("Content-Encoding") == "gzip" {
reqBody, err = gzip.NewReader(reqBody)
if err != nil {
- return errorRes(500, "Cannot create gzip reader", err)
+ return ctx.ErrorRes(500, "Cannot create gzip reader", err)
}
}
- repositoryPath := getData(ctx, "repositoryPath").(string)
- gist := getData(ctx, "gist").(*db.Gist)
+ repositoryPath := ctx.GetData("repositoryPath").(string)
+ gist := ctx.GetData("gist").(*db.Gist)
var stderr bytes.Buffer
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
@@ -220,17 +220,17 @@ func pack(ctx echo.Context, serviceType string) error {
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_ID="+strconv.Itoa(int(gist.ID)))
if err = cmd.Run(); err != nil {
- return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err)
+ return ctx.ErrorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err)
}
return nil
}
-func infoRefs(ctx echo.Context) error {
+func infoRefs(ctx *context.OGContext) error {
noCacheHeaders(ctx)
var service string
- gist := getData(ctx, "gist").(*db.Gist)
+ gist := ctx.GetData("gist").(*db.Gist)
serviceType := ctx.QueryParam("service")
if strings.HasPrefix(serviceType, "git-") {
@@ -239,14 +239,14 @@ func infoRefs(ctx echo.Context) error {
if service != "upload-pack" && service != "receive-pack" {
if err := gist.UpdateServerInfo(); err != nil {
- return errorRes(500, "Cannot update server info", err)
+ return ctx.ErrorRes(500, "Cannot update server info", err)
}
return sendFile(ctx, "text/plain; charset=utf-8")
}
refs, err := gist.RPC(service)
if err != nil {
- return errorRes(500, "Cannot run git "+service, err)
+ return ctx.ErrorRes(500, "Cannot run git "+service, err)
}
ctx.Response().Header().Set("Content-Type", "application/x-git-"+service+"-advertisement")
@@ -258,38 +258,38 @@ func infoRefs(ctx echo.Context) error {
return nil
}
-func textFile(ctx echo.Context) error {
+func textFile(ctx *context.OGContext) error {
noCacheHeaders(ctx)
return sendFile(ctx, "text/plain")
}
-func infoPacks(ctx echo.Context) error {
+func infoPacks(ctx *context.OGContext) error {
cacheHeadersForever(ctx)
return sendFile(ctx, "text/plain; charset=utf-8")
}
-func looseObject(ctx echo.Context) error {
+func looseObject(ctx *context.OGContext) error {
cacheHeadersForever(ctx)
return sendFile(ctx, "application/x-git-loose-object")
}
-func packFile(ctx echo.Context) error {
+func packFile(ctx *context.OGContext) error {
cacheHeadersForever(ctx)
return sendFile(ctx, "application/x-git-packed-objects")
}
-func idxFile(ctx echo.Context) error {
+func idxFile(ctx *context.OGContext) error {
cacheHeadersForever(ctx)
return sendFile(ctx, "application/x-git-packed-objects-toc")
}
-func noCacheHeaders(ctx echo.Context) {
+func noCacheHeaders(ctx *context.OGContext) {
ctx.Response().Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 UTC")
ctx.Response().Header().Set("Pragma", "no-cache")
ctx.Response().Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
}
-func cacheHeadersForever(ctx echo.Context) {
+func cacheHeadersForever(ctx *context.OGContext) {
now := time.Now().Unix()
expires := now + 31536000
ctx.Response().Header().Set("Date", fmt.Sprintf("%d", now))
@@ -297,9 +297,9 @@ func cacheHeadersForever(ctx echo.Context) {
ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000")
}
-func basicAuth(ctx echo.Context) error {
+func basicAuth(ctx *context.OGContext) error {
ctx.Response().Header().Set("WWW-Authenticate", `Basic realm="."`)
- return plainText(ctx, 401, "Requires authentication")
+ return ctx.PlainText(401, "Requires authentication")
}
func basicAuthDecode(encoded string) (string, string, error) {
@@ -312,12 +312,12 @@ func basicAuthDecode(encoded string) (string, string, error) {
return auth[0], auth[1], nil
}
-func sendFile(ctx echo.Context, contentType string) error {
+func sendFile(ctx *context.OGContext, contentType string) error {
gitFile := "/" + strings.Join(strings.Split(ctx.Request().URL.Path, "/")[3:], "/")
- gitFile = path.Join(getData(ctx, "repositoryPath").(string), gitFile)
+ gitFile = path.Join(ctx.GetData("repositoryPath").(string), gitFile)
fi, err := os.Stat(gitFile)
if os.IsNotExist(err) {
- return errorRes(404, "File not found", nil)
+ return ctx.ErrorRes(404, "File not found", nil)
}
ctx.Response().Header().Set("Content-Type", contentType)
ctx.Response().Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
diff --git a/internal/web/healthcheck.go b/internal/web/handler/healthcheck.go
similarity index 76%
rename from internal/web/healthcheck.go
rename to internal/web/handler/healthcheck.go
index 6927d28..0ae6953 100644
--- a/internal/web/healthcheck.go
+++ b/internal/web/handler/healthcheck.go
@@ -1,12 +1,12 @@
-package web
+package handler
import (
- "github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/db"
+ "github.com/thomiceli/opengist/internal/web/context"
"time"
)
-func healthcheck(ctx echo.Context) error {
+func healthcheck(ctx *context.OGContext) error {
// Check database connection
dbOk := "ok"
httpStatus := 200
@@ -26,6 +26,6 @@ func healthcheck(ctx echo.Context) error {
// metrics is a dummy handler to satisfy the /metrics endpoint (for Prometheus, Openmetrics, etc.)
// until we have a proper metrics endpoint
-func metrics(ctx echo.Context) 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
new file mode 100644
index 0000000..ea18541
--- /dev/null
+++ b/internal/web/handler/settings.go
@@ -0,0 +1,227 @@
+package handler
+
+import (
+ "crypto/md5"
+ "fmt"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/git"
+ "github.com/thomiceli/opengist/internal/i18n"
+ "github.com/thomiceli/opengist/internal/utils"
+ "github.com/thomiceli/opengist/internal/web/context"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/thomiceli/opengist/internal/db"
+ "golang.org/x/crypto/ssh"
+)
+
+func userSettings(ctx *context.OGContext) error {
+ user := ctx.User
+
+ keys, err := db.GetSSHKeysByUserID(user.ID)
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot get SSH keys", err)
+ }
+
+ passkeys, err := db.GetAllCredentialsForUser(user.ID)
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot get WebAuthn credentials", err)
+ }
+
+ _, hasTotp, err := user.HasMFA()
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot get MFA status", err)
+ }
+
+ ctx.SetData("email", user.Email)
+ ctx.SetData("sshKeys", keys)
+ ctx.SetData("passkeys", passkeys)
+ ctx.SetData("hasTotp", hasTotp)
+ ctx.SetData("hasPassword", user.Password != "")
+ ctx.SetData("disableForm", ctx.GetData("DisableLoginForm"))
+ ctx.SetData("htmlTitle", ctx.TrH("settings"))
+ return ctx.HTML_("settings.html")
+}
+
+func emailProcess(ctx *context.OGContext) error {
+ user := ctx.User
+ email := ctx.FormValue("email")
+ var hash string
+
+ if email == "" {
+ // generate random md5 string
+ hash = fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String())))
+ } else {
+ hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
+ }
+
+ user.Email = strings.ToLower(email)
+ user.MD5Hash = hash
+
+ if err := user.Update(); err != nil {
+ return ctx.ErrorRes(500, "Cannot update email", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.user.email-updated"), "success")
+ return ctx.RedirectTo("/settings")
+}
+
+func accountDeleteProcess(ctx *context.OGContext) error {
+ user := ctx.User
+
+ if err := user.Delete(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete this user", err)
+ }
+
+ return ctx.RedirectTo("/all")
+}
+
+func sshKeysProcess(ctx *context.OGContext) error {
+ user := ctx.User
+
+ dto := new(db.SSHKeyDTO)
+ if err := ctx.Bind(dto); err != nil {
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
+ }
+
+ if err := ctx.Validate(dto); err != nil {
+ ctx.AddFlash(utils.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
+ return ctx.RedirectTo("/settings")
+ }
+ key := dto.ToSSHKey()
+
+ key.UserID = user.ID
+
+ pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
+ if err != nil {
+ ctx.AddFlash(ctx.Tr("flash.user.invalid-ssh-key"), "error")
+ return ctx.RedirectTo("/settings")
+ }
+ key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
+
+ if exists, err := db.SSHKeyDoesExists(key.Content); exists {
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot check if SSH key exists", err)
+ }
+ ctx.AddFlash(ctx.Tr("settings.ssh-key-exists"), "error")
+ return ctx.RedirectTo("/settings")
+ }
+
+ if err := key.Create(); err != nil {
+ return ctx.ErrorRes(500, "Cannot add SSH key", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.user.ssh-key-added"), "success")
+ return ctx.RedirectTo("/settings")
+}
+
+func sshKeysDelete(ctx *context.OGContext) error {
+ user := ctx.User
+ keyId, err := strconv.Atoi(ctx.Param("id"))
+ if err != nil {
+ return ctx.RedirectTo("/settings")
+ }
+
+ key, err := db.GetSSHKeyByID(uint(keyId))
+
+ if err != nil || key.UserID != user.ID {
+ return ctx.RedirectTo("/settings")
+ }
+
+ if err := key.Delete(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete SSH key", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.user.ssh-key-deleted"), "success")
+ return ctx.RedirectTo("/settings")
+}
+
+func passkeyDelete(ctx *context.OGContext) error {
+ user := ctx.User
+ keyId, err := strconv.Atoi(ctx.Param("id"))
+ if err != nil {
+ return ctx.RedirectTo("/settings")
+ }
+
+ passkey, err := db.GetCredentialByIDDB(uint(keyId))
+ if err != nil || passkey.UserID != user.ID {
+ return ctx.RedirectTo("/settings")
+ }
+
+ if err := passkey.Delete(); err != nil {
+ return ctx.ErrorRes(500, "Cannot delete passkey", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.auth.passkey-deleted"), "success")
+ return ctx.RedirectTo("/settings")
+}
+
+func passwordProcess(ctx *context.OGContext) error {
+ user := ctx.User
+
+ dto := new(db.UserDTO)
+ if err := ctx.Bind(dto); err != nil {
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
+ }
+ dto.Username = user.Username
+
+ if err := ctx.Validate(dto); err != nil {
+ ctx.AddFlash(utils.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
+ return ctx.HTML_("settings.html")
+ }
+
+ password, err := utils.Argon2id.Hash(dto.Password)
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot hash password", err)
+ }
+ user.Password = password
+
+ if err = user.Update(); err != nil {
+ return ctx.ErrorRes(500, "Cannot update password", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.user.password-updated"), "success")
+ return ctx.RedirectTo("/settings")
+}
+
+func usernameProcess(ctx *context.OGContext) error {
+ user := ctx.User
+
+ dto := new(db.UserDTO)
+ if err := ctx.Bind(dto); err != nil {
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
+ }
+ dto.Password = user.Password
+
+ if err := ctx.Validate(dto); err != nil {
+ ctx.AddFlash(utils.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
+ return ctx.RedirectTo("/settings")
+ }
+
+ if exists, err := db.UserExists(dto.Username); err != nil || exists {
+ ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
+ return ctx.RedirectTo("/settings")
+ }
+
+ sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username))
+ destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username))
+
+ if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
+ err := os.Rename(sourceDir, destinationDir)
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot rename user directory", err)
+ }
+ }
+
+ user.Username = dto.Username
+
+ if err := user.Update(); err != nil {
+ return ctx.ErrorRes(500, "Cannot update username", err)
+ }
+
+ ctx.AddFlash(ctx.Tr("flash.user.username-updated"), "success")
+ return ctx.RedirectTo("/settings")
+}
diff --git a/internal/web/handler/util.go b/internal/web/handler/util.go
new file mode 100644
index 0000000..62ea6ad
--- /dev/null
+++ b/internal/web/handler/util.go
@@ -0,0 +1,79 @@
+package handler
+
+import (
+ "errors"
+ "github.com/thomiceli/opengist/internal/web/context"
+ "html/template"
+ "strconv"
+ "strings"
+)
+
+func getPage(ctx *context.OGContext) int {
+ page := ctx.QueryParam("page")
+ if page == "" {
+ page = "1"
+ }
+ pageInt, err := strconv.Atoi(page)
+ if err != nil {
+ pageInt = 1
+ }
+ ctx.SetData("currPage", pageInt)
+
+ return pageInt
+}
+
+func paginate[T any](ctx *context.OGContext, data []*T, pageInt int, perPage int, templateDataName string, urlPage string, labels int, urlParams ...string) error {
+ lenData := len(data)
+ if lenData == 0 && pageInt != 1 {
+ return errors.New("page not found")
+ }
+
+ if lenData > perPage {
+ if lenData > 1 {
+ data = data[:lenData-1]
+ }
+ ctx.SetData("nextPage", pageInt+1)
+ }
+ if pageInt > 1 {
+ ctx.SetData("prevPage", pageInt-1)
+ }
+
+ if len(urlParams) > 0 {
+ ctx.SetData("urlParams", template.URL(urlParams[0]))
+ }
+
+ switch labels {
+ case 1:
+ ctx.SetData("prevLabel", ctx.TrH("pagination.previous"))
+ ctx.SetData("nextLabel", ctx.TrH("pagination.next"))
+ case 2:
+ ctx.SetData("prevLabel", ctx.TrH("pagination.newer"))
+ ctx.SetData("nextLabel", ctx.TrH("pagination.older"))
+ }
+
+ ctx.SetData("urlPage", urlPage)
+ ctx.SetData(templateDataName, data)
+ return nil
+}
+
+func ParseSearchQueryStr(query string) (string, map[string]string) {
+ words := strings.Fields(query)
+ metadata := make(map[string]string)
+ var contentBuilder strings.Builder
+
+ for _, word := range words {
+ if strings.Contains(word, ":") {
+ keyValue := strings.SplitN(word, ":", 2)
+ if len(keyValue) == 2 {
+ key := keyValue[0]
+ value := keyValue[1]
+ metadata[key] = value
+ }
+ } else {
+ contentBuilder.WriteString(word + " ")
+ }
+ }
+
+ content := strings.TrimSpace(contentBuilder.String())
+ return content, metadata
+}
diff --git a/internal/web/server.go b/internal/web/server.go
deleted file mode 100644
index f3d4850..0000000
--- a/internal/web/server.go
+++ /dev/null
@@ -1,626 +0,0 @@
-package web
-
-import (
- "context"
- gojson "encoding/json"
- "errors"
- "fmt"
- htmlpkg "html"
- "html/template"
- "io"
- "net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
- "time"
-
- "github.com/thomiceli/opengist/internal/index"
- "github.com/thomiceli/opengist/internal/utils"
- "github.com/thomiceli/opengist/templates"
-
- "github.com/gorilla/sessions"
- "github.com/labstack/echo/v4"
- "github.com/labstack/echo/v4/middleware"
- "github.com/markbates/goth/gothic"
- "github.com/rs/zerolog/log"
- "github.com/thomiceli/opengist/internal/auth"
- "github.com/thomiceli/opengist/internal/config"
- "github.com/thomiceli/opengist/internal/db"
- "github.com/thomiceli/opengist/internal/git"
- "github.com/thomiceli/opengist/internal/i18n"
- "github.com/thomiceli/opengist/public"
- "golang.org/x/text/language"
-)
-
-var (
- dev bool
- flashStore *sessions.CookieStore // session store for flash messages
- userStore *sessions.FilesystemStore // session store for user sessions
- re = regexp.MustCompile("[^a-z0-9]+")
- fm = template.FuncMap{
- "split": strings.Split,
- "indexByte": strings.IndexByte,
- "toInt": func(i string) int {
- val, _ := strconv.Atoi(i)
- return val
- },
- "inc": func(i int) int {
- return i + 1
- },
- "splitGit": func(i string) []string {
- return strings.FieldsFunc(i, func(r rune) bool {
- return r == ',' || r == ' '
- })
- },
- "lines": func(i string) []string {
- return strings.Split(i, "\n")
- },
- "isMarkdown": func(i string) bool {
- return strings.ToLower(filepath.Ext(i)) == ".md"
- },
- "isCsv": func(i string) bool {
- return strings.ToLower(filepath.Ext(i)) == ".csv"
- },
- "isSvg": func(i string) bool {
- return strings.ToLower(filepath.Ext(i)) == ".svg"
- },
- "csvFile": func(file *git.File) *git.CsvFile {
- if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
- return nil
- }
-
- csvFile, err := git.ParseCsv(file)
- if err != nil {
- return nil
- }
-
- return csvFile
- },
- "httpStatusText": http.StatusText,
- "loadedTime": func(startTime time.Time) string {
- return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
- },
- "slug": func(s string) string {
- return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
- },
- "avatarUrl": func(user *db.User, noGravatar bool) string {
- if user.AvatarURL != "" {
- return user.AvatarURL
- }
-
- if user.MD5Hash != "" && !noGravatar {
- return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
- }
-
- return defaultAvatar()
- },
- "asset": asset,
- "custom": customAsset,
- "dev": func() bool {
- return dev
- },
- "defaultAvatar": defaultAvatar,
- "visibilityStr": func(visibility db.Visibility, lowercase bool) string {
- s := "Public"
- switch visibility {
- case 1:
- s = "Unlisted"
- case 2:
- s = "Private"
- }
-
- if lowercase {
- return strings.ToLower(s)
- }
- return s
- },
- "unescape": htmlpkg.UnescapeString,
- "join": func(s ...string) string {
- return strings.Join(s, "")
- },
- "toStr": func(i interface{}) string {
- return fmt.Sprint(i)
- },
- "safe": func(s string) template.HTML {
- return template.HTML(s)
- },
- "dict": func(values ...interface{}) (map[string]interface{}, error) {
- if len(values)%2 != 0 {
- return nil, errors.New("invalid dict call")
- }
- dict := make(map[string]interface{})
- for i := 0; i < len(values); i += 2 {
- key, ok := values[i].(string)
- if !ok {
- return nil, errors.New("dict keys must be strings")
- }
- dict[key] = values[i+1]
- }
- return dict, nil
- },
- "addMetadataToSearchQuery": addMetadataToSearchQuery,
- "indexEnabled": index.Enabled,
- "isUrl": func(s string) bool {
- _, err := url.ParseRequestURI(s)
- return err == nil
- },
- }
-)
-
-type Template struct {
- templates *template.Template
-}
-
-func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
- return t.templates.ExecuteTemplate(w, name, data)
-}
-
-type Server struct {
- echo *echo.Echo
- dev bool
-}
-
-func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server {
- dev = isDev
- flashStore = sessions.NewCookieStore([]byte("opengist"))
- encryptKey, _ := utils.GenerateSecretKey(filepath.Join(sessionsPath, "session-encrypt.key"))
- userStore = sessions.NewFilesystemStore(sessionsPath, config.SecretKey, encryptKey)
- userStore.MaxLength(10 * 1024)
- gothic.Store = userStore
-
- e := echo.New()
- e.HideBanner = true
- e.HidePort = true
-
- if err := i18n.Locales.LoadAll(); err != nil {
- log.Fatal().Err(err).Msg("Failed to load locales")
- }
-
- e.Use(dataInit)
- e.Use(locale)
- e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
- Getter: middleware.MethodFromForm("_method"),
- }))
- e.Pre(middleware.RemoveTrailingSlash())
- e.Pre(middleware.CORS())
- e.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).
- Str("ip", ctx.RealIP()).TimeDiff("duration", time.Now(), v.StartTime).
- Msg("HTTP")
- return nil
- },
- }))
- e.Use(middleware.Recover())
- e.Use(middleware.Secure())
-
- t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))
- customPattern := filepath.Join(config.GetHomeDir(), "custom", "*.html")
- matches, err := filepath.Glob(customPattern)
- if err != nil {
- log.Fatal().Err(err).Msg("Failed to check for custom templates")
- }
- if len(matches) > 0 {
- t, err = t.ParseGlob(customPattern)
- if err != nil {
- log.Fatal().Err(err).Msg("Failed to parse custom templates")
- }
- }
- e.Renderer = &Template{
- templates: t,
- }
-
- e.HTTPErrorHandler = func(er error, ctx echo.Context) {
- var httpErr *echo.HTTPError
- if errors.As(er, &httpErr) {
- acceptJson := strings.Contains(ctx.Request().Header.Get("Accept"), "application/json")
- setData(ctx, "error", er)
- if acceptJson {
- if fatalErr := jsonWithCode(ctx, httpErr.Code, httpErr); fatalErr != nil {
- log.Fatal().Err(fatalErr).Send()
- }
- } else {
- if fatalErr := htmlWithCode(ctx, httpErr.Code, "error.html"); fatalErr != nil {
- log.Fatal().Err(fatalErr).Send()
- }
- }
- } else {
- log.Fatal().Err(er).Send()
- }
- }
-
- e.Use(sessionInit)
-
- e.Validator = utils.NewValidator()
-
- if !dev {
- 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(csrfInit)
- }
-
- g1.GET("/", create, logged)
- g1.POST("/", processCreate, 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", adminUsers)
- 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 &Server{echo: e, dev: dev}
-}
-
-func (s *Server) Start() {
- addr := config.C.HttpHost + ":" + config.C.HttpPort
-
- log.Info().Msg("Starting HTTP server on http://" + addr)
- if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed {
- log.Fatal().Err(err).Msg("Failed to start HTTP server")
- }
-}
-
-func (s *Server) Stop() {
- if err := s.echo.Close(); err != nil {
- log.Fatal().Err(err).Msg("Failed to stop HTTP server")
- }
-}
-
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- s.echo.ServeHTTP(w, r)
-}
-
-func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
- ctx.SetRequest(ctx.Request().WithContext(ctxValue))
- setData(ctx, "loadStartTime", time.Now())
-
- if err := loadSettings(ctx); err != nil {
- return errorRes(500, "Cannot read settings from database", err)
- }
-
- setData(ctx, "c", config.C)
-
- setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
- setData(ctx, "gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "")
- setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
- setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
-
- httpProtocol := "http"
- if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
- httpProtocol = "https"
- }
- setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
-
- var baseHttpUrl string
- // if a custom external url is set, use it
- if config.C.ExternalUrl != "" {
- baseHttpUrl = config.C.ExternalUrl
- } else {
- baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
- }
-
- setData(ctx, "baseHttpUrl", baseHttpUrl)
-
- return next(ctx)
- }
-}
-
-func locale(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- // Check URL arguments
- lang := ctx.Request().URL.Query().Get("lang")
- changeLang := lang != ""
-
- // Then check cookies
- if len(lang) == 0 {
- cookie, _ := ctx.Request().Cookie("lang")
- if cookie != nil {
- lang = cookie.Value
- }
- }
-
- // Check again in case someone changes the supported language list.
- if lang != "" && !i18n.Locales.HasLocale(lang) {
- lang = ""
- changeLang = false
- }
-
- // 3.Then check from 'Accept-Language' header.
- if len(lang) == 0 {
- tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language"))
- lang = i18n.Locales.MatchTag(tags)
- }
-
- if changeLang {
- ctx.SetCookie(&http.Cookie{Name: "lang", Value: lang, Path: "/", MaxAge: 1<<31 - 1})
- }
-
- localeUsed, err := i18n.Locales.GetLocale(lang)
- if err != nil {
- return errorRes(500, "Cannot get locale", err)
- }
-
- setData(ctx, "localeName", localeUsed.Name)
- setData(ctx, "locale", localeUsed)
- setData(ctx, "allLocales", i18n.Locales.Locales)
-
- return next(ctx)
- }
-}
-
-func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- sess := getSession(ctx)
- if sess.Values["user"] != nil {
- var err error
- var user *db.User
-
- if user, err = db.GetUserById(sess.Values["user"].(uint)); err != nil {
- sess.Values["user"] = nil
- saveSession(sess, ctx)
- setData(ctx, "userLogged", nil)
- return redirect(ctx, "/all")
- }
- if user != nil {
- setData(ctx, "userLogged", user)
- }
- return next(ctx)
- }
-
- setData(ctx, "userLogged", nil)
- return next(ctx)
- }
-}
-
-func csrfInit(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- setCsrfHtmlForm(ctx)
- return next(ctx)
- }
-}
-
-func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- gist := getData(ctx, "gist")
- user := getUserLogged(ctx)
- if !gist.(*db.Gist).CanWrite(user) {
- return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
- }
- return next(ctx)
- }
-}
-
-func adminPermission(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- user := getUserLogged(ctx)
- if user == nil || !user.IsAdmin {
- return notFound("User not found")
- }
- return next(ctx)
- }
-}
-
-func logged(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- user := getUserLogged(ctx)
- if user != nil {
- return next(ctx)
- }
- return redirect(ctx, "/all")
- }
-}
-
-func inMFASession(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- sess := getSession(ctx)
- _, ok := sess.Values["mfaID"].(uint)
- if !ok {
- return errorRes(400, tr(ctx, "error.not-in-mfa-session"), nil)
- }
- return next(ctx)
- }
-}
-
-func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(ctx echo.Context) error {
- if user := getUserLogged(ctx); user != nil {
- return next(ctx)
- }
-
- allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
- if err != nil {
- log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
- }
-
- if !allow {
- addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error")
- return redirect(ctx, "/login")
- }
- return next(ctx)
- }
- }
-}
-
-func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
- return makeCheckRequireLogin(false)(next)
-}
-
-func noRouteFound(echo.Context) error {
- return notFound("Page not found")
-}
-
-// ---
-
-type Asset struct {
- File string `json:"file"`
-}
-
-var manifestEntries map[string]Asset
-
-func parseManifestEntries() {
- file, err := public.Files.Open("manifest.json")
- if err != nil {
- log.Fatal().Err(err).Msg("Failed to open manifest.json")
- }
- byteValue, err := io.ReadAll(file)
- if err != nil {
- log.Fatal().Err(err).Msg("Failed to read manifest.json")
- }
- if err = gojson.Unmarshal(byteValue, &manifestEntries); err != nil {
- log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
- }
-}
-
-func defaultAvatar() string {
- if dev {
- return "http://localhost:16157/default.png"
- }
- return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File
-}
-
-func asset(file string) string {
- if dev {
- return "http://localhost:16157/" + file
- }
- return config.C.ExternalUrl + "/" + manifestEntries[file].File
-}
-
-func customAsset(file string) string {
- assetpath, err := url.JoinPath("/", "assets", file)
- if err != nil {
- log.Error().Err(err).Msgf("Failed to join path for custom file %s", file)
- }
- return config.C.ExternalUrl + assetpath
-}
diff --git a/internal/web/server/middlewares.go b/internal/web/server/middlewares.go
new file mode 100644
index 0000000..4544447
--- /dev/null
+++ b/internal/web/server/middlewares.go
@@ -0,0 +1,175 @@
+package server
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+ "github.com/rs/zerolog/log"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/db"
+ "github.com/thomiceli/opengist/internal/i18n"
+ "github.com/thomiceli/opengist/internal/web/context"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+ "net/http"
+ "strings"
+ "time"
+)
+
+func (s *Server) useCustomContext() {
+ s.echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ cc := context.NewContext(c)
+ return next(cc)
+ }
+ })
+}
+
+func (s *Server) RegisterMiddlewares(e *echo.Echo) {
+ e.Use(Middleware(dataInit).ToEcho())
+ e.Use(Middleware(locale).ToEcho())
+
+ e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
+ Getter: middleware.MethodFromForm("_method"),
+ }))
+ e.Pre(middleware.RemoveTrailingSlash())
+ e.Pre(middleware.CORS())
+ e.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).
+ Str("ip", ctx.RealIP()).TimeDiff("duration", time.Now(), v.StartTime).
+ Msg("HTTP")
+ return nil
+ },
+ }))
+ e.Use(middleware.Recover())
+ e.Use(middleware.Secure())
+
+ e.Use(Middleware(sessionInit).ToEcho())
+}
+
+func dataInit(next Handler) Handler {
+ return func(ctx *context.OGContext) error {
+ ctx.SetData("loadStartTime", time.Now())
+
+ if err := loadSettings(ctx); err != nil {
+ return ctx.ErrorRes(500, "Cannot load settings", err)
+ }
+
+ ctx.SetData("c", config.C)
+
+ ctx.SetData("githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
+ ctx.SetData("gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "")
+ ctx.SetData("giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
+ ctx.SetData("oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
+
+ httpProtocol := "http"
+ if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
+ httpProtocol = "https"
+ }
+ ctx.SetData("httpProtocol", strings.ToUpper(httpProtocol))
+
+ var baseHttpUrl string
+ // if a custom external url is set, use it
+ if config.C.ExternalUrl != "" {
+ baseHttpUrl = config.C.ExternalUrl
+ } else {
+ baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
+ }
+
+ ctx.SetData("baseHttpUrl", baseHttpUrl)
+
+ return next(ctx)
+ }
+}
+
+func locale(next Handler) Handler {
+ return func(ctx *context.OGContext) error {
+ // Check URL arguments
+ lang := ctx.Request().URL.Query().Get("lang")
+ changeLang := lang != ""
+
+ // Then check cookies
+ if len(lang) == 0 {
+ cookie, _ := ctx.Request().Cookie("lang")
+ if cookie != nil {
+ lang = cookie.Value
+ }
+ }
+
+ // Check again in case someone changes the supported language list.
+ if lang != "" && !i18n.Locales.HasLocale(lang) {
+ lang = ""
+ changeLang = false
+ }
+
+ // 3.Then check from 'Accept-Language' header.
+ if len(lang) == 0 {
+ tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language"))
+ lang = i18n.Locales.MatchTag(tags)
+ }
+
+ if changeLang {
+ ctx.SetCookie(&http.Cookie{Name: "lang", Value: lang, Path: "/", MaxAge: 1<<31 - 1})
+ }
+
+ localeUsed, err := i18n.Locales.GetLocale(lang)
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot get locale", err)
+ }
+
+ ctx.SetData("localeName", localeUsed.Name)
+ ctx.SetData("locale", localeUsed)
+ ctx.SetData("allLocales", i18n.Locales.Locales)
+
+ return next(ctx)
+ }
+}
+
+func sessionInit(next Handler) Handler {
+ return func(ctx *context.OGContext) error {
+ sess := ctx.GetSession()
+ if sess.Values["user"] != nil {
+ var err error
+ var user *db.User
+
+ if user, err = db.GetUserById(sess.Values["user"].(uint)); err != nil {
+ sess.Values["user"] = nil
+ ctx.SaveSession(sess)
+ ctx.User = nil
+ ctx.SetData("userLogged", nil)
+ return ctx.RedirectTo("/all")
+ }
+ if user != nil {
+ ctx.User = user
+ ctx.SetData("userLogged", user)
+ }
+ return next(ctx)
+ }
+
+ ctx.User = nil
+ ctx.SetData("userLogged", nil)
+ return next(ctx)
+ }
+}
+
+func csrfInit(next Handler) Handler {
+ return func(ctx *context.OGContext) error {
+ setCsrfHtmlForm(ctx)
+ return next(ctx)
+ }
+}
+
+func loadSettings(ctx *context.OGContext) error {
+ settings, err := db.GetSettings()
+ if err != nil {
+ return err
+ }
+
+ for key, value := range settings {
+ s := strings.ReplaceAll(key, "-", " ")
+ s = cases.Title(language.English).String(s)
+ ctx.SetData(strings.ReplaceAll(s, " ", ""), value == "1")
+ }
+ return nil
+}
diff --git a/internal/web/server/renderer.go b/internal/web/server/renderer.go
new file mode 100644
index 0000000..ad4cb6c
--- /dev/null
+++ b/internal/web/server/renderer.go
@@ -0,0 +1,216 @@
+package server
+
+import (
+ gojson "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/rs/zerolog/log"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/db"
+ "github.com/thomiceli/opengist/internal/git"
+ "github.com/thomiceli/opengist/internal/index"
+ "github.com/thomiceli/opengist/internal/web/handler"
+ "github.com/thomiceli/opengist/public"
+ "github.com/thomiceli/opengist/templates"
+ htmlpkg "html"
+ "html/template"
+ "io"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var re = regexp.MustCompile("[^a-z0-9]+")
+
+func (s *Server) setFuncMap() {
+ fm := template.FuncMap{
+ "split": strings.Split,
+ "indexByte": strings.IndexByte,
+ "toInt": func(i string) int {
+ val, _ := strconv.Atoi(i)
+ return val
+ },
+ "inc": func(i int) int {
+ return i + 1
+ },
+ "splitGit": func(i string) []string {
+ return strings.FieldsFunc(i, func(r rune) bool {
+ return r == ',' || r == ' '
+ })
+ },
+ "lines": func(i string) []string {
+ return strings.Split(i, "\n")
+ },
+ "isMarkdown": func(i string) bool {
+ return strings.ToLower(filepath.Ext(i)) == ".md"
+ },
+ "isCsv": func(i string) bool {
+ return strings.ToLower(filepath.Ext(i)) == ".csv"
+ },
+ "isSvg": func(i string) bool {
+ return strings.ToLower(filepath.Ext(i)) == ".svg"
+ },
+ "csvFile": func(file *git.File) *git.CsvFile {
+ if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
+ return nil
+ }
+
+ csvFile, err := git.ParseCsv(file)
+ if err != nil {
+ return nil
+ }
+
+ return csvFile
+ },
+ "httpStatusText": http.StatusText,
+ "loadedTime": func(startTime time.Time) string {
+ return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
+ },
+ "slug": func(s string) string {
+ return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
+ },
+ "avatarUrl": func(user *db.User, noGravatar bool) string {
+ if user.AvatarURL != "" {
+ return user.AvatarURL
+ }
+
+ if user.MD5Hash != "" && !noGravatar {
+ return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
+ }
+
+ return defaultAvatar()
+ },
+ "asset": asset,
+ "custom": customAsset,
+ "dev": func() bool {
+ return s.dev
+ },
+ "defaultAvatar": defaultAvatar,
+ "visibilityStr": func(visibility db.Visibility, lowercase bool) string {
+ s := "Public"
+ switch visibility {
+ case 1:
+ s = "Unlisted"
+ case 2:
+ s = "Private"
+ }
+
+ if lowercase {
+ return strings.ToLower(s)
+ }
+ return s
+ },
+ "unescape": htmlpkg.UnescapeString,
+ "join": func(s ...string) string {
+ return strings.Join(s, "")
+ },
+ "toStr": func(i interface{}) string {
+ return fmt.Sprint(i)
+ },
+ "safe": func(s string) template.HTML {
+ return template.HTML(s)
+ },
+ "dict": func(values ...interface{}) (map[string]interface{}, error) {
+ if len(values)%2 != 0 {
+ return nil, errors.New("invalid dict call")
+ }
+ dict := make(map[string]interface{})
+ for i := 0; i < len(values); i += 2 {
+ key, ok := values[i].(string)
+ if !ok {
+ return nil, errors.New("dict keys must be strings")
+ }
+ dict[key] = values[i+1]
+ }
+ return dict, nil
+ },
+ "addMetadataToSearchQuery": addMetadataToSearchQuery,
+ "indexEnabled": index.Enabled,
+ "isUrl": func(s string) bool {
+ _, err := url.ParseRequestURI(s)
+ return err == nil
+ },
+ }
+
+ t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))
+ customPattern := filepath.Join(config.GetHomeDir(), "custom", "*.html")
+ matches, err := filepath.Glob(customPattern)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to check for custom templates")
+ }
+ if len(matches) > 0 {
+ t, err = t.ParseGlob(customPattern)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to parse custom templates")
+ }
+ }
+ s.echo.Renderer = &Template{
+ templates: t,
+ }
+}
+
+type Asset struct {
+ File string `json:"file"`
+}
+
+var ManifestEntries map[string]Asset
+
+func parseManifestEntries() {
+ file, err := public.Files.Open("manifest.json")
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to open manifest.json")
+ }
+ byteValue, err := io.ReadAll(file)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to read manifest.json")
+ }
+ if err = gojson.Unmarshal(byteValue, &ManifestEntries); err != nil {
+ log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
+ }
+}
+
+var dev = true
+
+func defaultAvatar() string {
+ if dev {
+ return "http://localhost:16157/default.png"
+ }
+ return config.C.ExternalUrl + "/" + ManifestEntries["default.png"].File
+}
+
+func asset(file string) string {
+ if dev {
+ return "http://localhost:16157/" + file
+ }
+ return config.C.ExternalUrl + "/" + ManifestEntries[file].File
+}
+
+func customAsset(file string) string {
+ assetpath, err := url.JoinPath("/", "assets", file)
+ if err != nil {
+ log.Error().Err(err).Msgf("Failed to join path for custom file %s", file)
+ }
+ return config.C.ExternalUrl + assetpath
+}
+
+func addMetadataToSearchQuery(input, key, value string) string {
+ content, metadata := handler.ParseSearchQueryStr(input)
+
+ metadata[key] = value
+
+ var resultBuilder strings.Builder
+ resultBuilder.WriteString(content)
+
+ for k, v := range metadata {
+ resultBuilder.WriteString(" ")
+ resultBuilder.WriteString(k)
+ resultBuilder.WriteString(":")
+ resultBuilder.WriteString(v)
+ }
+
+ return strings.TrimSpace(resultBuilder.String())
+}
diff --git a/internal/web/server/router.go b/internal/web/server/router.go
new file mode 100644
index 0000000..abb4e43
--- /dev/null
+++ b/internal/web/server/router.go
@@ -0,0 +1 @@
+package server
diff --git a/internal/web/server/server.go b/internal/web/server/server.go
new file mode 100644
index 0000000..6066952
--- /dev/null
+++ b/internal/web/server/server.go
@@ -0,0 +1,319 @@
+package server
+
+import (
+ "errors"
+ "github.com/thomiceli/opengist/internal/web/context"
+ "github.com/thomiceli/opengist/internal/web/handler"
+ "html/template"
+ "io"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/thomiceli/opengist/internal/index"
+ "github.com/thomiceli/opengist/internal/utils"
+
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+ "github.com/rs/zerolog/log"
+ "github.com/thomiceli/opengist/internal/auth"
+ "github.com/thomiceli/opengist/internal/config"
+ "github.com/thomiceli/opengist/internal/db"
+ "github.com/thomiceli/opengist/internal/i18n"
+ "github.com/thomiceli/opengist/public"
+)
+
+type Template struct {
+ templates *template.Template
+}
+
+func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
+ return t.templates.ExecuteTemplate(w, name, data)
+}
+
+type Server struct {
+ echo *echo.Echo
+ flashStore *sessions.CookieStore // session store for flash messages
+ UserStore *sessions.FilesystemStore // session store for user sessions
+
+ dev bool
+ sessionsPath string
+ ignoreCsrf bool
+}
+
+func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server {
+ e := echo.New()
+ e.HideBanner = true
+ e.HidePort = true
+
+ s := &Server{echo: e, dev: isDev, sessionsPath: sessionsPath, ignoreCsrf: ignoreCsrf}
+
+ s.useCustomContext()
+
+ if err := i18n.Locales.LoadAll(); err != nil {
+ log.Fatal().Err(err).Msg("Failed to load locales")
+ }
+
+ s.RegisterMiddlewares(e)
+ s.setFuncMap()
+ s.setHTTPErrorHandler()
+
+ e.Validator = utils.NewValidator()
+
+ if !s.dev {
+ 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)
+
+ return s
+}
+
+func (s *Server) Start() {
+ addr := config.C.HttpHost + ":" + config.C.HttpPort
+
+ log.Info().Msg("Starting HTTP server on http://" + addr)
+ if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed {
+ log.Fatal().Err(err).Msg("Failed to start HTTP server")
+ }
+}
+
+func (s *Server) Stop() {
+ if err := s.echo.Close(); err != nil {
+ log.Fatal().Err(err).Msg("Failed to stop HTTP server")
+ }
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ s.echo.ServeHTTP(w, r)
+}
+
+func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ gist := getData(ctx, "gist")
+ user := getUserLogged(ctx)
+ if !gist.(*db.Gist).CanWrite(user) {
+ return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
+ }
+ return next(ctx)
+ }
+}
+
+func adminPermission(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ user := getUserLogged(ctx)
+ if user == nil || !user.IsAdmin {
+ return notFound("User not found")
+ }
+ return next(ctx)
+ }
+}
+
+func logged(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ user := getUserLogged(ctx)
+ if user != nil {
+ return next(ctx)
+ }
+ return redirect(ctx, "/all")
+ }
+}
+
+func inMFASession(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ sess := getSession(ctx)
+ _, ok := sess.Values["mfaID"].(uint)
+ if !ok {
+ return errorRes(400, tr(ctx, "error.not-in-mfa-session"), nil)
+ }
+ return next(ctx)
+ }
+}
+
+func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ if user := getUserLogged(ctx); user != nil {
+ return next(ctx)
+ }
+
+ allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
+ }
+
+ if !allow {
+ addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error")
+ return redirect(ctx, "/login")
+ }
+ return next(ctx)
+ }
+ }
+}
+
+func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
+ return makeCheckRequireLogin(false)(next)
+}
+
+func noRouteFound(echo.Context) error {
+ return notFound("Page not found")
+}
+
+func (s *Server) setHTTPErrorHandler() {
+ s.echo.HTTPErrorHandler = func(er error, c echo.Context) {
+ ctx := c.(*context.OGContext)
+ var httpErr *echo.HTTPError
+ if errors.As(er, &httpErr) {
+ acceptJson := strings.Contains(ctx.Request().Header.Get("Accept"), "application/json")
+ ctx.SetData("error", er)
+ if acceptJson {
+ if fatalErr := jsonWithCode(ctx, httpErr.Code, httpErr); fatalErr != nil {
+ log.Fatal().Err(fatalErr).Send()
+ }
+ } else {
+ if fatalErr := htmlWithCode(ctx, httpErr.Code, "error.html"); fatalErr != nil {
+ log.Fatal().Err(fatalErr).Send()
+ }
+ }
+ } else {
+ log.Fatal().Err(er).Send()
+ }
+ }
+}
diff --git a/internal/web/server/util.go b/internal/web/server/util.go
new file mode 100644
index 0000000..e9872e5
--- /dev/null
+++ b/internal/web/server/util.go
@@ -0,0 +1,33 @@
+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
+
+func (h Handler) ToEcho() echo.HandlerFunc {
+ return func(c echo.Context) error {
+ return h(c.(*context.OGContext))
+ }
+}
+
+func (m Middleware) ToEcho() echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return m(func(c *context.OGContext) error {
+ return next(c)
+ }).ToEcho()
+ }
+}
diff --git a/internal/web/settings.go b/internal/web/settings.go
deleted file mode 100644
index f6b6324..0000000
--- a/internal/web/settings.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package web
-
-import (
- "crypto/md5"
- "fmt"
- "github.com/thomiceli/opengist/internal/config"
- "github.com/thomiceli/opengist/internal/git"
- "github.com/thomiceli/opengist/internal/i18n"
- "github.com/thomiceli/opengist/internal/utils"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "time"
-
- "github.com/labstack/echo/v4"
- "github.com/thomiceli/opengist/internal/db"
- "golang.org/x/crypto/ssh"
-)
-
-func userSettings(ctx echo.Context) error {
- user := getUserLogged(ctx)
-
- keys, err := db.GetSSHKeysByUserID(user.ID)
- if err != nil {
- return errorRes(500, "Cannot get SSH keys", err)
- }
-
- passkeys, err := db.GetAllCredentialsForUser(user.ID)
- if err != nil {
- return errorRes(500, "Cannot get WebAuthn credentials", err)
- }
-
- _, hasTotp, err := user.HasMFA()
- if err != nil {
- return errorRes(500, "Cannot get MFA status", err)
- }
-
- setData(ctx, "email", user.Email)
- setData(ctx, "sshKeys", keys)
- setData(ctx, "passkeys", passkeys)
- setData(ctx, "hasTotp", hasTotp)
- setData(ctx, "hasPassword", user.Password != "")
- setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
- setData(ctx, "htmlTitle", trH(ctx, "settings"))
- return html(ctx, "settings.html")
-}
-
-func emailProcess(ctx echo.Context) error {
- user := getUserLogged(ctx)
- email := ctx.FormValue("email")
- var hash string
-
- if email == "" {
- // generate random md5 string
- hash = fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String())))
- } else {
- hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
- }
-
- user.Email = strings.ToLower(email)
- user.MD5Hash = hash
-
- if err := user.Update(); err != nil {
- return errorRes(500, "Cannot update email", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.user.email-updated"), "success")
- return redirect(ctx, "/settings")
-}
-
-func accountDeleteProcess(ctx echo.Context) error {
- user := getUserLogged(ctx)
-
- if err := user.Delete(); err != nil {
- return errorRes(500, "Cannot delete this user", err)
- }
-
- return redirect(ctx, "/all")
-}
-
-func sshKeysProcess(ctx echo.Context) error {
- user := getUserLogged(ctx)
-
- dto := new(db.SSHKeyDTO)
- if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
- }
-
- if err := ctx.Validate(dto); err != nil {
- addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
- return redirect(ctx, "/settings")
- }
- key := dto.ToSSHKey()
-
- key.UserID = user.ID
-
- pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
- if err != nil {
- addFlash(ctx, tr(ctx, "flash.user.invalid-ssh-key"), "error")
- return redirect(ctx, "/settings")
- }
- key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
-
- if exists, err := db.SSHKeyDoesExists(key.Content); exists {
- if err != nil {
- return errorRes(500, "Cannot check if SSH key exists", err)
- }
- addFlash(ctx, tr(ctx, "settings.ssh-key-exists"), "error")
- return redirect(ctx, "/settings")
- }
-
- if err := key.Create(); err != nil {
- return errorRes(500, "Cannot add SSH key", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.user.ssh-key-added"), "success")
- return redirect(ctx, "/settings")
-}
-
-func sshKeysDelete(ctx echo.Context) error {
- user := getUserLogged(ctx)
- keyId, err := strconv.Atoi(ctx.Param("id"))
- if err != nil {
- return redirect(ctx, "/settings")
- }
-
- key, err := db.GetSSHKeyByID(uint(keyId))
-
- if err != nil || key.UserID != user.ID {
- return redirect(ctx, "/settings")
- }
-
- if err := key.Delete(); err != nil {
- return errorRes(500, "Cannot delete SSH key", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.user.ssh-key-deleted"), "success")
- return redirect(ctx, "/settings")
-}
-
-func passkeyDelete(ctx echo.Context) error {
- user := getUserLogged(ctx)
- keyId, err := strconv.Atoi(ctx.Param("id"))
- if err != nil {
- return redirect(ctx, "/settings")
- }
-
- passkey, err := db.GetCredentialByIDDB(uint(keyId))
- if err != nil || passkey.UserID != user.ID {
- return redirect(ctx, "/settings")
- }
-
- if err := passkey.Delete(); err != nil {
- return errorRes(500, "Cannot delete passkey", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.auth.passkey-deleted"), "success")
- return redirect(ctx, "/settings")
-}
-
-func passwordProcess(ctx echo.Context) error {
- user := getUserLogged(ctx)
-
- dto := new(db.UserDTO)
- if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
- }
- dto.Username = user.Username
-
- if err := ctx.Validate(dto); err != nil {
- addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
- return html(ctx, "settings.html")
- }
-
- password, err := utils.Argon2id.Hash(dto.Password)
- if err != nil {
- return errorRes(500, "Cannot hash password", err)
- }
- user.Password = password
-
- if err = user.Update(); err != nil {
- return errorRes(500, "Cannot update password", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.user.password-updated"), "success")
- return redirect(ctx, "/settings")
-}
-
-func usernameProcess(ctx echo.Context) error {
- user := getUserLogged(ctx)
-
- dto := new(db.UserDTO)
- if err := ctx.Bind(dto); err != nil {
- return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
- }
- dto.Password = user.Password
-
- if err := ctx.Validate(dto); err != nil {
- addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
- return redirect(ctx, "/settings")
- }
-
- if exists, err := db.UserExists(dto.Username); err != nil || exists {
- addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
- return redirect(ctx, "/settings")
- }
-
- sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username))
- destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username))
-
- if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
- err := os.Rename(sourceDir, destinationDir)
- if err != nil {
- return errorRes(500, "Cannot rename user directory", err)
- }
- }
-
- user.Username = dto.Username
-
- if err := user.Update(); err != nil {
- return errorRes(500, "Cannot update username", err)
- }
-
- addFlash(ctx, tr(ctx, "flash.user.username-updated"), "success")
- return redirect(ctx, "/settings")
-}
diff --git a/internal/web/util.go b/internal/web/util.go
deleted file mode 100644
index d0adc7b..0000000
--- a/internal/web/util.go
+++ /dev/null
@@ -1,248 +0,0 @@
-package web
-
-import (
- "context"
- "errors"
- "github.com/gorilla/sessions"
- "github.com/labstack/echo/v4"
- "github.com/rs/zerolog/log"
- "github.com/thomiceli/opengist/internal/config"
- "github.com/thomiceli/opengist/internal/db"
- "github.com/thomiceli/opengist/internal/i18n"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
- "html/template"
- "net/http"
- "strconv"
- "strings"
-)
-
-type dataTypeKey string
-
-const dataKey dataTypeKey = "data"
-
-func setData(ctx echo.Context, key string, value any) {
- data := ctx.Request().Context().Value(dataKey).(echo.Map)
- data[key] = value
- ctxValue := context.WithValue(ctx.Request().Context(), dataKey, data)
- ctx.SetRequest(ctx.Request().WithContext(ctxValue))
-}
-
-func getData(ctx echo.Context, key string) any {
- data := ctx.Request().Context().Value(dataKey).(echo.Map)
- return data[key]
-}
-
-func dataMap(ctx echo.Context) echo.Map {
- return ctx.Request().Context().Value(dataKey).(echo.Map)
-}
-
-func html(ctx echo.Context, template string) error {
- return htmlWithCode(ctx, 200, template)
-}
-
-func htmlWithCode(ctx echo.Context, code int, template string) error {
- setErrorFlashes(ctx)
- return ctx.Render(code, template, ctx.Request().Context().Value(dataKey))
-}
-
-func json(ctx echo.Context, data any) error {
- return jsonWithCode(ctx, 200, data)
-}
-
-func jsonWithCode(ctx echo.Context, code int, data any) error {
- return ctx.JSON(code, data)
-}
-
-func redirect(ctx echo.Context, location string) error {
- return ctx.Redirect(302, config.C.ExternalUrl+location)
-}
-
-func plainText(ctx echo.Context, code int, message string) error {
- return ctx.String(code, message)
-}
-
-func notFound(message string) error {
- return errorRes(404, message, nil)
-}
-
-func errorRes(code int, message string, err error) error {
- if code >= 500 {
- var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger()
- skipLogger.Error().Err(err).Msg(message)
- }
-
- return &echo.HTTPError{Code: code, Message: message, Internal: err}
-}
-
-func jsonErrorRes(code int, message string, err error) error {
- if code >= 500 {
- var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger()
- skipLogger.Error().Err(err).Msg(message)
- }
-
- return &echo.HTTPError{Code: code, Message: message, Internal: err}
-}
-
-func getUserLogged(ctx echo.Context) *db.User {
- user := getData(ctx, "userLogged")
- if user != nil {
- return user.(*db.User)
- }
- return nil
-}
-
-func setErrorFlashes(ctx echo.Context) {
- sess, _ := flashStore.Get(ctx.Request(), "flash")
-
- setData(ctx, "flashErrors", sess.Flashes("error"))
- setData(ctx, "flashSuccess", sess.Flashes("success"))
- setData(ctx, "flashWarnings", sess.Flashes("warning"))
-
- _ = sess.Save(ctx.Request(), ctx.Response())
-}
-
-func addFlash(ctx echo.Context, flashMessage string, flashType string) {
- sess, _ := flashStore.Get(ctx.Request(), "flash")
- sess.AddFlash(flashMessage, flashType)
- _ = sess.Save(ctx.Request(), ctx.Response())
-}
-
-func getSession(ctx echo.Context) *sessions.Session {
- sess, _ := userStore.Get(ctx.Request(), "session")
- return sess
-}
-
-func saveSession(sess *sessions.Session, ctx echo.Context) {
- _ = sess.Save(ctx.Request(), ctx.Response())
-}
-
-func deleteSession(ctx echo.Context) {
- sess := getSession(ctx)
- sess.Options.MaxAge = -1
- saveSession(sess, ctx)
-}
-
-func setCsrfHtmlForm(ctx echo.Context) {
- var csrf string
- if csrfToken, ok := ctx.Get("csrf").(string); ok {
- csrf = csrfToken
- }
- setData(ctx, "csrfHtml", template.HTML(``))
-}
-
-func deleteCsrfCookie(ctx echo.Context) {
- ctx.SetCookie(&http.Cookie{Name: "_csrf", Path: "/", MaxAge: -1})
-}
-
-func loadSettings(ctx echo.Context) error {
- settings, err := db.GetSettings()
- if err != nil {
- return err
- }
-
- for key, value := range settings {
- s := strings.ReplaceAll(key, "-", " ")
- s = cases.Title(language.English).String(s)
- setData(ctx, strings.ReplaceAll(s, " ", ""), value == "1")
- }
- return nil
-}
-
-func getPage(ctx echo.Context) int {
- page := ctx.QueryParam("page")
- if page == "" {
- page = "1"
- }
- pageInt, err := strconv.Atoi(page)
- if err != nil {
- pageInt = 1
- }
- setData(ctx, "currPage", pageInt)
-
- return pageInt
-}
-
-func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, templateDataName string, urlPage string, labels int, urlParams ...string) error {
- lenData := len(data)
- if lenData == 0 && pageInt != 1 {
- return errors.New("page not found")
- }
-
- if lenData > perPage {
- if lenData > 1 {
- data = data[:lenData-1]
- }
- setData(ctx, "nextPage", pageInt+1)
- }
- if pageInt > 1 {
- setData(ctx, "prevPage", pageInt-1)
- }
-
- if len(urlParams) > 0 {
- setData(ctx, "urlParams", template.URL(urlParams[0]))
- }
-
- switch labels {
- case 1:
- setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
- setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
- case 2:
- setData(ctx, "prevLabel", trH(ctx, "pagination.newer"))
- setData(ctx, "nextLabel", trH(ctx, "pagination.older"))
- }
-
- setData(ctx, "urlPage", urlPage)
- setData(ctx, templateDataName, data)
- return nil
-}
-
-func trH(ctx echo.Context, key string, args ...any) template.HTML {
- l := getData(ctx, "locale").(*i18n.Locale)
- return l.Tr(key, args...)
-}
-
-func tr(ctx echo.Context, key string, args ...any) string {
- l := getData(ctx, "locale").(*i18n.Locale)
- return l.String(key, args...)
-}
-
-func parseSearchQueryStr(query string) (string, map[string]string) {
- words := strings.Fields(query)
- metadata := make(map[string]string)
- var contentBuilder strings.Builder
-
- for _, word := range words {
- if strings.Contains(word, ":") {
- keyValue := strings.SplitN(word, ":", 2)
- if len(keyValue) == 2 {
- key := keyValue[0]
- value := keyValue[1]
- metadata[key] = value
- }
- } else {
- contentBuilder.WriteString(word + " ")
- }
- }
-
- content := strings.TrimSpace(contentBuilder.String())
- return content, metadata
-}
-
-func addMetadataToSearchQuery(input, key, value string) string {
- content, metadata := parseSearchQueryStr(input)
-
- metadata[key] = value
-
- var resultBuilder strings.Builder
- resultBuilder.WriteString(content)
-
- for k, v := range metadata {
- resultBuilder.WriteString(" ")
- resultBuilder.WriteString(k)
- resultBuilder.WriteString(":")
- resultBuilder.WriteString(v)
- }
-
- return strings.TrimSpace(resultBuilder.String())
-}