Files
palace/server/main.go
2025-07-19 00:55:13 +02:00

266 lines
6.2 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3"
)
type Note struct {
ID string `json:"id"`
X float64 `json:"x"`
Y float64 `json:"y"`
Content string `json:"content"`
}
type Space struct {
ID string `json:"id"`
Notes []Note `json:"notes"`
}
var db *sql.DB
func main() {
dbPath := os.Getenv("DB_PATH")
if dbPath == "" {
dbPath = "./palace.db"
}
var err error
db, err = sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := initDB(); err != nil {
log.Fatal(err)
}
r := mux.NewRouter()
r.HandleFunc("/api/spaces", getSpaces).Methods("GET")
r.HandleFunc("/api/spaces/{id}", getSpace).Methods("GET")
r.HandleFunc("/api/spaces/{id}", saveSpace).Methods("PUT")
r.HandleFunc("/api/spaces/{id}", deleteSpace).Methods("DELETE")
r.HandleFunc("/api/spaces/{id}/notes", addNote).Methods("POST")
r.HandleFunc("/api/spaces/{spaceId}/notes/{noteId}", updateNote).Methods("PATCH")
r.HandleFunc("/api/spaces/{spaceId}/notes/{noteId}", deleteNote).Methods("DELETE")
r.Use(corsMiddleware)
port := os.Getenv("PORT")
if port == "" {
port = "3001"
}
log.Printf("Server starting on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, r))
}
func initDB() error {
query := `
CREATE TABLE IF NOT EXISTS spaces (
id TEXT PRIMARY KEY,
notes TEXT DEFAULT '[]'
);`
_, err := db.Exec(query)
return err
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func getSpaces(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id FROM spaces")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var spaces []Space
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
continue
}
spaces = append(spaces, Space{ID: id, Notes: []Note{}})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(spaces)
}
func getSpace(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var notesJSON string
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", id).Scan(&notesJSON)
if err == sql.ErrNoRows {
http.Error(w, "Space not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var notes []Note
if err := json.Unmarshal([]byte(notesJSON), &notes); err != nil {
notes = []Note{}
}
space := Space{ID: id, Notes: notes}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(space)
}
func saveSpace(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var space Space
if err := json.NewDecoder(r.Body).Decode(&space); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
notesJSON, _ := json.Marshal(space.Notes)
_, err := db.Exec("INSERT OR REPLACE INTO spaces (id, notes) VALUES (?, ?)", id, string(notesJSON))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func deleteSpace(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
_, err := db.Exec("DELETE FROM spaces WHERE id = ?", id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func addNote(w http.ResponseWriter, r *http.Request) {
spaceId := mux.Vars(r)["id"]
var note Note
if err := json.NewDecoder(r.Body).Decode(&note); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var notesJSON string
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(&notesJSON)
if err != nil {
http.Error(w, "Space not found", http.StatusNotFound)
return
}
var notes []Note
json.Unmarshal([]byte(notesJSON), &notes)
notes = append(notes, note)
newNotesJSON, _ := json.Marshal(notes)
_, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func updateNote(w http.ResponseWriter, r *http.Request) {
spaceId := mux.Vars(r)["spaceId"]
noteId := mux.Vars(r)["noteId"]
var updates Note
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var notesJSON string
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(&notesJSON)
if err != nil {
http.Error(w, "Space not found", http.StatusNotFound)
return
}
var notes []Note
json.Unmarshal([]byte(notesJSON), &notes)
for i, note := range notes {
if note.ID == noteId {
if updates.X != 0 { notes[i].X = updates.X }
if updates.Y != 0 { notes[i].Y = updates.Y }
if updates.Content != "" { notes[i].Content = updates.Content }
break
}
}
newNotesJSON, _ := json.Marshal(notes)
_, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func deleteNote(w http.ResponseWriter, r *http.Request) {
spaceId := mux.Vars(r)["spaceId"]
noteId := mux.Vars(r)["noteId"]
var notesJSON string
err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(&notesJSON)
if err != nil {
http.Error(w, "Space not found", http.StatusNotFound)
return
}
var notes []Note
json.Unmarshal([]byte(notesJSON), &notes)
for i, note := range notes {
if note.ID == noteId {
notes = append(notes[:i], notes[i+1:]...)
break
}
}
newNotesJSON, _ := json.Marshal(notes)
_, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}