mirror of
https://github.com/thomiceli/opengist.git
synced 2025-01-03 16:22:40 +00:00
wip
This commit is contained in:
parent
d2f6fe1ab8
commit
d75840eba8
11 changed files with 303 additions and 310 deletions
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"github.com/thomiceli/opengist/internal/ssh"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
"github.com/urfave/cli/v2"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -37,7 +37,7 @@ var CmdStart = cli.Command{
|
|||
|
||||
Initialize(ctx)
|
||||
|
||||
go web.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start()
|
||||
go server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start()
|
||||
go ssh.Start()
|
||||
|
||||
<-stopCtx.Done()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -9,29 +10,38 @@ import (
|
|||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type OGContext struct {
|
||||
echo.Context
|
||||
|
||||
data echo.Map
|
||||
lock sync.RWMutex
|
||||
|
||||
store *Store
|
||||
User *db.User
|
||||
}
|
||||
|
||||
func NewContext(c echo.Context) *OGContext {
|
||||
func NewContext(c echo.Context, sessionPath string) *OGContext {
|
||||
return &OGContext{
|
||||
Context: c,
|
||||
data: make(echo.Map),
|
||||
store: NewStore(sessionPath),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *OGContext) SetData(key string, value any) {
|
||||
ctx.lock.Lock()
|
||||
defer ctx.lock.Unlock()
|
||||
|
||||
ctx.data[key] = value
|
||||
}
|
||||
|
||||
func (ctx *OGContext) GetData(key string) any {
|
||||
ctx.lock.RLock()
|
||||
defer ctx.lock.RUnlock()
|
||||
|
||||
return ctx.data[key]
|
||||
}
|
||||
|
||||
|
@ -45,14 +55,7 @@ func (ctx *OGContext) ErrorRes(code int, message string, err error) error {
|
|||
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)
|
||||
}
|
||||
ctx.SetRequest(ctx.Request().WithContext(context.WithValue(ctx.Request().Context(), "data", ctx.data)))
|
||||
|
||||
return &echo.HTTPError{Code: code, Message: message, Internal: err}
|
||||
}
|
||||
|
@ -138,3 +141,9 @@ func (ctx *OGContext) Tr(key string, args ...any) string {
|
|||
l := ctx.GetData("locale").(*i18n.Locale)
|
||||
return l.String(key, args...)
|
||||
}
|
||||
|
||||
var ManifestEntries map[string]Asset
|
||||
|
||||
type Asset struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@ type Store struct {
|
|||
}
|
||||
|
||||
func NewStore(sessionsPath string) *Store {
|
||||
return &Store{sessionsPath: sessionsPath}
|
||||
}
|
||||
s := &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
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -443,7 +443,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error {
|
|||
sess := ctx.GetSession()
|
||||
jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte)
|
||||
if !ok {
|
||||
return ctx.JsonErrorRes(401, "Cannot get WebAuthn registration session", nil)
|
||||
return ctx.ErrorRes(401, "Cannot get WebAuthn registration session", nil)
|
||||
}
|
||||
|
||||
user := ctx.User
|
||||
|
@ -451,7 +451,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error {
|
|||
// extract passkey name from request
|
||||
body, err := io.ReadAll(ctx.Request().Body)
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(400, "Failed to read request body", err)
|
||||
return ctx.ErrorRes(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 *context.OGContext) error {
|
|||
_ = gojson.Unmarshal(body, &dto)
|
||||
|
||||
if err = ctx.Validate(dto); err != nil {
|
||||
return ctx.JsonErrorRes(400, "Invalid request", err)
|
||||
return ctx.ErrorRes(400, "Invalid request", err)
|
||||
}
|
||||
passkeyName := dto.PasskeyName
|
||||
if passkeyName == "" {
|
||||
|
@ -469,11 +469,11 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error {
|
|||
|
||||
waCredential, err := webauthn.FinishBinding(user, jsonWaSession, ctx.Request())
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(403, "Failed binding attempt for passkey", err)
|
||||
return ctx.ErrorRes(403, "Failed binding attempt for passkey", err)
|
||||
}
|
||||
|
||||
if _, err = db.CreateFromCrendential(user.ID, passkeyName, waCredential); err != nil {
|
||||
return ctx.JsonErrorRes(500, "Cannot create WebAuthn credential on database", err)
|
||||
return ctx.ErrorRes(500, "Cannot create WebAuthn credential on database", err)
|
||||
}
|
||||
|
||||
delete(sess.Values, "webauthn_registration_session")
|
||||
|
@ -486,7 +486,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error {
|
|||
func BeginWebAuthnLogin(ctx *context.OGContext) error {
|
||||
credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err)
|
||||
return ctx.ErrorRes(401, "Cannot begin WebAuthn login", err)
|
||||
}
|
||||
|
||||
sess := ctx.GetSession()
|
||||
|
@ -501,12 +501,12 @@ func FinishWebAuthnLogin(ctx *context.OGContext) error {
|
|||
sess := ctx.GetSession()
|
||||
sessionData, ok := sess.Values["webauthn_login_session"].([]byte)
|
||||
if !ok {
|
||||
return ctx.JsonErrorRes(401, "Cannot get WebAuthn login session", nil)
|
||||
return ctx.ErrorRes(401, "Cannot get WebAuthn login session", nil)
|
||||
}
|
||||
|
||||
userID, err := webauthn.FinishDiscoverableLogin(sessionData, ctx.Request())
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(403, "Failed authentication attempt for passkey", err)
|
||||
return ctx.ErrorRes(403, "Failed authentication attempt for passkey", err)
|
||||
}
|
||||
|
||||
sess.Values["user"] = userID
|
||||
|
@ -523,12 +523,12 @@ func BeginWebAuthnAssertion(ctx *context.OGContext) error {
|
|||
|
||||
ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint))
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(500, "Cannot get user", err)
|
||||
return ctx.ErrorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
credsCreation, jsonWaSession, err := webauthn.BeginLogin(ogUser)
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err)
|
||||
return ctx.ErrorRes(401, "Cannot begin WebAuthn login", err)
|
||||
}
|
||||
|
||||
sess.Values["webauthn_assertion_session"] = jsonWaSession
|
||||
|
@ -542,18 +542,18 @@ func FinishWebAuthnAssertion(ctx *context.OGContext) error {
|
|||
sess := ctx.GetSession()
|
||||
sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte)
|
||||
if !ok {
|
||||
return ctx.JsonErrorRes(401, "Cannot get WebAuthn assertion session", nil)
|
||||
return ctx.ErrorRes(401, "Cannot get WebAuthn assertion session", nil)
|
||||
}
|
||||
|
||||
userId := sess.Values["mfaID"].(uint)
|
||||
|
||||
ogUser, err := db.GetUserById(userId)
|
||||
if err != nil {
|
||||
return ctx.JsonErrorRes(500, "Cannot get user", err)
|
||||
return ctx.ErrorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
if err = webauthn.FinishLogin(ogUser, sessionData, ctx.Request()); err != nil {
|
||||
return ctx.JsonErrorRes(403, "Failed authentication attempt for passkey", err)
|
||||
return ctx.ErrorRes(403, "Failed authentication attempt for passkey", err)
|
||||
}
|
||||
|
||||
sess.Values["user"] = userId
|
||||
|
@ -803,13 +803,13 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
|
|||
}
|
||||
|
||||
type ContextAuthInfo struct {
|
||||
context *context.OGContext
|
||||
Context *context.OGContext
|
||||
}
|
||||
|
||||
func (auth ContextAuthInfo) RequireLogin() (bool, error) {
|
||||
return auth.context.GetData("RequireLogin") == true, nil
|
||||
return auth.Context.GetData("RequireLogin") == true, nil
|
||||
}
|
||||
|
||||
func (auth ContextAuthInfo) AllowGistsWithoutLogin() (bool, error) {
|
||||
return auth.context.GetData("AllowGistsWithoutLogin") == true, nil
|
||||
return auth.Context.GetData("AllowGistsWithoutLogin") == true, nil
|
||||
}
|
||||
|
|
|
@ -8,10 +8,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -25,116 +23,10 @@ import (
|
|||
"github.com/thomiceli/opengist/internal/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
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":
|
||||
ctx.SetData("gistpage", "js")
|
||||
gistName = strings.TrimSuffix(gistName, ".js")
|
||||
case ".json":
|
||||
ctx.SetData("gistpage", "json")
|
||||
gistName = strings.TrimSuffix(gistName, ".json")
|
||||
case ".git":
|
||||
ctx.SetData("gistpage", "git")
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
}
|
||||
|
||||
gist, err := db.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return ctx.NotFound("Gist not found")
|
||||
}
|
||||
|
||||
if gist.Private == db.PrivateVisibility {
|
||||
if currUser == nil || currUser.ID != gist.UserID {
|
||||
return ctx.NotFound("Gist not found")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetData("gist", gist)
|
||||
|
||||
if config.C.SshGit {
|
||||
var sshDomain string
|
||||
|
||||
if config.C.SshExternalDomain != "" {
|
||||
sshDomain = config.C.SshExternalDomain
|
||||
} else {
|
||||
sshDomain = strings.Split(ctx.Request().Host, ":")[0]
|
||||
}
|
||||
|
||||
if config.C.SshPort == "22" {
|
||||
ctx.SetData("sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git")
|
||||
} else {
|
||||
ctx.SetData("sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git")
|
||||
}
|
||||
}
|
||||
|
||||
baseHttpUrl := ctx.GetData("baseHttpUrl").(string)
|
||||
|
||||
if config.C.HttpGit {
|
||||
ctx.SetData("httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
||||
}
|
||||
|
||||
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 ctx.ErrorRes(500, "Error fetching number of commits", err)
|
||||
}
|
||||
ctx.SetData("nbCommits", nbCommits)
|
||||
|
||||
if currUser != nil {
|
||||
hasLiked, err := currUser.HasLiked(gist)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot get user like status", err)
|
||||
}
|
||||
ctx.SetData("hasLiked", hasLiked)
|
||||
}
|
||||
|
||||
if gist.Private > 0 {
|
||||
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
|
||||
// useful for git clients using HTTP to obfuscate the existence of a private gist
|
||||
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)
|
||||
ctx.SetData("gist", gist)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *context.OGContext) error {
|
||||
var err error
|
||||
var urlPage string
|
||||
|
@ -382,7 +274,7 @@ func GistJson(ctx *context.OGContext) error {
|
|||
return ctx.ErrorRes(500, "Error joining js url", err)
|
||||
}
|
||||
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), server.ManifestEntries["embed.css"].File)
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["embed.css"].File)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining css url", err)
|
||||
}
|
||||
|
@ -426,7 +318,7 @@ func GistJs(ctx *context.OGContext) error {
|
|||
}
|
||||
_ = w.Flush()
|
||||
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), server.ManifestEntries["embed.css"].File)
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["embed.css"].File)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining css url", err)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -12,6 +13,7 @@ import (
|
|||
"golang.org/x/text/language"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -19,7 +21,7 @@ import (
|
|||
func (s *Server) useCustomContext() {
|
||||
s.echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
cc := context.NewContext(c)
|
||||
cc := context.NewContext(c, s.sessionsPath)
|
||||
return next(cc)
|
||||
}
|
||||
})
|
||||
|
@ -47,6 +49,16 @@ func (s *Server) RegisterMiddlewares() {
|
|||
s.echo.Use(middleware.Secure())
|
||||
|
||||
s.echo.Use(Middleware(sessionInit).ToEcho())
|
||||
|
||||
if !s.ignoreCsrf {
|
||||
s.echo.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf,header:X-CSRF-Token",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}))
|
||||
s.echo.Use(Middleware(csrfInit).ToEcho())
|
||||
}
|
||||
}
|
||||
|
||||
func dataInit(next Handler) Handler {
|
||||
|
@ -180,3 +192,107 @@ func loadSettings(ctx *context.OGContext) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GistInit(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
currUser := ctx.User
|
||||
|
||||
userName := ctx.Param("user")
|
||||
gistName := ctx.Param("gistname")
|
||||
|
||||
switch filepath.Ext(gistName) {
|
||||
case ".js":
|
||||
ctx.SetData("gistpage", "js")
|
||||
gistName = strings.TrimSuffix(gistName, ".js")
|
||||
case ".json":
|
||||
ctx.SetData("gistpage", "json")
|
||||
gistName = strings.TrimSuffix(gistName, ".json")
|
||||
case ".git":
|
||||
ctx.SetData("gistpage", "git")
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
}
|
||||
|
||||
gist, err := db.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return ctx.NotFound("Gist not found")
|
||||
}
|
||||
|
||||
if gist.Private == db.PrivateVisibility {
|
||||
if currUser == nil || currUser.ID != gist.UserID {
|
||||
return ctx.NotFound("Gist not found")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetData("gist", gist)
|
||||
|
||||
if config.C.SshGit {
|
||||
var sshDomain string
|
||||
|
||||
if config.C.SshExternalDomain != "" {
|
||||
sshDomain = config.C.SshExternalDomain
|
||||
} else {
|
||||
sshDomain = strings.Split(ctx.Request().Host, ":")[0]
|
||||
}
|
||||
|
||||
if config.C.SshPort == "22" {
|
||||
ctx.SetData("sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git")
|
||||
} else {
|
||||
ctx.SetData("sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git")
|
||||
}
|
||||
}
|
||||
|
||||
baseHttpUrl := ctx.GetData("baseHttpUrl").(string)
|
||||
|
||||
if config.C.HttpGit {
|
||||
ctx.SetData("httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
||||
}
|
||||
|
||||
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 ctx.ErrorRes(500, "Error fetching number of commits", err)
|
||||
}
|
||||
ctx.SetData("nbCommits", nbCommits)
|
||||
|
||||
if currUser != nil {
|
||||
hasLiked, err := currUser.HasLiked(gist)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot get user like status", err)
|
||||
}
|
||||
ctx.SetData("hasLiked", hasLiked)
|
||||
}
|
||||
|
||||
if gist.Private > 0 {
|
||||
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
|
||||
// useful for git clients using HTTP to obfuscate the existence of a private gist
|
||||
func GistSoftInit(next Handler) 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)
|
||||
ctx.SetData("gist", gist)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// GistNewPushSoftInit has the same behavior as gistSoftInit but create a new gist empty instead
|
||||
func GistNewPushSoftInit(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
ctx.SetData("gist", new(db.Gist))
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"github.com/thomiceli/opengist/internal/web/handler"
|
||||
"github.com/thomiceli/opengist/public"
|
||||
"github.com/thomiceli/opengist/templates"
|
||||
|
@ -82,14 +83,33 @@ func (s *Server) setFuncMap() {
|
|||
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
|
||||
}
|
||||
|
||||
return defaultAvatar()
|
||||
if s.dev {
|
||||
return "http://localhost:16157/default.png"
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + context.ManifestEntries["default.png"].File
|
||||
},
|
||||
"asset": func(file string) string {
|
||||
if s.dev {
|
||||
return "http://localhost:16157/" + file
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + context.ManifestEntries[file].File
|
||||
},
|
||||
"custom": func(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
|
||||
},
|
||||
"asset": asset,
|
||||
"custom": customAsset,
|
||||
"dev": func() bool {
|
||||
return s.dev
|
||||
},
|
||||
"defaultAvatar": defaultAvatar,
|
||||
"defaultAvatar": func() string {
|
||||
if s.dev {
|
||||
return "http://localhost:16157/default.png"
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + context.ManifestEntries["default.png"].File
|
||||
},
|
||||
"visibilityStr": func(visibility db.Visibility, lowercase bool) string {
|
||||
s := "Public"
|
||||
switch visibility {
|
||||
|
@ -128,8 +148,24 @@ func (s *Server) setFuncMap() {
|
|||
}
|
||||
return dict, nil
|
||||
},
|
||||
"addMetadataToSearchQuery": addMetadataToSearchQuery,
|
||||
"indexEnabled": index.Enabled,
|
||||
"addMetadataToSearchQuery": func(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())
|
||||
},
|
||||
"indexEnabled": index.Enabled,
|
||||
"isUrl": func(s string) bool {
|
||||
_, err := url.ParseRequestURI(s)
|
||||
return err == nil
|
||||
|
@ -153,12 +189,6 @@ func (s *Server) setFuncMap() {
|
|||
}
|
||||
}
|
||||
|
||||
type Asset struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
var ManifestEntries map[string]Asset
|
||||
|
||||
func parseManifestEntries() {
|
||||
file, err := public.Files.Open("manifest.json")
|
||||
if err != nil {
|
||||
|
@ -168,49 +198,7 @@ func parseManifestEntries() {
|
|||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to read manifest.json")
|
||||
}
|
||||
if err = gojson.Unmarshal(byteValue, &ManifestEntries); err != nil {
|
||||
if err = gojson.Unmarshal(byteValue, &context.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())
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package server
|
|||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
|
@ -21,15 +20,6 @@ func (s *Server) setupRoutes() {
|
|||
|
||||
// Web based routes
|
||||
{
|
||||
if !s.ignoreCsrf {
|
||||
r.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf,header:X-CSRF-Token",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}))
|
||||
r.Use(csrfInit)
|
||||
}
|
||||
|
||||
r.GET("/", handler.Create, logged)
|
||||
r.POST("/", handler.ProcessCreate, logged)
|
||||
|
@ -68,67 +58,67 @@ func (s *Server) setupRoutes() {
|
|||
r.DELETE("/settings/totp", handler.DisableTotp, logged)
|
||||
r.POST("/settings/totp/regenerate", handler.RegenerateTotpRecoveryCodes, logged)
|
||||
|
||||
g2 := g1.Group("/admin-panel")
|
||||
g2 := r.SubGroup("/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)
|
||||
g2.GET("", handler.AdminIndex)
|
||||
g2.GET("/users", handler.AdminUsers)
|
||||
g2.POST("/users/:user/delete", handler.AdminUserDelete)
|
||||
g2.GET("/gists", handler.AdminGists)
|
||||
g2.POST("/gists/:gist/delete", handler.AdminGistDelete)
|
||||
g2.GET("/invitations", handler.AdminInvitations)
|
||||
g2.POST("/invitations", handler.AdminInvitationsCreate)
|
||||
g2.POST("/invitations/:id/delete", handler.AdminInvitationsDelete)
|
||||
g2.POST("/sync-fs", handler.AdminSyncReposFromFS)
|
||||
g2.POST("/sync-db", handler.AdminSyncReposFromDB)
|
||||
g2.POST("/gc-repos", handler.AdminGcRepos)
|
||||
g2.POST("/sync-previews", handler.AdminSyncGistPreviews)
|
||||
g2.POST("/reset-hooks", handler.AdminResetHooks)
|
||||
g2.POST("/index-gists", handler.AdminIndexGists)
|
||||
g2.GET("/configuration", handler.AdminConfig)
|
||||
g2.PUT("/set-config", handler.AdminSetConfig)
|
||||
}
|
||||
|
||||
if config.C.HttpGit {
|
||||
e.Any("/init/*", gitHttp, gistNewPushSoftInit)
|
||||
r.Any("/init/*", handler.GitHttp, GistNewPushSoftInit)
|
||||
}
|
||||
|
||||
g1.GET("/all", allGists, checkRequireLogin)
|
||||
r.GET("/all", handler.AllGists, checkRequireLogin)
|
||||
|
||||
if index.Enabled() {
|
||||
g1.GET("/search", search, checkRequireLogin)
|
||||
r.GET("/search", handler.Search, checkRequireLogin)
|
||||
} else {
|
||||
g1.GET("/search", allGists, checkRequireLogin)
|
||||
r.GET("/search", handler.AllGists, checkRequireLogin)
|
||||
}
|
||||
|
||||
g1.GET("/:user", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/liked", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/forked", allGists, checkRequireLogin)
|
||||
r.GET("/:user", handler.AllGists, checkRequireLogin)
|
||||
r.GET("/:user/liked", handler.AllGists, checkRequireLogin)
|
||||
r.GET("/:user/forked", handler.AllGists, checkRequireLogin)
|
||||
|
||||
g3 := g1.Group("/:user/:gistname")
|
||||
g3 := r.SubGroup("/: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)
|
||||
g3.Use(makeCheckRequireLogin(true), GistInit)
|
||||
g3.GET("", handler.GistIndex)
|
||||
g3.GET("/rev/:revision", handler.GistIndex)
|
||||
g3.GET("/revisions", handler.Revisions)
|
||||
g3.GET("/archive/:revision", handler.DownloadZip)
|
||||
g3.POST("/visibility", handler.EditVisibility, logged, writePermission)
|
||||
g3.POST("/delete", handler.DeleteGist, logged, writePermission)
|
||||
g3.GET("/raw/:revision/:file", handler.RawFile)
|
||||
g3.GET("/download/:revision/:file", handler.DownloadFile)
|
||||
g3.GET("/edit", handler.Edit, logged, writePermission)
|
||||
g3.POST("/edit", handler.ProcessCreate, logged, writePermission)
|
||||
g3.POST("/like", handler.Like, logged)
|
||||
g3.GET("/likes", handler.Likes, checkRequireLogin)
|
||||
g3.POST("/fork", handler.Fork, logged)
|
||||
g3.GET("/forks", handler.Forks, checkRequireLogin)
|
||||
g3.PUT("/checkbox", handler.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 {
|
||||
r.GET("/assets/*", func(ctx *context.OGContext) error {
|
||||
if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !s.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))
|
||||
|
||||
|
@ -137,8 +127,8 @@ func (s *Server) setupRoutes() {
|
|||
|
||||
// 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")
|
||||
if err := ctx.HTML_(ctx.Param("*")); err != nil {
|
||||
return ctx.NotFound("Page not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -148,10 +138,10 @@ func (s *Server) setupRoutes() {
|
|||
|
||||
// Git HTTP routes
|
||||
if config.C.HttpGit {
|
||||
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
|
||||
r.Any("/:user/:gistname/*", handler.GitHttp, GistSoftInit)
|
||||
}
|
||||
|
||||
e.Any("/*", noRouteFound)
|
||||
r.Any("/*", noRouteFound)
|
||||
}
|
||||
|
||||
// Router wraps echo.Group to provide custom Handler support
|
||||
|
@ -200,6 +190,10 @@ func (r *Router) PATCH(path string, h Handler, m ...Middleware) {
|
|||
r.Group.PATCH(path, Chain(h, m...).toEchoHandler())
|
||||
}
|
||||
|
||||
func (r *Router) Any(path string, h Handler, m ...Middleware) {
|
||||
r.Group.Any(path, Chain(h, m...).toEchoHandler())
|
||||
}
|
||||
|
||||
// Use registers middleware for the entire router group
|
||||
func (r *Router) Use(middleware ...Middleware) {
|
||||
for _, m := range middleware {
|
||||
|
|
|
@ -2,29 +2,21 @@ package server
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"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 {
|
||||
|
@ -60,7 +52,7 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server {
|
|||
|
||||
s.RegisterMiddlewares()
|
||||
s.setFuncMap()
|
||||
s.setHTTPErrorHandler()
|
||||
s.echo.HTTPErrorHandler = s.errorHandler
|
||||
|
||||
e.Validator = utils.NewValidator()
|
||||
|
||||
|
@ -92,95 +84,95 @@ 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)
|
||||
func writePermission(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
gist := ctx.GetData("gist")
|
||||
user := ctx.User
|
||||
if !gist.(*db.Gist).CanWrite(user) {
|
||||
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
|
||||
return ctx.RedirectTo("/" + 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)
|
||||
func adminPermission(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
user := ctx.User
|
||||
if user == nil || !user.IsAdmin {
|
||||
return notFound("User not found")
|
||||
return ctx.NotFound("User not found")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func logged(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
func logged(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
user := ctx.User
|
||||
if user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
return redirect(ctx, "/all")
|
||||
return ctx.RedirectTo("/all")
|
||||
}
|
||||
}
|
||||
|
||||
func inMFASession(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
func inMFASession(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
sess := ctx.GetSession()
|
||||
_, ok := sess.Values["mfaID"].(uint)
|
||||
if !ok {
|
||||
return errorRes(400, tr(ctx, "error.not-in-mfa-session"), nil)
|
||||
return ctx.ErrorRes(400, ctx.Tr("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 {
|
||||
func makeCheckRequireLogin(isSingleGistAccess bool) Middleware {
|
||||
return func(next Handler) Handler {
|
||||
return func(ctx *context.OGContext) error {
|
||||
if user := ctx.User; user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handler.ContextAuthInfo{Context: 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")
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.must-be-logged-in"), "error")
|
||||
return ctx.RedirectTo("/login")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
func checkRequireLogin(next Handler) Handler {
|
||||
return makeCheckRequireLogin(false)(next)
|
||||
}
|
||||
|
||||
func noRouteFound(echo.Context) error {
|
||||
return notFound("Page not found")
|
||||
func noRouteFound(ctx *context.OGContext) error {
|
||||
return ctx.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()
|
||||
}
|
||||
func (s *Server) errorHandler(err error, ctx echo.Context) {
|
||||
var httpErr *echo.HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
acceptJson := strings.Contains(ctx.Request().Header.Get("Accept"), "application/json")
|
||||
data := ctx.Request().Context().Value("data").(echo.Map)
|
||||
data["error"] = err
|
||||
if acceptJson {
|
||||
if err := ctx.JSON(httpErr.Code, httpErr); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
} else {
|
||||
log.Fatal().Err(er).Send()
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Render(httpErr.Code, "error", data); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package test
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -21,19 +22,18 @@ import (
|
|||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
)
|
||||
|
||||
var databaseType string
|
||||
|
||||
type testServer struct {
|
||||
server *web.Server
|
||||
server *server.Server
|
||||
sessionCookie string
|
||||
}
|
||||
|
||||
func newTestServer() (*testServer, error) {
|
||||
s := &testServer{
|
||||
server: web.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions"), true),
|
||||
server: server.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions"), true),
|
||||
}
|
||||
|
||||
go s.start()
|
||||
|
|
2
templates/pages/error.html
vendored
2
templates/pages/error.html
vendored
|
@ -1,3 +1,4 @@
|
|||
{{ define "error" }}
|
||||
{{ template "header" .}}
|
||||
|
||||
<div class="mt-4">
|
||||
|
@ -12,3 +13,4 @@
|
|||
{{ end }}
|
||||
</div>
|
||||
{{ template "footer" .}}
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue