Files
anders 4df87cbf9a Phase 2-6: Git sync, sharing, 2FA, AI integration
- Git: init, commit, log, diff, restore, remotes, push/pull
- Auto-commit on every file save
- Sharing: share/unshare files with other users (ro/rw)
- Shared documents view in sidebar
- 2FA: TOTP setup/verify/disable, enforced at login
- AI: verify spec endpoint (LiteLLM), generate (summarize/prompt/expand)
- Light/dark theme with CSS variables
- File delete (recursive for folders)
- Admin panel + preferences panel
- File creation timestamp display
2026-05-22 19:53:24 +02:00

140 lines
3.9 KiB
Go

package api
import (
"net/http"
"markdownhub/internal/files"
"github.com/google/uuid"
)
func (s *Server) handleShareFile(w http.ResponseWriter, r *http.Request) {
var req struct {
Path string `json:"path"`
UserID string `json:"user_id"`
Username string `json:"username"`
Level string `json:"level"` // "ro" or "rw"
}
if err := decodeBody(r, &req); err != nil || req.Path == "" || req.Level == "" {
writeJSON(w, 400, map[string]string{"error": "path, user_id/username, and level required"})
return
}
if req.Level != "ro" && req.Level != "rw" {
writeJSON(w, 400, map[string]string{"error": "level must be 'ro' or 'rw'"})
return
}
ownerID := getUserID(r)
// Resolve username to user_id if needed
targetUserID := req.UserID
if targetUserID == "" && req.Username != "" {
err := s.db.QueryRow("SELECT id FROM users WHERE username = ?", req.Username).Scan(&targetUserID)
if err != nil {
writeJSON(w, 404, map[string]string{"error": "user not found"})
return
}
}
if targetUserID == "" {
writeJSON(w, 400, map[string]string{"error": "user_id or username required"})
return
}
// Get or create file record
fileID := ""
err := s.db.QueryRow("SELECT id FROM files WHERE owner_id = ? AND path = ?", ownerID, req.Path).Scan(&fileID)
if err != nil {
// Create file record
fileID = uuid.New().String()
s.db.Exec("INSERT INTO files (id, owner_id, path) VALUES (?, ?, ?)", fileID, ownerID, req.Path)
}
// Upsert permission
permID := uuid.New().String()
_, err = s.db.Exec(
`INSERT INTO permissions (id, file_id, user_id, level, granted_by)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(file_id, user_id) DO UPDATE SET level = ?`,
permID, fileID, targetUserID, req.Level, ownerID, req.Level,
)
if err != nil {
writeJSON(w, 500, map[string]string{"error": "share failed"})
return
}
writeJSON(w, 200, map[string]string{"status": "shared"})
}
func (s *Server) handleUnshareFile(w http.ResponseWriter, r *http.Request) {
var req struct {
Path string `json:"path"`
UserID string `json:"user_id"`
}
if err := decodeBody(r, &req); err != nil || req.Path == "" || req.UserID == "" {
writeJSON(w, 400, map[string]string{"error": "path and user_id required"})
return
}
ownerID := getUserID(r)
fileID := ""
s.db.QueryRow("SELECT id FROM files WHERE owner_id = ? AND path = ?", ownerID, req.Path).Scan(&fileID)
if fileID == "" {
writeJSON(w, 404, map[string]string{"error": "file not found"})
return
}
s.db.Exec("DELETE FROM permissions WHERE file_id = ? AND user_id = ?", fileID, req.UserID)
writeJSON(w, 200, map[string]string{"status": "unshared"})
}
func (s *Server) handleListSharedFiles(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
// Files shared WITH me
rows, err := s.db.Query(`
SELECT f.path, f.owner_id, u.username, p.level
FROM permissions p
JOIN files f ON f.id = p.file_id
JOIN users u ON u.id = f.owner_id
WHERE p.user_id = ?
`, userID)
if err != nil {
writeJSON(w, 200, []files.FileInfo{})
return
}
defer rows.Close()
var shared []map[string]string
for rows.Next() {
var path, ownerID, ownerName, level string
rows.Scan(&path, &ownerID, &ownerName, &level)
shared = append(shared, map[string]string{
"path": path, "owner": ownerName, "level": level, "owner_id": ownerID,
})
}
// Files I shared with others
rows2, err := s.db.Query(`
SELECT f.path, p.user_id, u.username, p.level
FROM permissions p
JOIN files f ON f.id = p.file_id
JOIN users u ON u.id = p.user_id
WHERE f.owner_id = ?
`, userID)
if err == nil {
defer rows2.Close()
for rows2.Next() {
var path, sharedWith, sharedWithName, level string
rows2.Scan(&path, &sharedWith, &sharedWithName, &level)
shared = append(shared, map[string]string{
"path": path, "shared_with": sharedWithName, "level": level, "type": "outgoing",
})
}
}
if shared == nil {
shared = []map[string]string{}
}
writeJSON(w, 200, shared)
}