Complete TODO items: security, features, polish

Security:
- Encrypt Gitea tokens at rest (AES-256-GCM with MH_SECRET)
- Secure cookie flag when behind HTTPS (X-Forwarded-Proto)
- Password complexity (min 8 chars)
- TOTP: defer persist until verified (totp_pending column)
- Audit log table + logging on login/rename/password change

Features:
- Rename files/folders (double-click in tree, /api/files/rename)
- beforeunload warning for unsaved changes
- Mobile hamburger menu
- PWA icons (192px, 512px)
- Max file size enforcement (10MB)
- Shared file read access (cross-user with permission check)

Polish:
- Toast notifications replace all alert() calls
- Keyboard shortcut help overlay (Ctrl+/)
- File rename via double-click in FileTree
This commit is contained in:
2026-05-26 23:51:02 +02:00
parent f60d223c06
commit 68eaee0b9f
12 changed files with 310 additions and 49 deletions
+18 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/google/uuid"
"markdownhub/internal/crypto"
"markdownhub/internal/files"
)
@@ -39,10 +40,18 @@ func (s *Server) handleBuildSubmit(w http.ResponseWriter, r *http.Request) {
}
jobID := uuid.New().String()
// Encrypt Gitea token at rest
encToken := req.GiteaToken
if encToken != "" {
key := crypto.KeyFromSecret(s.secret)
if enc, err := crypto.EncryptString(encToken, key); err == nil {
encToken = enc
}
}
_, err := s.db.Exec(
`INSERT INTO build_jobs (id, user_id, status, spec_content, gitea_url, gitea_token, gitea_org, repo_name, model)
VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?)`,
jobID, userID, specContent, req.GiteaURL, req.GiteaToken, req.GiteaOrg, req.RepoName, req.Model,
jobID, userID, specContent, req.GiteaURL, encToken, req.GiteaOrg, req.RepoName, req.Model,
)
if err != nil {
writeJSON(w, 500, map[string]string{"error": "failed to create job"})
@@ -133,6 +142,14 @@ func (s *Server) handleDaemonPoll(w http.ResponseWriter, r *http.Request) {
// Mark as picked up
s.db.Exec("UPDATE build_jobs SET status = 'running', updated_at = datetime('now') WHERE id = ?", id)
// Decrypt token
if giteaToken != "" {
key := crypto.KeyFromSecret(s.secret)
if dec, err := crypto.DecryptString(giteaToken, key); err == nil {
giteaToken = dec
}
}
writeJSON(w, 200, map[string]interface{}{
"job_id": id,
"spec_content": specContent,