68eaee0b9f
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
80 lines
2.2 KiB
Go
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"})
|
|
}
|