fix view
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@ __pycache__/
|
|||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
|
|
||||||
|
./gobsidian
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gobsidian/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionCookieName = "gobsidian_session"
|
const sessionCookieName = "gobsidian_session"
|
||||||
@@ -26,9 +27,12 @@ func (h *Handlers) LoginPage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
token, _ := c.Get("csrf_token")
|
token, _ := c.Get("csrf_token")
|
||||||
|
// propagate return_to if provided
|
||||||
|
returnTo := c.Query("return_to")
|
||||||
c.HTML(http.StatusOK, "login", gin.H{
|
c.HTML(http.StatusOK, "login", gin.H{
|
||||||
"app_name": h.config.AppName,
|
"app_name": h.config.AppName,
|
||||||
"csrf_token": token,
|
"csrf_token": token,
|
||||||
|
"return_to": returnTo,
|
||||||
"ContentTemplate": "login_content",
|
"ContentTemplate": "login_content",
|
||||||
"ScriptsTemplate": "login_scripts",
|
"ScriptsTemplate": "login_scripts",
|
||||||
"Page": "login",
|
"Page": "login",
|
||||||
@@ -192,8 +196,17 @@ func (h *Handlers) MFALoginVerify(c *gin.Context) {
|
|||||||
// success: set user_id and clear mfa_user_id
|
// success: set user_id and clear mfa_user_id
|
||||||
delete(session.Values, "mfa_user_id")
|
delete(session.Values, "mfa_user_id")
|
||||||
session.Values["user_id"] = uid
|
session.Values["user_id"] = uid
|
||||||
|
// use return_to if set in session
|
||||||
|
var dest string
|
||||||
|
if v, ok := session.Values["return_to"].(string); ok {
|
||||||
|
dest = sanitizeReturnTo(h.config.URLPrefix, v)
|
||||||
|
delete(session.Values, "return_to")
|
||||||
|
}
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
|
if dest == "" {
|
||||||
|
dest = h.config.URLPrefix + "/"
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusFound, dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfileMFASetupPage shows QR and input to verify during enrollment
|
// ProfileMFASetupPage shows QR and input to verify during enrollment
|
||||||
@@ -223,9 +236,16 @@ func (h *Handlers) ProfileMFASetupPage(c *gin.Context) {
|
|||||||
label := url.PathEscape(fmt.Sprintf("%s:%s", issuer, username))
|
label := url.PathEscape(fmt.Sprintf("%s:%s", issuer, username))
|
||||||
otpauth := fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=%s&digits=6&period=30&algorithm=SHA1", label, secret, url.QueryEscape(issuer))
|
otpauth := fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=%s&digits=6&period=30&algorithm=SHA1", label, secret, url.QueryEscape(issuer))
|
||||||
|
|
||||||
// Render simple page (uses base.html shell)
|
// Build sidebar tree for consistent UI and pass auth flags
|
||||||
|
notesTree, _ := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
|
||||||
c.HTML(http.StatusOK, "mfa_setup", gin.H{
|
c.HTML(http.StatusOK, "mfa_setup", gin.H{
|
||||||
"app_name": h.config.AppName,
|
"app_name": h.config.AppName,
|
||||||
|
"notes_tree": notesTree,
|
||||||
|
"active_path": []string{},
|
||||||
|
"current_note": nil,
|
||||||
|
"breadcrumbs": utils.GenerateBreadcrumbs(""),
|
||||||
|
"Authenticated": true,
|
||||||
|
"IsAdmin": isAdmin(c),
|
||||||
"Secret": secret,
|
"Secret": secret,
|
||||||
"OTPAuthURI": otpauth,
|
"OTPAuthURI": otpauth,
|
||||||
"ContentTemplate": "mfa_setup_content",
|
"ContentTemplate": "mfa_setup_content",
|
||||||
@@ -313,6 +333,7 @@ func verifyTOTP(base32Secret, code string, t time.Time) bool {
|
|||||||
func (h *Handlers) LoginPost(c *gin.Context) {
|
func (h *Handlers) LoginPost(c *gin.Context) {
|
||||||
username := c.PostForm("username")
|
username := c.PostForm("username")
|
||||||
password := c.PostForm("password")
|
password := c.PostForm("password")
|
||||||
|
returnTo := strings.TrimSpace(c.PostForm("return_to"))
|
||||||
|
|
||||||
user, err := h.authSvc.Authenticate(username, password)
|
user, err := h.authSvc.Authenticate(username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -323,6 +344,7 @@ func (h *Handlers) LoginPost(c *gin.Context) {
|
|||||||
"app_name": h.config.AppName,
|
"app_name": h.config.AppName,
|
||||||
"csrf_token": token,
|
"csrf_token": token,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
|
"return_to": returnTo,
|
||||||
"ContentTemplate": "login_content",
|
"ContentTemplate": "login_content",
|
||||||
"ScriptsTemplate": "login_scripts",
|
"ScriptsTemplate": "login_scripts",
|
||||||
"Page": "login",
|
"Page": "login",
|
||||||
@@ -333,28 +355,30 @@ func (h *Handlers) LoginPost(c *gin.Context) {
|
|||||||
if user.MFASecret.Valid && user.MFASecret.String != "" {
|
if user.MFASecret.Valid && user.MFASecret.String != "" {
|
||||||
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
|
||||||
|
if rt := sanitizeReturnTo(h.config.URLPrefix, returnTo); rt != "" {
|
||||||
|
session.Values["return_to"] = rt
|
||||||
|
}
|
||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/mfa")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/mfa")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If admin created an enrollment for this user, force MFA setup after login
|
// Do NOT automatically force MFA setup just because an enrollment row exists.
|
||||||
var pending int
|
// Some deployments may leave stale enrollment rows; we only require MFA when
|
||||||
if err := h.authSvc.DB.QueryRow(`SELECT 1 FROM mfa_enrollments WHERE user_id = ?`, user.ID).Scan(&pending); err == nil {
|
// the user actually has MFA enabled (mfa_secret set) or when they explicitly
|
||||||
// normal login, then redirect to setup
|
// navigate to setup from profile.
|
||||||
session, _ := h.store.Get(c.Request, sessionCookieName)
|
|
||||||
session.Values["user_id"] = user.ID
|
|
||||||
_ = session.Save(c.Request, c.Writer)
|
|
||||||
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/profile/mfa/setup")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create normal session
|
// Create normal session
|
||||||
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, h.config.URLPrefix+"/")
|
// Redirect to requested page if provided and safe; otherwise home
|
||||||
|
if rt := sanitizeReturnTo(h.config.URLPrefix, returnTo); rt != "" {
|
||||||
|
c.Redirect(http.StatusFound, rt)
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutPost clears the session
|
// LogoutPost clears the session
|
||||||
@@ -364,3 +388,34 @@ func (h *Handlers) LogoutPost(c *gin.Context) {
|
|||||||
_ = session.Save(c.Request, c.Writer)
|
_ = session.Save(c.Request, c.Writer)
|
||||||
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitizeReturnTo ensures the provided return_to is a safe in-app path.
|
||||||
|
// It rejects absolute URLs and protocol-relative URLs. When URLPrefix is set,
|
||||||
|
// it enforces that the destination stays within that prefix; if a bare
|
||||||
|
// "/..." path is provided, it will be rewritten to include the prefix.
|
||||||
|
func sanitizeReturnTo(prefix, v string) string {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Disallow absolute and protocol-relative URLs
|
||||||
|
if strings.HasPrefix(v, "//") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if u, err := url.Parse(v); err != nil || (u != nil && u.IsAbs()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Must be a path
|
||||||
|
if !strings.HasPrefix(v, "/") {
|
||||||
|
v = "/" + v
|
||||||
|
}
|
||||||
|
// Enforce prefix containment when configured
|
||||||
|
if prefix != "" {
|
||||||
|
if strings.HasPrefix(v, prefix+"/") || v == prefix || v == prefix+"/" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
// If it's a root-relative path without prefix, rewrite into prefix
|
||||||
|
return prefix + v
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -84,7 +85,17 @@ 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, s.config.URLPrefix+"/editor/login")
|
// Attach return_to so user can be redirected back after login
|
||||||
|
requested := c.Request.URL.RequestURI()
|
||||||
|
q := url.Values{}
|
||||||
|
if requested != "" {
|
||||||
|
q.Set("return_to", requested)
|
||||||
|
}
|
||||||
|
loginURL := s.config.URLPrefix + "/editor/login"
|
||||||
|
if qs := q.Encode(); qs != "" {
|
||||||
|
loginURL = loginURL + "?" + qs
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusFound, loginURL)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,7 +107,16 @@ 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, s.config.URLPrefix+"/editor/login")
|
requested := c.Request.URL.RequestURI()
|
||||||
|
q := url.Values{}
|
||||||
|
if requested != "" {
|
||||||
|
q.Set("return_to", requested)
|
||||||
|
}
|
||||||
|
loginURL := s.config.URLPrefix + "/editor/login"
|
||||||
|
if qs := q.Encode(); qs != "" {
|
||||||
|
loginURL = loginURL + "?" + qs
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusFound, loginURL)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,13 +328,18 @@
|
|||||||
<!-- Breadcrumbs -->
|
<!-- Breadcrumbs -->
|
||||||
{{if .breadcrumbs}}
|
{{if .breadcrumbs}}
|
||||||
<div class="bg-slate-800 border-b border-gray-700 px-6 py-3">
|
<div class="bg-slate-800 border-b border-gray-700 px-6 py-3">
|
||||||
<nav class="flex items-center space-x-2 text-sm">
|
<nav class="flex items-center flex-wrap gap-1.5 text-sm">
|
||||||
{{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 mx-1"></i>{{end}}
|
||||||
{{if $crumb.URL}}
|
{{if $crumb.URL}}
|
||||||
<a href="{{url $crumb.URL}}" class="text-blue-400 hover:text-blue-300 transition-colors">{{$crumb.Name}}</a>
|
<a href="{{url $crumb.URL}}" class="inline-flex items-center px-2.5 py-1 rounded-md border border-slate-600 bg-slate-700/40 text-blue-300 hover:bg-slate-700 hover:text-blue-200 transition-colors" aria-label="Breadcrumb: {{$crumb.Name}}">
|
||||||
|
{{if and (eq $i 0) (eq $crumb.Name "/")}}<i class="fas fa-folder-tree mr-1.5"></i>{{end}}
|
||||||
|
<span class="leading-none">{{$crumb.Name}}</span>
|
||||||
|
</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="text-gray-300">{{$crumb.Name}}</span>
|
<span class="inline-flex items-center px-2.5 py-1 rounded-md border border-slate-600 bg-slate-700/60 text-gray-200">
|
||||||
|
<span class="leading-none">{{$crumb.Name}}</span>
|
||||||
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -263,7 +263,12 @@ console.log('Hello, World!');
|
|||||||
formData.append('path', uploadPath);
|
formData.append('path', uploadPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(window.prefix('/upload'), { method: 'POST', body: formData });
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
const resp = await fetch(window.prefix('/editor/upload'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
|
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');
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,12 @@
|
|||||||
formData.append('path', uploadPath);
|
formData.append('path', uploadPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(window.prefix('/upload'), { method: 'POST', body: formData });
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||||
|
const resp = await fetch(window.prefix('/editor/upload'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||||
|
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');
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Authenticated}}
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<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
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
<i class="fas fa-plus mr-2"></i>New Note
|
<i class="fas fa-plus mr-2"></i>New Note
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload Area (hidden by default) -->
|
<!-- Upload Area (hidden by default) -->
|
||||||
@@ -69,15 +71,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
{{if eq .Type "md"}}
|
{{if $.Authenticated}}
|
||||||
<a href="{{url (print "/editor/edit/" .Path)}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
{{if eq .Type "md"}}
|
||||||
<i class="fas fa-edit"></i>
|
<a href="{{url (print "/editor/edit/" .Path)}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||||
</a>
|
<i class="fas fa-edit"></i>
|
||||||
{{end}}
|
</a>
|
||||||
{{if eq .Type "text"}}
|
{{end}}
|
||||||
<a href="{{url (print "/editor/edit_text/" .Path)}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
{{if eq .Type "text"}}
|
||||||
<i class="fas fa-edit"></i>
|
<a href="{{url (print "/editor/edit_text/" .Path)}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||||
</a>
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if eq .Type "image"}}
|
{{if eq .Type "image"}}
|
||||||
<a href="{{url (print "/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">
|
||||||
@@ -89,9 +93,11 @@
|
|||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button class="text-red-400 hover:text-red-300 p-2 delete-btn" data-path="{{.Path}}" title="Delete">
|
{{if $.Authenticated}}
|
||||||
<i class="fas fa-trash"></i>
|
<button class="text-red-400 hover:text-red-300 p-2 delete-btn" data-path="{{.Path}}" title="Delete">
|
||||||
</button>
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,39 +138,47 @@
|
|||||||
let deleteTarget = null;
|
let deleteTarget = null;
|
||||||
|
|
||||||
// Toggle upload area
|
// Toggle upload area
|
||||||
uploadBtn.addEventListener('click', function() {
|
if (uploadBtn) {
|
||||||
uploadArea.classList.toggle('hidden');
|
uploadBtn.addEventListener('click', function() {
|
||||||
});
|
uploadArea && uploadArea.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// File selection
|
// File selection
|
||||||
selectFilesBtn.addEventListener('click', function() {
|
if (selectFilesBtn && fileInput) {
|
||||||
fileInput.click();
|
selectFilesBtn.addEventListener('click', function() {
|
||||||
});
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fileInput.addEventListener('change', function() {
|
if (fileInput) {
|
||||||
if (this.files.length > 0) {
|
fileInput.addEventListener('change', function() {
|
||||||
uploadFiles(this.files);
|
if (this.files.length > 0) {
|
||||||
}
|
uploadFiles(this.files);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Drag and drop
|
// Drag and drop
|
||||||
uploadArea.addEventListener('dragover', function(e) {
|
if (uploadArea) {
|
||||||
e.preventDefault();
|
uploadArea.addEventListener('dragover', function(e) {
|
||||||
this.classList.add('dragover');
|
e.preventDefault();
|
||||||
});
|
this.classList.add('dragover');
|
||||||
|
});
|
||||||
|
|
||||||
uploadArea.addEventListener('dragleave', function(e) {
|
uploadArea.addEventListener('dragleave', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.classList.remove('dragover');
|
this.classList.remove('dragover');
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadArea.addEventListener('drop', function(e) {
|
uploadArea.addEventListener('drop', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.classList.remove('dragover');
|
this.classList.remove('dragover');
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
uploadFiles(e.dataTransfer.files);
|
uploadFiles(e.dataTransfer.files);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Upload files function
|
// Upload files function
|
||||||
function uploadFiles(files) {
|
function uploadFiles(files) {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<form method="POST" action="{{url "/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}}" />
|
||||||
|
{{if .return_to}}
|
||||||
|
<input type="hidden" name="return_to" value="{{.return_to}}" />
|
||||||
|
{{end}}
|
||||||
<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>
|
||||||
<input id="username" name="username" type="text" required class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white" />
|
<input id="username" name="username" type="text" required class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white" />
|
||||||
|
|||||||
Reference in New Issue
Block a user