Files
anders 68eaee0b9f 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
2026-05-26 23:51:02 +02:00

80 lines
2.2 KiB
Go

package api
import (
"net/http"
"markdownhub/internal/auth"
)
func (s *Server) handleTOTPSetup(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
secret := auth.GenerateTOTPSecret()
var email string
s.db.QueryRow("SELECT email FROM users WHERE id = ?", userID).Scan(&email)
uri := auth.TOTPUri(secret, email, "MarkdownHub")
// Store in pending column — not active until verified
s.db.Exec("UPDATE users SET totp_pending = ? WHERE id = ?", secret, userID)
writeJSON(w, 200, map[string]string{
"secret": secret,
"uri": uri,
})
}
func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
var req struct {
Code string `json:"code"`
}
if err := decodeBody(r, &req); err != nil || req.Code == "" {
writeJSON(w, 400, map[string]string{"error": "code required"})
return
}
userID := getUserID(r)
var pending *string
s.db.QueryRow("SELECT totp_pending FROM users WHERE id = ?", userID).Scan(&pending)
if pending == nil || *pending == "" {
writeJSON(w, 400, map[string]string{"error": "TOTP not set up — run setup first"})
return
}
if !auth.ValidateTOTP(*pending, req.Code) {
writeJSON(w, 401, map[string]string{"error": "invalid code"})
return
}
// Code valid — promote pending to active
s.db.Exec("UPDATE users SET totp_secret = ?, totp_pending = NULL WHERE id = ?", *pending, userID)
writeJSON(w, 200, map[string]string{"status": "verified"})
}
func (s *Server) handleTOTPDisable(w http.ResponseWriter, r *http.Request) {
var req struct {
Code string `json:"code"`
}
if err := decodeBody(r, &req); err != nil || req.Code == "" {
writeJSON(w, 400, map[string]string{"error": "code required"})
return
}
userID := getUserID(r)
var secret *string
s.db.QueryRow("SELECT totp_secret FROM users WHERE id = ?", userID).Scan(&secret)
if secret == nil || *secret == "" {
writeJSON(w, 400, map[string]string{"error": "2FA not enabled"})
return
}
if !auth.ValidateTOTP(*secret, req.Code) {
writeJSON(w, 401, map[string]string{"error": "invalid code"})
return
}
s.db.Exec("UPDATE users SET totp_secret = NULL, totp_pending = NULL WHERE id = ?", userID)
writeJSON(w, 200, map[string]string{"status": "disabled"})
}