package api import ( "encoding/json" "net/http" "time" "github.com/google/uuid" "markdownhub/internal/auth" "markdownhub/internal/files" "markdownhub/internal/git" ) func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func decodeBody(r *http.Request, v interface{}) error { defer r.Body.Close() return json.NewDecoder(r.Body).Decode(v) } // ─── Auth ──────────────────────────────────────────────────────────────────── func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` Password string `json:"password"` TOTPCode string `json:"totp_code"` } if err := decodeBody(r, &req); err != nil { writeJSON(w, 400, map[string]string{"error": "invalid request"}) return } var id, hash string var isAdmin bool var totpSecret *string err := s.db.QueryRow( "SELECT id, password_hash, is_admin, totp_secret FROM users WHERE email = ?", req.Email, ).Scan(&id, &hash, &isAdmin, &totpSecret) if err != nil || !auth.CheckPassword(hash, req.Password) { writeJSON(w, 401, map[string]string{"error": "invalid credentials"}) return } // Check TOTP if enabled if totpSecret != nil && *totpSecret != "" { if req.TOTPCode == "" { writeJSON(w, 401, map[string]string{"error": "totp_required"}) return } if !auth.ValidateTOTP(*totpSecret, req.TOTPCode) { writeJSON(w, 401, map[string]string{"error": "invalid TOTP code"}) return } } token, err := auth.CreateToken(id, isAdmin, s.secret) if err != nil { writeJSON(w, 500, map[string]string{"error": "token creation failed"}) return } http.SetCookie(w, &http.Cookie{ Name: "authToken", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(72 * time.Hour), }) writeJSON(w, 200, map[string]interface{}{ "token": token, "userId": id, "isAdmin": isAdmin, }) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: "authToken", Value: "", Path: "/", HttpOnly: true, MaxAge: -1, }) writeJSON(w, 200, map[string]string{"status": "ok"}) } func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) { var req struct { CurrentPassword string `json:"current_password"` NewPassword string `json:"new_password"` } if err := decodeBody(r, &req); err != nil || req.CurrentPassword == "" || req.NewPassword == "" { writeJSON(w, 400, map[string]string{"error": "current_password and new_password required"}) return } userID := getUserID(r) var hash string s.db.QueryRow("SELECT password_hash FROM users WHERE id = ?", userID).Scan(&hash) if !auth.CheckPassword(hash, req.CurrentPassword) { writeJSON(w, 401, map[string]string{"error": "current password is incorrect"}) return } newHash, err := auth.HashPassword(req.NewPassword) if err != nil { writeJSON(w, 500, map[string]string{"error": "failed to hash password"}) return } s.db.Exec("UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?", newHash, userID) writeJSON(w, 200, map[string]string{"status": "password changed"}) } // ─── Users ─────────────────────────────────────────────────────────────────── func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { var req struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` IsAdmin bool `json:"isAdmin"` } if err := decodeBody(r, &req); err != nil || req.Email == "" || req.Password == "" || req.Username == "" { writeJSON(w, 400, map[string]string{"error": "username, email, and password required"}) return } hash, err := auth.HashPassword(req.Password) if err != nil { writeJSON(w, 500, map[string]string{"error": "failed to hash password"}) return } id := uuid.New().String() _, err = s.db.Exec( "INSERT INTO users (id, username, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)", id, req.Username, req.Email, hash, req.IsAdmin, ) if err != nil { writeJSON(w, 409, map[string]string{"error": "user already exists"}) return } // Create user's file directory files.EnsureUserDir(s.dataDir, id) writeJSON(w, 201, map[string]interface{}{"id": id, "username": req.Username, "email": req.Email}) } func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { rows, err := s.db.Query("SELECT id, username, email, is_admin, created_at FROM users") if err != nil { writeJSON(w, 500, map[string]string{"error": "query failed"}) return } defer rows.Close() var users []map[string]interface{} for rows.Next() { var id, username, email, createdAt string var isAdmin bool rows.Scan(&id, &username, &email, &isAdmin, &createdAt) users = append(users, map[string]interface{}{ "id": id, "username": username, "email": email, "isAdmin": isAdmin, "createdAt": createdAt, }) } if users == nil { users = []map[string]interface{}{} } writeJSON(w, 200, users) } // ─── Files ─────────────────────────────────────────────────────────────────── func (s *Server) handleListFiles(w http.ResponseWriter, r *http.Request) { userID := getUserID(r) tree, err := files.ListTree(s.dataDir, userID) if err != nil { writeJSON(w, 500, map[string]string{"error": "failed to list files"}) return } if tree == nil { tree = []files.FileInfo{} } writeJSON(w, 200, tree) } func (s *Server) handleReadFile(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` } if err := decodeBody(r, &req); err != nil || req.Path == "" { writeJSON(w, 400, map[string]string{"error": "path required"}) return } userID := getUserID(r) content, err := files.ReadFile(s.dataDir, userID, req.Path) if err != nil { writeJSON(w, 404, map[string]string{"error": "file not found"}) return } created := files.GetCreatedTime(s.dataDir, userID, req.Path) writeJSON(w, 200, map[string]interface{}{"path": req.Path, "content": content, "created": created}) } func (s *Server) handleWriteFile(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` Content string `json:"content"` } if err := decodeBody(r, &req); err != nil || req.Path == "" { writeJSON(w, 400, map[string]string{"error": "path required"}) return } userID := getUserID(r) if err := files.WriteFile(s.dataDir, userID, req.Path, req.Content); err != nil { writeJSON(w, 500, map[string]string{"error": "write failed"}) return } // Auto-commit on save go func() { git.InitRepo(s.dataDir, userID) git.AutoCommit(s.dataDir, userID, req.Path) }() writeJSON(w, 200, map[string]string{"status": "ok", "path": req.Path}) } func (s *Server) handleCreateFile(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` Content string `json:"content"` } if err := decodeBody(r, &req); err != nil || req.Path == "" { writeJSON(w, 400, map[string]string{"error": "path required"}) return } userID := getUserID(r) if err := files.WriteFile(s.dataDir, userID, req.Path, req.Content); err != nil { writeJSON(w, 500, map[string]string{"error": "create failed"}) return } writeJSON(w, 201, map[string]string{"status": "created", "path": req.Path}) } func (s *Server) handleCreateFolder(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` } if err := decodeBody(r, &req); err != nil || req.Path == "" { writeJSON(w, 400, map[string]string{"error": "path required"}) return } userID := getUserID(r) if err := files.CreateFolder(s.dataDir, userID, req.Path); err != nil { writeJSON(w, 500, map[string]string{"error": "create folder failed"}) return } writeJSON(w, 201, map[string]string{"status": "created", "path": req.Path}) } func (s *Server) handleDeleteFile(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` } if err := decodeBody(r, &req); err != nil || req.Path == "" { writeJSON(w, 400, map[string]string{"error": "path required"}) return } userID := getUserID(r) if err := files.DeleteFile(s.dataDir, userID, req.Path); err != nil { writeJSON(w, 404, map[string]string{"error": "file not found"}) return } writeJSON(w, 200, map[string]string{"status": "deleted"}) } func (s *Server) handleMoveFile(w http.ResponseWriter, r *http.Request) { var req struct { From string `json:"from"` To string `json:"to"` } if err := decodeBody(r, &req); err != nil || req.From == "" || req.To == "" { writeJSON(w, 400, map[string]string{"error": "from and to required"}) return } userID := getUserID(r) if err := files.MoveFile(s.dataDir, userID, req.From, req.To); err != nil { writeJSON(w, 500, map[string]string{"error": "move failed"}) return } writeJSON(w, 200, map[string]string{"status": "moved"}) } func (s *Server) handleSharedFiles(w http.ResponseWriter, r *http.Request) { // TODO: query permissions table for files shared with this user // For now return empty list writeJSON(w, 200, []files.FileInfo{}) } func (s *Server) handleListTrash(w http.ResponseWriter, r *http.Request) { userID := getUserID(r) items, err := files.ListTrash(s.dataDir, userID) if err != nil { writeJSON(w, 500, map[string]string{"error": "failed to list trash"}) return } if items == nil { items = []files.FileInfo{} } writeJSON(w, 200, items) } func (s *Server) handleRestoreTrash(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` } if err := decodeBody(r, &req); err != nil || req.Name == "" { writeJSON(w, 400, map[string]string{"error": "name required"}) return } userID := getUserID(r) if err := files.RestoreFromTrash(s.dataDir, userID, req.Name); err != nil { writeJSON(w, 500, map[string]string{"error": "restore failed"}) return } writeJSON(w, 200, map[string]string{"status": "restored"}) } func (s *Server) handleEmptyTrash(w http.ResponseWriter, r *http.Request) { userID := getUserID(r) if err := files.EmptyTrash(s.dataDir, userID); err != nil { writeJSON(w, 500, map[string]string{"error": "empty trash failed"}) return } writeJSON(w, 200, map[string]string{"status": "emptied"}) }