This commit is contained in:
Thomas Miceli 2024-12-03 02:18:04 +01:00
parent 526da6ccbb
commit 60027e4ab8
18 changed files with 1938 additions and 1817 deletions

View file

@ -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")
}

View file

@ -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...)
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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(`<script src="%s"></script>`, baseHttpUrl+"/"+userName+"/"+gistName+".js"))
ctx.SetData("httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
ctx.SetData("currentUrl", template.URL(ctx.Request().URL.Path))
ctx.SetData("embedScript", fmt.Sprintf(`<script src="%s"></script>`, 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) {

View file

@ -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()))

View file

@ -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, "")
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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())
}

View file

@ -0,0 +1 @@
package server

View file

@ -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()
}
}
}

View file

@ -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(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
ctx.SetData("csrfHtml", template.HTML(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
}
type Handler func(ctx *context.OGContext) error
type 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()
}
}

View file

@ -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")
}

View file

@ -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(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
}
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())
}