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
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user