opengist/internal/web/server.go

615 lines
17 KiB
Go
Raw Normal View History

2023-03-14 15:22:52 +00:00
package web
import (
"context"
2024-10-07 21:56:32 +00:00
gojson "encoding/json"
2024-01-04 02:38:15 +00:00
"errors"
2023-03-14 15:22:52 +00:00
"fmt"
htmlpkg "html"
"html/template"
"io"
"net/http"
2024-01-20 22:46:47 +00:00
"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"
2023-03-14 15:22:52 +00:00
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/markbates/goth/gothic"
2023-03-14 15:22:52 +00:00
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
2023-05-15 19:07:29 +00:00
"github.com/thomiceli/opengist/internal/config"
2023-09-02 22:30:57 +00:00
"github.com/thomiceli/opengist/internal/db"
2023-05-15 19:07:29 +00:00
"github.com/thomiceli/opengist/internal/git"
2023-09-22 15:26:09 +00:00
"github.com/thomiceli/opengist/internal/i18n"
2023-09-02 22:30:57 +00:00
"github.com/thomiceli/opengist/public"
2023-09-22 15:26:09 +00:00
"golang.org/x/text/language"
2023-03-14 15:22:52 +00:00
)
var (
2024-04-02 23:48:31 +00:00
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"
},
"csvFile": func(file *git.File) *git.CsvFile {
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
return nil
}
2023-03-23 15:00:48 +00:00
csvFile, err := git.ParseCsv(file)
if err != nil {
return nil
}
2023-03-23 15:00:48 +00:00
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()
},
2024-01-20 22:46:47 +00:00
"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)
},
2024-01-04 02:38:15 +00:00
"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,
2024-04-02 15:12:54 +00:00
"isUrl": func(s string) bool {
_, err := url.ParseRequestURI(s)
return err == nil
},
}
)
2023-03-14 15:22:52 +00:00
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)
}
2023-09-16 22:59:47 +00:00
type Server struct {
echo *echo.Echo
dev bool
}
2024-05-27 23:30:08 +00:00
func NewServer(isDev bool, sessionsPath string) *Server {
2023-09-16 22:59:47 +00:00
dev = isDev
2024-04-02 23:48:31 +00:00
flashStore = sessions.NewCookieStore([]byte("opengist"))
2024-05-27 23:30:08 +00:00
userStore = sessions.NewFilesystemStore(sessionsPath,
utils.ReadKey(path.Join(sessionsPath, "session-auth.key")),
utils.ReadKey(path.Join(sessionsPath, "session-encrypt.key")),
2024-04-02 23:48:31 +00:00
)
userStore.MaxLength(10 * 1024)
gothic.Store = userStore
2023-04-17 17:11:32 +00:00
2023-03-14 15:22:52 +00:00
e := echo.New()
e.HideBanner = true
e.HidePort = true
2023-09-22 15:26:09 +00:00
if err := i18n.Locales.LoadAll(); err != nil {
log.Fatal().Err(err).Msg("Failed to load locales")
}
2023-03-14 15:22:52 +00:00
e.Use(dataInit)
2023-09-22 15:26:09 +00:00
e.Use(locale)
2023-03-14 15:22:52 +00:00
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
Getter: middleware.MethodFromForm("_method"),
}))
e.Pre(middleware.RemoveTrailingSlash())
e.Pre(middleware.CORS())
e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
2023-03-14 15:22:52 +00:00
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).
2023-03-14 15:22:52 +00:00
Msg("HTTP")
return nil
},
}))
2024-01-04 03:44:56 +00:00
e.Use(middleware.Recover())
2023-03-14 15:22:52 +00:00
e.Use(middleware.Secure())
2024-04-02 15:12:54 +00:00
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")
}
}
2023-03-14 15:22:52 +00:00
e.Renderer = &Template{
2024-04-02 15:12:54 +00:00
templates: t,
2023-03-14 15:22:52 +00:00
}
2024-04-02 15:12:54 +00:00
2023-03-14 15:22:52 +00:00
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
2024-10-07 21:56:32 +00:00
if httpErr, ok := er.(*HTMLError); ok {
setData(ctx, "error", er)
if fatalErr := htmlWithCode(ctx, httpErr.Code, "error.html"); fatalErr != nil {
log.Fatal().Err(fatalErr).Send()
}
} else if httpErr, ok := er.(*JSONError); ok {
if fatalErr := json(ctx, httpErr.Code, httpErr); fatalErr != nil {
log.Fatal().Err(fatalErr).Send()
2023-03-14 15:22:52 +00:00
}
} else {
log.Fatal().Err(er).Send()
}
}
2023-03-15 09:37:17 +00:00
e.Use(sessionInit)
2023-03-14 15:22:52 +00:00
e.Validator = utils.NewValidator()
2023-03-14 15:22:52 +00:00
2023-04-06 09:47:30 +00:00
if !dev {
parseManifestEntries()
}
2023-03-14 15:22:52 +00:00
// Web based routes
g1 := e.Group("")
{
2023-09-16 22:59:47 +00:00
if !dev {
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
2024-10-07 21:56:32 +00:00
TokenLookup: "form:_csrf,header:X-CSRF-Token",
2023-09-16 22:59:47 +00:00
CookiePath: "/",
CookieHTTPOnly: true,
CookieSameSite: http.SameSiteStrictMode,
}))
}
2024-10-07 21:56:32 +00:00
g1.Use(csrfInit)
2023-03-14 15:22:52 +00:00
g1.GET("/", create, logged)
g1.POST("/", processCreate, logged)
2024-02-24 17:09:23 +00:00
g1.GET("/preview", preview, logged)
2023-03-14 15:22:52 +00:00
2023-12-16 00:27:00 +00:00
g1.GET("/healthcheck", healthcheck)
2024-09-11 23:45:30 +00:00
g1.GET("/metrics", metrics)
2023-12-16 00:27:00 +00:00
2023-03-14 15:22:52 +00:00
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)
2024-09-22 21:21:43 +00:00
g1.GET("/oauth/:provider/unlink", oauthUnlink, logged)
2024-10-07 21:56:32 +00:00
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)
2023-03-14 15:22:52 +00:00
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)
2024-10-07 21:56:32 +00:00
g1.DELETE("/settings/passkeys/:id", passkeyDelete, logged)
2023-11-20 17:03:28 +00:00
g1.PUT("/settings/password", passwordProcess, logged)
2024-01-01 22:45:19 +00:00
g1.PUT("/settings/username", usernameProcess, logged)
g2 := g1.Group("/admin-panel")
2023-03-14 15:22:52 +00:00
{
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)
2023-09-04 09:11:54 +00:00
g2.POST("/gc-repos", adminGcRepos)
g2.POST("/sync-previews", adminSyncGistPreviews)
g2.POST("/reset-hooks", adminResetHooks)
2024-01-04 02:38:15 +00:00
g2.POST("/index-gists", adminIndexGists)
2023-06-07 18:50:30 +00:00
g2.GET("/configuration", adminConfig)
g2.PUT("/set-config", adminSetConfig)
2023-03-14 15:22:52 +00:00
}
if config.C.HttpGit {
2023-09-25 16:43:55 +00:00
e.Any("/init/*", gitHttp, gistNewPushSoftInit)
}
2023-04-28 18:31:10 +00:00
g1.GET("/all", allGists, checkRequireLogin)
2024-01-04 02:38:15 +00:00
if index.Enabled() {
g1.GET("/search", search, checkRequireLogin)
} else {
g1.GET("/search", allGists, checkRequireLogin)
}
2023-04-28 18:31:10 +00:00
g1.GET("/:user", allGists, checkRequireLogin)
2023-06-21 16:19:17 +00:00
g1.GET("/:user/liked", allGists, checkRequireLogin)
g1.GET("/:user/forked", allGists, checkRequireLogin)
2023-03-14 15:22:52 +00:00
g3 := g1.Group("/:user/:gistname")
{
g3.Use(makeCheckRequireLogin(true), gistInit)
2023-03-17 13:56:39 +00:00
g3.GET("", gistIndex)
g3.GET("/rev/:revision", gistIndex)
2023-03-14 15:22:52 +00:00
g3.GET("/revisions", revisions)
g3.GET("/archive/:revision", downloadZip)
g3.POST("/visibility", editVisibility, logged, writePermission)
2023-03-14 15:22:52 +00:00
g3.POST("/delete", deleteGist, logged, writePermission)
g3.GET("/raw/:revision/:file", rawFile)
g3.GET("/download/:revision/:file", downloadFile)
2023-03-14 15:22:52 +00:00
g3.GET("/edit", edit, logged, writePermission)
g3.POST("/edit", processCreate, logged, writePermission)
g3.POST("/like", like, logged)
g3.GET("/likes", likes, checkRequireLogin)
2023-03-14 22:26:39 +00:00
g3.POST("/fork", fork, logged)
g3.GET("/forks", forks, checkRequireLogin)
g3.PUT("/checkbox", checkbox, logged, writePermission)
2023-03-14 15:22:52 +00:00
}
}
2024-04-02 15:12:54 +00:00
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 {
2024-09-08 01:41:41 +00:00
ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000")
ctx.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat))
2024-04-02 15:12:54 +00:00
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)
})
2023-03-14 15:22:52 +00:00
// Git HTTP routes
2023-04-06 23:52:56 +00:00
if config.C.HttpGit {
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
2023-03-14 15:22:52 +00:00
}
e.Any("/*", noRouteFound)
2023-09-16 22:59:47 +00:00
return &Server{echo: e, dev: dev}
}
func (s *Server) Start() {
2023-04-06 23:52:56 +00:00
addr := config.C.HttpHost + ":" + config.C.HttpPort
2023-03-14 15:22:52 +00:00
2023-09-16 22:59:47 +00:00
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")
2023-03-14 15:22:52 +00:00
}
}
2023-09-16 22:59:47 +00:00
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.echo.ServeHTTP(w, r)
}
2023-03-14 15:22:52 +00:00
func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
2023-04-26 21:13:11 +00:00
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
2023-03-14 15:22:52 +00:00
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
setData(ctx, "loadStartTime", time.Now())
2023-04-28 18:31:10 +00:00
if err := loadSettings(ctx); err != nil {
return errorRes(500, "Cannot read settings from database", err)
}
2023-03-15 09:37:17 +00:00
setData(ctx, "c", config.C)
2023-04-17 18:25:35 +00:00
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
2023-12-18 00:35:44 +00:00
setData(ctx, "gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "")
2023-04-17 18:25:35 +00:00
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
2023-09-15 21:56:14 +00:00
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
2023-04-17 18:25:35 +00:00
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)
2023-03-14 15:22:52 +00:00
return next(ctx)
}
}
2023-09-22 15:26:09 +00:00
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.
2023-09-22 15:26:09 +00:00
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)
}
}
2023-03-15 09:37:17 +00:00
func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
2023-03-14 15:22:52 +00:00
return func(ctx echo.Context) error {
sess := getSession(ctx)
if sess.Values["user"] != nil {
2023-03-17 13:56:39 +00:00
var err error
2023-09-02 22:30:57 +00:00
var user *db.User
2023-03-17 13:56:39 +00:00
2023-09-02 22:30:57 +00:00
if user, err = db.GetUserById(sess.Values["user"].(uint)); err != nil {
2023-03-14 15:22:52 +00:00
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)
2023-09-02 22:30:57 +00:00
if !gist.(*db.Gist).CanWrite(user) {
2023-12-26 02:24:04 +00:00
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
2023-03-14 15:22:52 +00:00
}
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")
2023-03-14 15:22:52 +00:00
}
}
2024-10-07 21:56:32 +00:00
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 {
2024-05-27 23:30:08 +00:00
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
}
2023-04-28 18:31:10 +00:00
if !allow {
addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error")
return redirect(ctx, "/login")
}
return next(ctx)
2023-04-28 18:31:10 +00:00
}
}
}
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
return makeCheckRequireLogin(false)(next)
}
2023-03-14 15:22:52 +00:00
func noRouteFound(echo.Context) error {
return notFound("Page not found")
}
2023-03-23 15:00:48 +00:00
// ---
type Asset struct {
File string `json:"file"`
}
var manifestEntries map[string]Asset
func parseManifestEntries() {
2023-09-02 22:30:57 +00:00
file, err := public.Files.Open("manifest.json")
2023-03-23 15:00:48 +00:00
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")
}
2024-10-07 21:56:32 +00:00
if err = gojson.Unmarshal(byteValue, &manifestEntries); err != nil {
2023-03-23 15:00:48 +00:00
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
}
2024-01-20 22:46:47 +00:00
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
}