355 lines
10 KiB
Go
355 lines
10 KiB
Go
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"})
|
|
}
|