add optional prefix to url
This commit is contained in:
@@ -17,6 +17,7 @@ type Config struct {
|
|||||||
SecretKey string
|
SecretKey string
|
||||||
Debug bool
|
Debug bool
|
||||||
MaxContentLength int64 // in bytes
|
MaxContentLength int64 // in bytes
|
||||||
|
URLPrefix string
|
||||||
|
|
||||||
// MD Notes App settings
|
// MD Notes App settings
|
||||||
AppName string
|
AppName string
|
||||||
@@ -68,6 +69,7 @@ var defaultConfig = map[string]map[string]string{
|
|||||||
"SECRET_KEY": "change-this-secret-key",
|
"SECRET_KEY": "change-this-secret-key",
|
||||||
"DEBUG": "false",
|
"DEBUG": "false",
|
||||||
"MAX_CONTENT_LENGTH": "16", // in MB
|
"MAX_CONTENT_LENGTH": "16", // in MB
|
||||||
|
"URL_PREFIX": "",
|
||||||
},
|
},
|
||||||
"MD_NOTES_APP": {
|
"MD_NOTES_APP": {
|
||||||
"APP_NAME": "Gobsidian",
|
"APP_NAME": "Gobsidian",
|
||||||
@@ -148,6 +150,14 @@ func Load() (*Config, error) {
|
|||||||
config.Debug, _ = SERVERSection.Key("DEBUG").Bool()
|
config.Debug, _ = SERVERSection.Key("DEBUG").Bool()
|
||||||
maxContentMB, _ := SERVERSection.Key("MAX_CONTENT_LENGTH").Int()
|
maxContentMB, _ := SERVERSection.Key("MAX_CONTENT_LENGTH").Int()
|
||||||
config.MaxContentLength = int64(maxContentMB) * 1024 * 1024 // Convert MB to bytes
|
config.MaxContentLength = int64(maxContentMB) * 1024 * 1024 // Convert MB to bytes
|
||||||
|
// Normalize URL prefix: "" or starts with '/' and no trailing '/'
|
||||||
|
rawPrefix := strings.TrimSpace(SERVERSection.Key("URL_PREFIX").String())
|
||||||
|
if rawPrefix == "/" { rawPrefix = "" }
|
||||||
|
if rawPrefix != "" {
|
||||||
|
if !strings.HasPrefix(rawPrefix, "/") { rawPrefix = "/" + rawPrefix }
|
||||||
|
rawPrefix = strings.TrimRight(rawPrefix, "/")
|
||||||
|
}
|
||||||
|
config.URLPrefix = rawPrefix
|
||||||
|
|
||||||
// Load MD_NOTES_APP section
|
// Load MD_NOTES_APP section
|
||||||
notesSection := cfg.Section("MD_NOTES_APP")
|
notesSection := cfg.Section("MD_NOTES_APP")
|
||||||
@@ -340,6 +350,14 @@ func (c *Config) SaveSetting(section, key, value string) error {
|
|||||||
if size, err := strconv.ParseInt(value, 10, 64); err == nil {
|
if size, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
c.MaxContentLength = size * 1024 * 1024
|
c.MaxContentLength = size * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
case "URL_PREFIX":
|
||||||
|
v := strings.TrimSpace(value)
|
||||||
|
if v == "/" { v = "" }
|
||||||
|
if v != "" {
|
||||||
|
if !strings.HasPrefix(v, "/") { v = "/" + v }
|
||||||
|
v = strings.TrimRight(v, "/")
|
||||||
|
}
|
||||||
|
c.URLPrefix = v
|
||||||
}
|
}
|
||||||
case "MD_NOTES_APP":
|
case "MD_NOTES_APP":
|
||||||
switch key {
|
switch key {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ const sessionCookieName = "gobsidian_session"
|
|||||||
|
|
||||||
// LoginPage renders the login form
|
// LoginPage renders the login form
|
||||||
func (h *Handlers) LoginPage(c *gin.Context) {
|
func (h *Handlers) LoginPage(c *gin.Context) {
|
||||||
|
// If already authenticated, redirect to home (respect URL prefix)
|
||||||
|
if isAuthenticated(c) {
|
||||||
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
|
||||||
|
return
|
||||||
|
}
|
||||||
token, _ := c.Get("csrf_token")
|
token, _ := c.Get("csrf_token")
|
||||||
c.HTML(http.StatusOK, "login", gin.H{
|
c.HTML(http.StatusOK, "login", gin.H{
|
||||||
"app_name": h.config.AppName,
|
"app_name": h.config.AppName,
|
||||||
@@ -128,7 +133,7 @@ func isAllDigits(s string) bool {
|
|||||||
func (h *Handlers) MFALoginPage(c *gin.Context) {
|
func (h *Handlers) MFALoginPage(c *gin.Context) {
|
||||||
session, _ := h.store.Get(c.Request, sessionCookieName)
|
session, _ := h.store.Get(c.Request, sessionCookieName)
|
||||||
if _, ok := session.Values["mfa_user_id"]; !ok {
|
if _, ok := session.Values["mfa_user_id"]; !ok {
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token, _ := c.Get("csrf_token")
|
token, _ := c.Get("csrf_token")
|
||||||
@@ -161,7 +166,7 @@ func (h *Handlers) MFALoginVerify(c *gin.Context) {
|
|||||||
session, _ := h.store.Get(c.Request, sessionCookieName)
|
session, _ := h.store.Get(c.Request, sessionCookieName)
|
||||||
uidAny, ok := session.Values["mfa_user_id"]
|
uidAny, ok := session.Values["mfa_user_id"]
|
||||||
if !ok {
|
if !ok {
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uid, _ := uidAny.(int64)
|
uid, _ := uidAny.(int64)
|
||||||
@@ -188,14 +193,14 @@ func (h *Handlers) MFALoginVerify(c *gin.Context) {
|
|||||||
delete(session.Values, "mfa_user_id")
|
delete(session.Values, "mfa_user_id")
|
||||||
session.Values["user_id"] = uid
|
session.Values["user_id"] = uid
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, "/")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfileMFASetupPage shows QR and input to verify during enrollment
|
// ProfileMFASetupPage shows QR and input to verify during enrollment
|
||||||
func (h *Handlers) ProfileMFASetupPage(c *gin.Context) {
|
func (h *Handlers) ProfileMFASetupPage(c *gin.Context) {
|
||||||
uidPtr := getUserIDPtr(c)
|
uidPtr := getUserIDPtr(c)
|
||||||
if uidPtr == nil {
|
if uidPtr == nil {
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ensure enrollment exists, otherwise create one
|
// ensure enrollment exists, otherwise create one
|
||||||
@@ -329,7 +334,7 @@ func (h *Handlers) LoginPost(c *gin.Context) {
|
|||||||
session, _ := h.store.Get(c.Request, sessionCookieName)
|
session, _ := h.store.Get(c.Request, sessionCookieName)
|
||||||
session.Values["mfa_user_id"] = user.ID
|
session.Values["mfa_user_id"] = user.ID
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, "/editor/mfa")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/mfa")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +345,7 @@ func (h *Handlers) LoginPost(c *gin.Context) {
|
|||||||
session, _ := h.store.Get(c.Request, sessionCookieName)
|
session, _ := h.store.Get(c.Request, sessionCookieName)
|
||||||
session.Values["user_id"] = user.ID
|
session.Values["user_id"] = user.ID
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, "/editor/profile/mfa/setup")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/profile/mfa/setup")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +354,7 @@ func (h *Handlers) LoginPost(c *gin.Context) {
|
|||||||
session.Values["user_id"] = user.ID
|
session.Values["user_id"] = user.ID
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
|
|
||||||
c.Redirect(http.StatusFound, "/")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutPost clears the session
|
// LogoutPost clears the session
|
||||||
@@ -357,5 +362,5 @@ func (h *Handlers) LogoutPost(c *gin.Context) {
|
|||||||
session, _ := h.store.Get(c.Request, sessionCookieName)
|
session, _ := h.store.Get(c.Request, sessionCookieName)
|
||||||
session.Options.MaxAge = -1
|
session.Options.MaxAge = -1
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ func (h *Handlers) CreateNoteHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect based on extension
|
// Redirect based on extension
|
||||||
redirect := "/note/" + notePath
|
redirect := h.config.URLPrefix + "/note/" + notePath
|
||||||
if strings.ToLower(ext) != "md" {
|
if strings.ToLower(ext) != "md" {
|
||||||
redirect = "/view_text/" + notePath
|
redirect = h.config.URLPrefix + "/view_text/" + notePath
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -297,7 +297,7 @@ func (h *Handlers) EditNoteHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Note saved successfully",
|
"message": "Note saved successfully",
|
||||||
"redirect": "/note/" + notePath,
|
"redirect": h.config.URLPrefix + "/note/" + notePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ func (h *Handlers) ProfilePage(c *gin.Context) {
|
|||||||
// Must be authenticated; middleware ensures user_id is set
|
// Must be authenticated; middleware ensures user_id is set
|
||||||
uidPtr := getUserIDPtr(c)
|
uidPtr := getUserIDPtr(c)
|
||||||
if uidPtr == nil {
|
if uidPtr == nil {
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ func (h *Handlers) PostProfileEnableMFA(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "setup": true, "redirect": "/editor/profile/mfa/setup"})
|
c.JSON(http.StatusOK, gin.H{"success": true, "setup": true, "redirect": h.config.URLPrefix+"/editor/profile/mfa/setup"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostProfileDisableMFA clears the user's MFA secret
|
// PostProfileDisableMFA clears the user's MFA secret
|
||||||
@@ -611,16 +611,18 @@ func (h *Handlers) AdminEnableUserMFA(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Create or replace an enrollment so user is prompted on next login
|
// Admin enable: set a new secret directly so MFA is immediately enabled
|
||||||
secret, err := generateBase32Secret()
|
secret, err := generateBase32Secret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate secret"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate secret"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := h.authSvc.DB.Exec(`INSERT OR REPLACE INTO mfa_enrollments (user_id, secret) VALUES (?, ?)`, id, secret); err != nil {
|
if _, err := h.authSvc.DB.Exec(`UPDATE users SET mfa_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, secret, id); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Remove any pending enrollment rows
|
||||||
|
_, _ = h.authSvc.DB.Exec(`DELETE FROM mfa_enrollments WHERE user_id = ?`, id)
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,7 +892,7 @@ func (h *Handlers) PostEditTextHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "redirect": "/view_text/" + filePath})
|
c.JSON(http.StatusOK, gin.H{"success": true, "redirect": h.config.URLPrefix + "/view_text/" + filePath})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config, store *sessions.CookieStore, authSvc *auth.Service) *Handlers {
|
func New(cfg *config.Config, store *sessions.CookieStore, authSvc *auth.Service) *Handlers {
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ func (h *Handlers) SettingsPageHandler(c *gin.Context) {
|
|||||||
"notes_tree": notesTree,
|
"notes_tree": notesTree,
|
||||||
"active_path": []string{},
|
"active_path": []string{},
|
||||||
"current_note": nil,
|
"current_note": nil,
|
||||||
"breadcrumbs": []gin.H{
|
"breadcrumbs": utils.GenerateBreadcrumbs(""),
|
||||||
{"name": "/", "url": "/"},
|
"Authenticated": isAuthenticated(c),
|
||||||
{"name": "Settings", "url": ""},
|
"IsAdmin": isAdmin(c),
|
||||||
},
|
|
||||||
"ContentTemplate": "settings_content",
|
"ContentTemplate": "settings_content",
|
||||||
"ScriptsTemplate": "settings_scripts",
|
"ScriptsTemplate": "settings_scripts",
|
||||||
"Page": "settings",
|
"Page": "settings",
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ func (r *Renderer) processObsidianImages(content string, notePath string) string
|
|||||||
imageURL = fmt.Sprintf("/serve_attached_image/%s", cleanPath)
|
imageURL = fmt.Sprintf("/serve_attached_image/%s", cleanPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefix with configured URL base
|
||||||
|
if bp := r.config.URLPrefix; bp != "" {
|
||||||
|
imageURL = bp + imageURL
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to standard markdown image syntax
|
// Convert to standard markdown image syntax
|
||||||
alt := filepath.Base(imagePath)
|
alt := filepath.Base(imagePath)
|
||||||
return fmt.Sprintf("", alt, imageURL)
|
return fmt.Sprintf("", alt, imageURL)
|
||||||
@@ -127,6 +132,9 @@ func (r *Renderer) processObsidianLinks(content string) string {
|
|||||||
// Convert note name to URL-friendly format
|
// Convert note name to URL-friendly format
|
||||||
noteURL := strings.ReplaceAll(noteName, " ", "%20")
|
noteURL := strings.ReplaceAll(noteName, " ", "%20")
|
||||||
noteURL = fmt.Sprintf("/note/%s.md", noteURL)
|
noteURL = fmt.Sprintf("/note/%s.md", noteURL)
|
||||||
|
if bp := r.config.URLPrefix; bp != "" {
|
||||||
|
noteURL = bp + noteURL
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to standard markdown link syntax
|
// Convert to standard markdown link syntax
|
||||||
return fmt.Sprintf("[%s](%s)", displayText, noteURL)
|
return fmt.Sprintf("[%s](%s)", displayText, noteURL)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func (s *Server) CSRFRequire() gin.HandlerFunc {
|
|||||||
func (s *Server) RequireAuth() gin.HandlerFunc {
|
func (s *Server) RequireAuth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if _, exists := c.Get("user_id"); !exists {
|
if _, exists := c.Get("user_id"); !exists {
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, s.config.URLPrefix+"/editor/login")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ func (s *Server) RequireAuth() gin.HandlerFunc {
|
|||||||
func (s *Server) RequireAdmin() gin.HandlerFunc {
|
func (s *Server) RequireAdmin() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if _, exists := c.Get("user_id"); !exists {
|
if _, exists := c.Get("user_id"); !exists {
|
||||||
c.Redirect(http.StatusFound, "/editor/login")
|
c.Redirect(http.StatusFound, s.config.URLPrefix+"/editor/login")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,28 +85,30 @@ func (s *Server) Start() error {
|
|||||||
|
|
||||||
func (s *Server) setupRoutes() {
|
func (s *Server) setupRoutes() {
|
||||||
h := handlers.New(s.config, s.store, s.auth)
|
h := handlers.New(s.config, s.store, s.auth)
|
||||||
|
// Group all routes under optional URL prefix
|
||||||
|
r := s.router.Group(s.config.URLPrefix)
|
||||||
|
|
||||||
// Main routes
|
// Main routes
|
||||||
s.router.GET("/", h.IndexHandler)
|
r.GET("/", h.IndexHandler)
|
||||||
s.router.GET("/folder/*path", h.FolderHandler)
|
r.GET("/folder/*path", h.FolderHandler)
|
||||||
s.router.GET("/note/*path", h.NoteHandler)
|
r.GET("/note/*path", h.NoteHandler)
|
||||||
|
|
||||||
// File serving routes
|
// File serving routes
|
||||||
s.router.GET("/serve_attached_image/*path", h.ServeAttachedImageHandler)
|
r.GET("/serve_attached_image/*path", h.ServeAttachedImageHandler)
|
||||||
s.router.GET("/serve_stored_image/:filename", h.ServeStoredImageHandler)
|
r.GET("/serve_stored_image/:filename", h.ServeStoredImageHandler)
|
||||||
s.router.GET("/download/*path", h.DownloadHandler)
|
r.GET("/download/*path", h.DownloadHandler)
|
||||||
s.router.GET("/view_text/*path", h.ViewTextHandler)
|
r.GET("/view_text/*path", h.ViewTextHandler)
|
||||||
|
|
||||||
// Auth routes
|
// Auth routes
|
||||||
s.router.GET("/editor/login", h.LoginPage)
|
r.GET("/editor/login", h.LoginPage)
|
||||||
s.router.POST("/editor/login", s.CSRFRequire(), h.LoginPost)
|
r.POST("/editor/login", s.CSRFRequire(), h.LoginPost)
|
||||||
s.router.POST("/editor/logout", s.RequireAuth(), s.CSRFRequire(), h.LogoutPost)
|
r.POST("/editor/logout", s.RequireAuth(), s.CSRFRequire(), h.LogoutPost)
|
||||||
// MFA challenge routes (no auth yet, but CSRF)
|
// MFA challenge routes (no auth yet, but CSRF)
|
||||||
s.router.GET("/editor/mfa", s.CSRFRequire(), h.MFALoginPage)
|
r.GET("/editor/mfa", s.CSRFRequire(), h.MFALoginPage)
|
||||||
s.router.POST("/editor/mfa", s.CSRFRequire(), h.MFALoginVerify)
|
r.POST("/editor/mfa", s.CSRFRequire(), h.MFALoginVerify)
|
||||||
|
|
||||||
// New /editor group protected by auth + CSRF
|
// New /editor group protected by auth + CSRF
|
||||||
editor := s.router.Group("/editor", s.RequireAuth(), s.CSRFRequire())
|
editor := r.Group("/editor", s.RequireAuth(), s.CSRFRequire())
|
||||||
{
|
{
|
||||||
editor.GET("/create", h.CreateNotePageHandler)
|
editor.GET("/create", h.CreateNotePageHandler)
|
||||||
editor.POST("/create", h.CreateNoteHandler)
|
editor.POST("/create", h.CreateNoteHandler)
|
||||||
@@ -171,8 +173,8 @@ func (s *Server) setupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
s.router.GET("/api/tree", h.TreeAPIHandler)
|
r.GET("/api/tree", h.TreeAPIHandler)
|
||||||
s.router.GET("/api/search", h.SearchHandler)
|
r.GET("/api/search", h.SearchHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) setupStaticFiles() {
|
func (s *Server) setupStaticFiles() {
|
||||||
@@ -181,9 +183,9 @@ func (s *Server) setupStaticFiles() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
s.router.StaticFS("/static", http.FS(sub))
|
s.router.StaticFS(s.config.URLPrefix+"/static", http.FS(sub))
|
||||||
// Favicon from same sub FS
|
// Favicon from same sub FS
|
||||||
s.router.StaticFileFS("/favicon.ico", "favicon.ico", http.FS(sub))
|
s.router.StaticFileFS(s.config.URLPrefix+"/favicon.ico", "favicon.ico", http.FS(sub))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) setupTemplates() {
|
func (s *Server) setupTemplates() {
|
||||||
@@ -192,6 +194,24 @@ func (s *Server) setupTemplates() {
|
|||||||
"formatSize": utils.FormatFileSize,
|
"formatSize": utils.FormatFileSize,
|
||||||
"formatTime": utils.FormatTime,
|
"formatTime": utils.FormatTime,
|
||||||
"join": strings.Join,
|
"join": strings.Join,
|
||||||
|
// base returns the configured URL prefix ("" for root)
|
||||||
|
"base": func() string { return s.config.URLPrefix },
|
||||||
|
// url joins the base with the given path ensuring single slash
|
||||||
|
"url": func(p string) string {
|
||||||
|
bp := s.config.URLPrefix
|
||||||
|
if bp == "" {
|
||||||
|
if p == "" { return "" }
|
||||||
|
if strings.HasPrefix(p, "/") { return p }
|
||||||
|
return "/" + p
|
||||||
|
}
|
||||||
|
if p == "" || p == "/" {
|
||||||
|
return bp + "/"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(p, "/") {
|
||||||
|
return bp + p
|
||||||
|
}
|
||||||
|
return bp + "/" + p
|
||||||
|
},
|
||||||
"contains": func(slice []string, item string) bool {
|
"contains": func(slice []string, item string) bool {
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
if s == item {
|
if s == item {
|
||||||
|
|||||||
@@ -64,9 +64,11 @@ function initEnhancedUpload() {
|
|||||||
|
|
||||||
// Enhanced upload function with progress tracking
|
// Enhanced upload function with progress tracking
|
||||||
function uploadFilesWithProgress(files) {
|
function uploadFilesWithProgress(files) {
|
||||||
const folderPath = window.location.pathname.includes('/folder/')
|
// Derive folder path accounting for BASE prefix
|
||||||
? window.location.pathname.replace('/folder/', '')
|
const base = (window.BASE || '').replace(/\/$/, '');
|
||||||
: '';
|
let path = window.location.pathname || '';
|
||||||
|
if (base && path.startsWith(base)) path = path.slice(base.length);
|
||||||
|
const folderPath = path.startsWith('/folder/') ? path.replace('/folder/', '') : '';
|
||||||
|
|
||||||
uploadElements.progress.classList.remove('hidden');
|
uploadElements.progress.classList.remove('hidden');
|
||||||
uploadElements.progressBar.style.width = '0%';
|
uploadElements.progressBar.style.width = '0%';
|
||||||
@@ -117,7 +119,7 @@ function initEnhancedUpload() {
|
|||||||
|
|
||||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
xhr.open('POST', '/editor/upload');
|
xhr.open('POST', window.prefix('/editor/upload'));
|
||||||
if (csrf) {
|
if (csrf) {
|
||||||
try { xhr.setRequestHeader('X-CSRF-Token', csrf); } catch (_) {}
|
try { xhr.setRequestHeader('X-CSRF-Token', csrf); } catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -210,13 +212,13 @@ function initKeyboardShortcuts() {
|
|||||||
case 'n':
|
case 'n':
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = '/editor/create';
|
window.location.href = window.prefix('/editor/create');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = '/editor/settings';
|
window.location.href = window.prefix('/editor/settings');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
formCreateUser.addEventListener('submit', async (e) => {
|
formCreateUser.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const fd = new FormData(formCreateUser);
|
const fd = new FormData(formCreateUser);
|
||||||
const res = await fetch('/editor/admin/users', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
const res = await fetch(window.prefix('/editor/admin/users'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('User created', 'success'); window.location.reload(); }
|
if (res.ok && data.success) { showNotification('User created', 'success'); window.location.reload(); }
|
||||||
else { showNotification('Create user failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Create user failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (username === 'admin') { showNotification('Cannot delete default admin user', 'error'); return; }
|
if (username === 'admin') { showNotification('Cannot delete default admin user', 'error'); return; }
|
||||||
if (!confirm('Delete user ' + username + ' ?')) return;
|
if (!confirm('Delete user ' + username + ' ?')) return;
|
||||||
const res = await fetch('/editor/admin/users/' + encodeURIComponent(id), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
const res = await fetch(window.prefix('/editor/admin/users/' + encodeURIComponent(id)), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('User deleted', 'success'); window.location.reload(); }
|
if (res.ok && data.success) { showNotification('User deleted', 'success'); window.location.reload(); }
|
||||||
else { showNotification('Delete user failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Delete user failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
const active = action === 'user-activate' ? '1' : '0';
|
const active = action === 'user-activate' ? '1' : '0';
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.set('active', active);
|
fd.set('active', active);
|
||||||
const res = await fetch('/editor/admin/users/' + encodeURIComponent(id) + '/active', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
const res = await fetch(window.prefix('/editor/admin/users/' + encodeURIComponent(id) + '/active'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('User status updated', 'success'); window.location.reload(); }
|
if (res.ok && data.success) { showNotification('User status updated', 'success'); window.location.reload(); }
|
||||||
else { showNotification('Update status failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Update status failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
// MFA actions
|
// MFA actions
|
||||||
const mfaRequest = async (row, path, okMsg) => {
|
const mfaRequest = async (row, path, okMsg) => {
|
||||||
const id = row && row.getAttribute('data-user-id');
|
const id = row && row.getAttribute('data-user-id');
|
||||||
const res = await fetch('/editor/admin/users/' + encodeURIComponent(id) + path, { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() } });
|
const res = await fetch(window.prefix('/editor/admin/users/' + encodeURIComponent(id) + path), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() } });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification(okMsg, 'success'); window.location.reload(); }
|
if (res.ok && data.success) { showNotification(okMsg, 'success'); window.location.reload(); }
|
||||||
else { showNotification('MFA action failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('MFA action failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
formCreateGroup.addEventListener('submit', async (e) => {
|
formCreateGroup.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const fd = new FormData(formCreateGroup);
|
const fd = new FormData(formCreateGroup);
|
||||||
const res = await fetch('/editor/admin/groups', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
const res = await fetch(window.prefix('/editor/admin/groups'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('Group created', 'success'); window.location.reload(); }
|
if (res.ok && data.success) { showNotification('Group created', 'success'); window.location.reload(); }
|
||||||
else { showNotification('Create group failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Create group failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (name === 'admin' || name === 'public') { showNotification('Cannot delete core group: ' + name, 'error'); return; }
|
if (name === 'admin' || name === 'public') { showNotification('Cannot delete core group: ' + name, 'error'); return; }
|
||||||
if (!confirm('Delete group ' + name + ' ?')) return;
|
if (!confirm('Delete group ' + name + ' ?')) return;
|
||||||
const res = await fetch('/editor/admin/groups/' + encodeURIComponent(id), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
const res = await fetch(window.prefix('/editor/admin/groups/' + encodeURIComponent(id)), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('Group deleted', 'success'); window.location.reload(); }
|
if (res.ok && data.success) { showNotification('Group deleted', 'success'); window.location.reload(); }
|
||||||
else { showNotification('Delete group failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Delete group failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
formAddMem.addEventListener('submit', async (e) => {
|
formAddMem.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const fd = new FormData(formAddMem);
|
const fd = new FormData(formAddMem);
|
||||||
const res = await fetch('/editor/admin/memberships/add', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
const res = await fetch(window.prefix('/editor/admin/memberships/add'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('User added to group', 'success'); }
|
if (res.ok && data.success) { showNotification('User added to group', 'success'); }
|
||||||
else { showNotification('Add membership failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Add membership failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
formRemMem.addEventListener('submit', async (e) => {
|
formRemMem.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const fd = new FormData(formRemMem);
|
const fd = new FormData(formRemMem);
|
||||||
const res = await fetch('/editor/admin/memberships/remove', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
const res = await fetch(window.prefix('/editor/admin/memberships/remove'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) { showNotification('User removed from group', 'success'); }
|
if (res.ok && data.success) { showNotification('User removed from group', 'success'); }
|
||||||
else { showNotification('Remove membership failed: ' + (data.error || res.statusText), 'error'); }
|
else { showNotification('Remove membership failed: ' + (data.error || res.statusText), 'error'); }
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
<h1 class="text-3xl font-bold text-white">Admin Logs</h1>
|
<h1 class="text-3xl font-bold text-white">Admin Logs</h1>
|
||||||
<p class="text-gray-400">Recent access, errors, failed logins, and IP bans</p>
|
<p class="text-gray-400">Recent access, errors, failed logins, and IP bans</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/editor/admin" class="btn-secondary">Back to Admin</a>
|
<a href="{{url "/editor/admin"}}" class="btn-secondary">Back to Admin</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<form method="GET" action="/editor/admin/logs" class="mb-6 bg-slate-800 border border-slate-700 rounded-lg p-4">
|
<form method="GET" action="{{url "/editor/admin/logs"}}" class="mb-6 bg-slate-800 border border-slate-700 rounded-lg p-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-3 items-end">
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-3 items-end">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-300 mb-1">IP contains</label>
|
<label class="block text-sm text-gray-300 mb-1">IP contains</label>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button type="submit" class="btn-primary">Apply</button>
|
<button type="submit" class="btn-primary">Apply</button>
|
||||||
<a href="/editor/admin/logs" class="btn-secondary">Reset</a>
|
<a href="{{url "/editor/admin/logs"}}" class="btn-secondary">Reset</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -249,7 +249,7 @@ async function postForm(url, data) {
|
|||||||
const csrf = getCSRF();
|
const csrf = getCSRF();
|
||||||
const form = new URLSearchParams();
|
const form = new URLSearchParams();
|
||||||
Object.entries(data || {}).forEach(([k, v]) => form.append(k, v));
|
Object.entries(data || {}).forEach(([k, v]) => form.append(k, v));
|
||||||
const res = await fetch(url, { method: 'POST', headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrf ? {'X-CSRF-Token': csrf} : {}), body: form.toString() });
|
const res = await fetch(window.prefix(url), { method: 'POST', headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrf ? {'X-CSRF-Token': csrf} : {}), body: form.toString() });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const j = await res.json().catch(() => ({}));
|
const j = await res.json().catch(() => ({}));
|
||||||
throw new Error(j.error || res.statusText);
|
throw new Error(j.error || res.statusText);
|
||||||
|
|||||||
@@ -264,7 +264,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="p-4 border-b border-gray-700">
|
<div class="p-4 border-b border-gray-700">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<a href="/" class="sidebar-title text-xl font-bold text-white hover:text-blue-300" title="Home">
|
<a href="{{url "/"}}" class="sidebar-title text-xl font-bold text-white hover:text-blue-300" title="Home">
|
||||||
{{.app_name}}
|
{{.app_name}}
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center space-x-2 items-center">
|
<div class="flex items-center space-x-2 items-center">
|
||||||
@@ -274,14 +274,14 @@
|
|||||||
</button>
|
</button>
|
||||||
{{if .Authenticated}}
|
{{if .Authenticated}}
|
||||||
{{if .IsAdmin}}
|
{{if .IsAdmin}}
|
||||||
<a href="/editor/admin" class="text-gray-400 hover:text-white transition-colors" title="Admin">
|
<a href="{{url "/editor/admin"}}" class="text-gray-400 hover:text-white transition-colors" title="Admin">
|
||||||
<i class="fas fa-user-shield"></i>
|
<i class="fas fa-user-shield"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a href="/editor/profile" class="text-gray-400 hover:text-white transition-colors" title="Profile">
|
<a href="{{url "/editor/profile"}}" class="text-gray-400 hover:text-white transition-colors" title="Profile">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="/editor/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
|
<a href="{{url "/editor/settings"}}" class="text-gray-400 hover:text-white transition-colors" title="Settings">
|
||||||
<i class="fas fa-gear"></i>
|
<i class="fas fa-gear"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
<i class="fas fa-right-from-bracket"></i>
|
<i class="fas fa-right-from-bracket"></i>
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/editor/login" class="text-gray-400 hover:text-white transition-colors" title="Login">
|
<a href="{{url "/editor/login"}}" class="text-gray-400 hover:text-white transition-colors" title="Login">
|
||||||
<i class="fas fa-right-to-bracket"></i>
|
<i class="fas fa-right-to-bracket"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<div class="sidebar-content px-4 py-4">
|
<div class="sidebar-content px-4 py-4">
|
||||||
{{if .Authenticated}}
|
{{if .Authenticated}}
|
||||||
<a href="/editor/create" class="btn-primary text-sm w-full text-center">
|
<a href="{{url "/editor/create"}}" class="btn-primary text-sm w-full text-center">
|
||||||
<i class="fas fa-plus mr-2"></i>New Note
|
<i class="fas fa-plus mr-2"></i>New Note
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
{{range $i, $crumb := .breadcrumbs}}
|
{{range $i, $crumb := .breadcrumbs}}
|
||||||
{{if $i}}<i class="fas fa-chevron-right text-gray-500 text-xs"></i>{{end}}
|
{{if $i}}<i class="fas fa-chevron-right text-gray-500 text-xs"></i>{{end}}
|
||||||
{{if $crumb.URL}}
|
{{if $crumb.URL}}
|
||||||
<a href="{{$crumb.URL}}" class="text-blue-400 hover:text-blue-300 transition-colors">{{$crumb.Name}}</a>
|
<a href="{{url $crumb.URL}}" class="text-blue-400 hover:text-blue-300 transition-colors">{{$crumb.Name}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="text-gray-300">{{$crumb.Name}}</span>
|
<span class="text-gray-300">{{$crumb.Name}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -401,6 +401,18 @@
|
|||||||
// Initialize syntax highlighting
|
// Initialize syntax highlighting
|
||||||
hljs.highlightAll();
|
hljs.highlightAll();
|
||||||
|
|
||||||
|
// Base URL prefix from server
|
||||||
|
window.BASE = '{{base}}';
|
||||||
|
window.prefix = function(p) {
|
||||||
|
var b = window.BASE || '';
|
||||||
|
if (!b) {
|
||||||
|
if (!p) return '';
|
||||||
|
return p[0] === '/' ? p : '/' + p;
|
||||||
|
}
|
||||||
|
if (!p || p === '/') return b + '/';
|
||||||
|
return p[0] === '/' ? (b + p) : (b + '/' + p);
|
||||||
|
};
|
||||||
|
|
||||||
// Tree functionality
|
// Tree functionality
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
if (e.target.closest('.tree-toggle')) {
|
if (e.target.closest('.tree-toggle')) {
|
||||||
@@ -423,7 +435,7 @@
|
|||||||
if (!toggle) return;
|
if (!toggle) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const path = toggle.getAttribute('data-path') || '';
|
const path = toggle.getAttribute('data-path') || '';
|
||||||
const url = '/folder/' + path;
|
const url = window.prefix('/folder/' + path);
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -522,7 +534,7 @@
|
|||||||
if (hasTree) return; // already populated
|
if (hasTree) return; // already populated
|
||||||
|
|
||||||
// Fetch tree
|
// Fetch tree
|
||||||
fetch('/api/tree')
|
fetch(window.prefix('/api/tree'))
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data || !Array.isArray(data.children)) return;
|
if (!data || !Array.isArray(data.children)) return;
|
||||||
@@ -554,13 +566,13 @@
|
|||||||
wrapper.appendChild(toggle);
|
wrapper.appendChild(toggle);
|
||||||
wrapper.appendChild(children);
|
wrapper.appendChild(children);
|
||||||
} else {
|
} else {
|
||||||
let href = '/view_text/' + (node.path || '');
|
let href = window.prefix('/view_text/' + (node.path || ''));
|
||||||
let icon = '📄';
|
let icon = '📄';
|
||||||
if ((node.type || '').toLowerCase() === 'md') {
|
if ((node.type || '').toLowerCase() === 'md') {
|
||||||
href = '/note/' + (node.path || '');
|
href = window.prefix('/note/' + (node.path || ''));
|
||||||
icon = '📝';
|
icon = '📝';
|
||||||
} else if ((node.type || '').toLowerCase() === 'image') {
|
} else if ((node.type || '').toLowerCase() === 'image') {
|
||||||
href = '/serve_attached_image/' + (node.path || '');
|
href = window.prefix('/serve_attached_image/' + (node.path || ''));
|
||||||
icon = '🖼️';
|
icon = '🖼️';
|
||||||
}
|
}
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -636,7 +648,7 @@
|
|||||||
title.className = 'flex items-center justify-between text-sm text-blue-300 hover:text-blue-200 cursor-pointer';
|
title.className = 'flex items-center justify-between text-sm text-blue-300 hover:text-blue-200 cursor-pointer';
|
||||||
title.innerHTML = `<span><i class="fas ${r.type === 'md' ? 'fa-file-lines' : 'fa-file'} mr-2"></i>${escapeHTML(r.path)}</span>`;
|
title.innerHTML = `<span><i class="fas ${r.type === 'md' ? 'fa-file-lines' : 'fa-file'} mr-2"></i>${escapeHTML(r.path)}</span>`;
|
||||||
title.addEventListener('click', () => {
|
title.addEventListener('click', () => {
|
||||||
const url = r.path.endsWith('.md') ? `/note/${r.path}` : `/view_text/${r.path}`;
|
const url = r.path.endsWith('.md') ? window.prefix(`/note/${r.path}`) : window.prefix(`/view_text/${r.path}`);
|
||||||
// remember query
|
// remember query
|
||||||
if (searchInput) localStorage.setItem(LS_KEY_QUERY, searchInput.value.trim());
|
if (searchInput) localStorage.setItem(LS_KEY_QUERY, searchInput.value.trim());
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
@@ -661,7 +673,7 @@
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
searchStatus.textContent = 'Searching...';
|
searchStatus.textContent = 'Searching...';
|
||||||
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
const res = await fetch(window.prefix(`/api/search?q=${encodeURIComponent(query)}`));
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
localStorage.setItem(LS_KEY_QUERY, query);
|
localStorage.setItem(LS_KEY_QUERY, query);
|
||||||
@@ -696,12 +708,12 @@
|
|||||||
try {
|
try {
|
||||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
const res = await fetch('/editor/logout', {
|
const res = await fetch(window.prefix('/editor/logout'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
window.location.href = '/editor/login';
|
window.location.href = window.prefix('/editor/login');
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
showNotification('Logout failed: ' + (data.error || res.statusText), 'error');
|
showNotification('Logout failed: ' + (data.error || res.statusText), 'error');
|
||||||
@@ -762,17 +774,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if eq .node.Type "md"}}
|
{{if eq .node.Type "md"}}
|
||||||
<a href="/note/{{.node.Path}}" class="sidebar-item {{if eq .current_note .node.Path}}active{{end}}">
|
<a href="{{url (print "/note/" .node.Path)}}" class="sidebar-item {{if eq .current_note .node.Path}}active{{end}}">
|
||||||
<span class="mr-2">📝</span>
|
<span class="mr-2">📝</span>
|
||||||
<span>{{.node.Name}}</span>
|
<span>{{.node.Name}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{else if eq .node.Type "image"}}
|
{{else if eq .node.Type "image"}}
|
||||||
<a href="/serve_attached_image/{{.node.Path}}" target="_blank" class="sidebar-item" title="View image in new tab">
|
<a href="{{url (print "/serve_attached_image/" .node.Path)}}" target="_blank" class="sidebar-item" title="View image in new tab">
|
||||||
<span class="mr-2">🖼️</span>
|
<span class="mr-2">🖼️</span>
|
||||||
<span>{{.node.Name}}</span>
|
<span>{{.node.Name}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/view_text/{{.node.Path}}" class="sidebar-item">
|
<a href="{{url (print "/view_text/" .node.Path)}}" class="sidebar-item">
|
||||||
<span class="mr-2">📄</span>
|
<span class="mr-2">📄</span>
|
||||||
<span>{{.node.Name}}</span>
|
<span>{{.node.Name}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ console.log('Hello, World!');
|
|||||||
|
|
||||||
function buildImageURL(filename) {
|
function buildImageURL(filename) {
|
||||||
if (imageStorageMode === 2) {
|
if (imageStorageMode === 2) {
|
||||||
return `/serve_stored_image/${filename}`;
|
return window.prefix(`/serve_stored_image/${filename}`);
|
||||||
}
|
}
|
||||||
let path = filename;
|
let path = filename;
|
||||||
if (imageStorageMode === 3 && currentFolderPath) {
|
if (imageStorageMode === 3 && currentFolderPath) {
|
||||||
@@ -119,7 +119,7 @@ console.log('Hello, World!');
|
|||||||
} else if (imageStorageMode === 4 && currentFolderPath) {
|
} else if (imageStorageMode === 4 && currentFolderPath) {
|
||||||
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
|
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
|
||||||
}
|
}
|
||||||
return `/serve_attached_image/${path}`;
|
return window.prefix(`/serve_attached_image/${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformObsidianEmbeds(md) {
|
function transformObsidianEmbeds(md) {
|
||||||
@@ -177,7 +177,7 @@ console.log('Hello, World!');
|
|||||||
// CSRF token from cookie
|
// CSRF token from cookie
|
||||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
|
||||||
fetch('/editor/create', {
|
fetch(window.prefix('/editor/create'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
@@ -263,7 +263,7 @@ console.log('Hello, World!');
|
|||||||
formData.append('path', uploadPath);
|
formData.append('path', uploadPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/upload', { method: 'POST', body: formData });
|
const resp = await fetch(window.prefix('/upload'), { method: 'POST', body: formData });
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed');
|
if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed');
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{{if .folder_path}}
|
{{if .folder_path}}
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">
|
||||||
<i class="fas fa-folder mr-2"></i>
|
<i class="fas fa-folder mr-2"></i>
|
||||||
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
<a href="{{url "/folder/"}}{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
function buildImageURL(filename) {
|
function buildImageURL(filename) {
|
||||||
// Map Obsidian embed to server URL
|
// Map Obsidian embed to server URL
|
||||||
if (imageStorageMode === 2) {
|
if (imageStorageMode === 2) {
|
||||||
return `/serve_stored_image/${filename}`;
|
return window.prefix(`/serve_stored_image/${filename}`);
|
||||||
}
|
}
|
||||||
let path = filename;
|
let path = filename;
|
||||||
if (imageStorageMode === 3 && currentFolderPath) {
|
if (imageStorageMode === 3 && currentFolderPath) {
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
} else if (imageStorageMode === 4 && currentFolderPath) {
|
} else if (imageStorageMode === 4 && currentFolderPath) {
|
||||||
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
|
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
|
||||||
}
|
}
|
||||||
return `/serve_attached_image/${path}`;
|
return window.prefix(`/serve_attached_image/${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformObsidianEmbeds(md) {
|
function transformObsidianEmbeds(md) {
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
// CSRF token from cookie
|
// CSRF token from cookie
|
||||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
|
||||||
fetch('/editor/edit/' + notePath, {
|
fetch(window.prefix('/editor/edit/' + notePath), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
formData.append('path', uploadPath);
|
formData.append('path', uploadPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/upload', { method: 'POST', body: formData });
|
const resp = await fetch(window.prefix('/upload'), { method: 'POST', body: formData });
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed');
|
if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed');
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
{{if .folder_path}}
|
{{if .folder_path}}
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">
|
||||||
<i class="fas fa-folder mr-2"></i>
|
<i class="fas fa-folder mr-2"></i>
|
||||||
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
<a href="{{url "/folder/"}}{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
formData.append('content', cm ? cm.getValue() : contentEl.value);
|
formData.append('content', cm ? cm.getValue() : contentEl.value);
|
||||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
fetch('/editor/edit_text/' + filePath, { method: 'POST', headers: csrf ? { 'X-CSRF-Token': csrf } : {}, body: formData })
|
fetch(window.prefix('/editor/edit_text/' + filePath), { method: 'POST', headers: csrf ? { 'X-CSRF-Token': csrf } : {}, body: formData })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<a href="/" class="block btn-primary">
|
<a href="{{url "/"}}" class="block btn-primary">
|
||||||
<i class="fas fa-home mr-2"></i>Go Home
|
<i class="fas fa-home mr-2"></i>Go Home
|
||||||
</a>
|
</a>
|
||||||
<button onclick="history.back()" class="block btn-secondary w-full">
|
<button onclick="history.back()" class="block btn-secondary w-full">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<button id="upload-btn" class="btn-primary">
|
<button id="upload-btn" class="btn-primary">
|
||||||
<i class="fas fa-upload mr-2"></i>Upload File
|
<i class="fas fa-upload mr-2"></i>Upload File
|
||||||
</button>
|
</button>
|
||||||
<a href="/editor/create?folder={{.folder_path}}" class="btn-secondary">
|
<a href="{{url (print "/editor/create?folder=" .folder_path)}}" class="btn-secondary">
|
||||||
<i class="fas fa-plus mr-2"></i>New Note
|
<i class="fas fa-plus mr-2"></i>New Note
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,22 +70,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
{{if eq .Type "md"}}
|
{{if eq .Type "md"}}
|
||||||
<a href="/editor/edit/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
<a href="{{url (print "/editor/edit/" .Path)}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if eq .Type "text"}}
|
{{if eq .Type "text"}}
|
||||||
<a href="/editor/edit_text/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
<a href="{{url (print "/editor/edit_text/" .Path)}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if eq .Type "image"}}
|
{{if eq .Type "image"}}
|
||||||
<a href="/serve_attached_image/{{.Path}}" target="_blank" class="text-yellow-400 hover:text-yellow-300 p-2" title="View">
|
<a href="{{url (print "/serve_attached_image/" .Path)}}" target="_blank" class="text-yellow-400 hover:text-yellow-300 p-2" title="View">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if ne .Type "dir"}}
|
{{if ne .Type "dir"}}
|
||||||
<a href="/download/{{.Path}}" class="text-green-400 hover:text-green-300 p-2" title="Download">
|
<a href="{{url (print "/download/" .Path)}}" class="text-green-400 hover:text-green-300 p-2" title="Download">
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
|
|
||||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
fetch('/editor/upload', {
|
fetch(window.prefix('/editor/upload'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
@@ -213,13 +213,13 @@
|
|||||||
const type = itemCard.dataset.type;
|
const type = itemCard.dataset.type;
|
||||||
|
|
||||||
if (type === 'dir') {
|
if (type === 'dir') {
|
||||||
window.location.href = '/folder/' + path;
|
window.location.href = window.prefix('/folder/' + path);
|
||||||
} else if (type === 'md') {
|
} else if (type === 'md') {
|
||||||
window.location.href = '/note/' + path;
|
window.location.href = window.prefix('/note/' + path);
|
||||||
} else if (type === 'image') {
|
} else if (type === 'image') {
|
||||||
window.open('/serve_attached_image/' + path, '_blank');
|
window.open(window.prefix('/serve_attached_image/' + path), '_blank');
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/view_text/' + path;
|
window.location.href = window.prefix('/view_text/' + path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
if (deleteTarget) {
|
if (deleteTarget) {
|
||||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
fetch('/editor/delete/' + deleteTarget, {
|
fetch(window.prefix('/editor/delete/' + deleteTarget), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{{if .error}}
|
{{if .error}}
|
||||||
<div class="bg-red-900/50 border border-red-700 text-red-200 rounded p-3 mb-4">{{.error}}</div>
|
<div class="bg-red-900/50 border border-red-700 text-red-200 rounded p-3 mb-4">{{.error}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<form method="POST" action="/editor/login" class="space-y-4">
|
<form method="POST" action="{{url "/editor/login"}}" class="space-y-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{.csrf_token}}" />
|
<input type="hidden" name="csrf_token" value="{{.csrf_token}}" />
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-300 mb-1" for="username">Username or Email</label>
|
<label class="block text-sm text-gray-300 mb-1" for="username">Username or Email</label>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="mb-4 p-3 rounded bg-red-700 text-white">{{.error}}</div>
|
<div class="mb-4 p-3 rounded bg-red-700 text-white">{{.error}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form id="mfa-form" class="space-y-4" method="POST" action="/editor/mfa">
|
<form id="mfa-form" class="space-y-4" method="POST" action="{{url "/editor/mfa"}}">
|
||||||
<input type="hidden" name="csrf_token" value="{{.csrf_token}}" />
|
<input type="hidden" name="csrf_token" value="{{.csrf_token}}" />
|
||||||
<div>
|
<div>
|
||||||
<label for="code" class="block text-sm font-medium text-gray-300 mb-2">Authentication code</label>
|
<label for="code" class="block text-sm font-medium text-gray-300 mb-2">Authentication code</label>
|
||||||
|
|||||||
@@ -45,11 +45,11 @@
|
|||||||
const fd = new FormData(form);
|
const fd = new FormData(form);
|
||||||
const params = new URLSearchParams(Array.from(fd.entries()));
|
const params = new URLSearchParams(Array.from(fd.entries()));
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/editor/profile/mfa/verify', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: params });
|
const res = await fetch(window.prefix('/editor/profile/mfa/verify'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: params });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
showNotification('MFA enabled', 'success');
|
showNotification('MFA enabled', 'success');
|
||||||
window.location.href = '/editor/profile';
|
window.location.href = window.prefix('/editor/profile');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || res.statusText);
|
throw new Error(data.error || res.statusText);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
<h1 class="text-3xl font-bold text-white">{{.title}}</h1>
|
<h1 class="text-3xl font-bold text-white">{{.title}}</h1>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
{{if .Authenticated}}
|
{{if .Authenticated}}
|
||||||
<a href="/editor/edit/{{.note_path}}" class="btn-primary">
|
<a href="{{url (print "/editor/edit/" .note_path)}}" class="btn-primary">
|
||||||
<i class="fas fa-edit mr-2"></i>Edit
|
<i class="fas fa-edit mr-2"></i>Edit
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a href="/download/{{.note_path}}" class="btn-secondary">
|
<a href="{{url (print "/download/" .note_path)}}" class="btn-secondary">
|
||||||
<i class="fas fa-download mr-2"></i>Download
|
<i class="fas fa-download mr-2"></i>Download
|
||||||
</a>
|
</a>
|
||||||
{{if .Authenticated}}
|
{{if .Authenticated}}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
{{if .folder_path}}
|
{{if .folder_path}}
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">
|
||||||
<i class="fas fa-folder mr-2"></i>
|
<i class="fas fa-folder mr-2"></i>
|
||||||
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
<a href="{{url (print "/folder/" .folder_path)}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -79,13 +79,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const path = deleteBtn.dataset.path;
|
const path = deleteBtn.dataset.path;
|
||||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
fetch(`/editor/delete/${path}`, {
|
fetch(window.prefix(`/editor/delete/${path}`), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.location.href = '/';
|
window.location.href = window.prefix('/');
|
||||||
} else {
|
} else {
|
||||||
alert('Error deleting note');
|
alert('Error deleting note');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
if (emailForm) emailForm.addEventListener('submit', async (e) => {
|
if (emailForm) emailForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/editor/profile/email', {
|
const res = await fetch(window.prefix('/editor/profile/email'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: Object.assign({'X-CSRF-Token': getCSRF()}),
|
headers: Object.assign({'X-CSRF-Token': getCSRF()}),
|
||||||
body: formToJSON(emailForm)
|
body: formToJSON(emailForm)
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/editor/profile/password', {
|
const res = await fetch(window.prefix('/editor/profile/password'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: Object.assign({'X-CSRF-Token': getCSRF()}),
|
headers: Object.assign({'X-CSRF-Token': getCSRF()}),
|
||||||
body: formToJSON(passwordForm)
|
body: formToJSON(passwordForm)
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
async function toggleMFA(enable) {
|
async function toggleMFA(enable) {
|
||||||
try {
|
try {
|
||||||
const url = enable ? '/editor/profile/mfa/enable' : '/editor/profile/mfa/disable';
|
const url = enable ? '/editor/profile/mfa/enable' : '/editor/profile/mfa/disable';
|
||||||
const res = await fetch(url, { method: 'POST', headers: {'X-CSRF-Token': getCSRF()} });
|
const res = await fetch(window.prefix(url), { method: 'POST', headers: {'X-CSRF-Token': getCSRF()} });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<p class="text-gray-400">Access logs and security controls</p>
|
<p class="text-gray-400">Access logs and security controls</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<a href="/editor/admin/logs" target="_blank" class="btn-secondary inline-flex items-center">
|
<a href="{{url "/editor/admin/logs"}}" target="_blank" class="btn-secondary inline-flex items-center">
|
||||||
<i class="fas fa-list mr-2"></i>View Logs
|
<i class="fas fa-list mr-2"></i>View Logs
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
// Load current settings
|
// Load current settings
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
// Load image storage settings
|
// Load image storage settings
|
||||||
fetch('/editor/settings/image_storage')
|
fetch(window.prefix('/editor/settings/image_storage'))
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.querySelector(`input[name="storage_mode"][value="${data.mode}"]`).checked = true;
|
document.querySelector(`input[name="storage_mode"][value="${data.mode}"]`).checked = true;
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
.catch(error => console.error('Error loading image storage settings:', error));
|
.catch(error => console.error('Error loading image storage settings:', error));
|
||||||
|
|
||||||
// Load notes directory settings
|
// Load notes directory settings
|
||||||
fetch('/editor/settings/notes_dir')
|
fetch(window.prefix('/editor/settings/notes_dir'))
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('notes_dir').value = data.notes_dir || '';
|
document.getElementById('notes_dir').value = data.notes_dir || '';
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
.catch(error => console.error('Error loading notes directory settings:', error));
|
.catch(error => console.error('Error loading notes directory settings:', error));
|
||||||
|
|
||||||
// Load file extensions settings
|
// Load file extensions settings
|
||||||
fetch('/editor/settings/file_extensions')
|
fetch(window.prefix('/editor/settings/file_extensions'))
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('allowed_image_extensions').value = data.allowed_image_extensions || '';
|
document.getElementById('allowed_image_extensions').value = data.allowed_image_extensions || '';
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
.catch(error => console.error('Error loading file extensions settings:', error));
|
.catch(error => console.error('Error loading file extensions settings:', error));
|
||||||
|
|
||||||
// Load security settings
|
// Load security settings
|
||||||
fetch('/editor/settings/security')
|
fetch(window.prefix('/editor/settings/security'))
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('pwd_failures_threshold').value = data.pwd_failures_threshold ?? '';
|
document.getElementById('pwd_failures_threshold').value = data.pwd_failures_threshold ?? '';
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
|
|
||||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
|
||||||
fetch('/editor/settings/security', {
|
fetch(window.prefix('/editor/settings/security'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
@@ -348,7 +348,7 @@
|
|||||||
|
|
||||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
|
||||||
fetch('/editor/settings/image_storage', {
|
fetch(window.prefix('/editor/settings/image_storage'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
@@ -377,7 +377,7 @@
|
|||||||
|
|
||||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
|
||||||
fetch('/editor/settings/notes_dir', {
|
fetch(window.prefix('/editor/settings/notes_dir'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
|
|
||||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
|
||||||
fetch('/editor/settings/file_extensions', {
|
fetch(window.prefix('/editor/settings/file_extensions'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
body: formData
|
body: formData
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
<h1 class="text-3xl font-bold text-white">{{.file_name}}</h1>
|
<h1 class="text-3xl font-bold text-white">{{.file_name}}</h1>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
{{if and .Authenticated .is_editable}}
|
{{if and .Authenticated .is_editable}}
|
||||||
<a href="/edit_text/{{.file_path}}" class="btn-primary">
|
<a href="{{url (print "/editor/edit_text/" .file_path)}}" class="btn-primary">
|
||||||
<i class="fas fa-edit mr-2"></i>Edit
|
<i class="fas fa-edit mr-2"></i>Edit
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a href="/download/{{.file_path}}" class="btn-secondary">
|
<a href="{{url (print "/download/" .file_path)}}" class="btn-secondary">
|
||||||
<i class="fas fa-download mr-2"></i>Download
|
<i class="fas fa-download mr-2"></i>Download
|
||||||
</a>
|
</a>
|
||||||
{{if .Authenticated}}
|
{{if .Authenticated}}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
{{if .folder_path}}
|
{{if .folder_path}}
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">
|
||||||
<i class="fas fa-folder mr-2"></i>
|
<i class="fas fa-folder mr-2"></i>
|
||||||
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
<a href="{{url (print "/folder/" .folder_path)}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,8 +72,11 @@
|
|||||||
|
|
||||||
document.getElementById('confirm-delete').addEventListener('click', function() {
|
document.getElementById('confirm-delete').addEventListener('click', function() {
|
||||||
if (deleteTarget) {
|
if (deleteTarget) {
|
||||||
fetch('/delete/' + deleteTarget, {
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||||
method: 'DELETE'
|
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||||
|
fetch(window.prefix('/editor/delete/' + deleteTarget), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -82,9 +85,9 @@
|
|||||||
// Redirect to folder or root
|
// Redirect to folder or root
|
||||||
const folderPath = '{{.folder_path}}';
|
const folderPath = '{{.folder_path}}';
|
||||||
if (folderPath) {
|
if (folderPath) {
|
||||||
window.location.href = '/folder/' + folderPath;
|
window.location.href = window.prefix('/folder/' + folderPath);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/';
|
window.location.href = window.prefix('/');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || 'Delete failed');
|
throw new Error(data.error || 'Delete failed');
|
||||||
|
|||||||
Reference in New Issue
Block a user