263 lines
6.6 KiB
Go
263 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"woke/db"
|
|
)
|
|
|
|
// Notifier is called after mutating operations to notify the TUI.
|
|
type Notifier func()
|
|
|
|
// Server provides a REST API for alarm CRUD.
|
|
type Server struct {
|
|
store *db.Store
|
|
mux *http.ServeMux
|
|
notify Notifier
|
|
password string
|
|
}
|
|
|
|
func New(store *db.Store, password string, notify Notifier) *Server {
|
|
s := &Server{store: store, notify: notify, password: password}
|
|
s.mux = http.NewServeMux()
|
|
s.mux.HandleFunc("GET /api/alarms", s.listAlarms)
|
|
s.mux.HandleFunc("GET /api/alarms/{id}", s.getAlarm)
|
|
s.mux.HandleFunc("POST /api/alarms", s.createAlarm)
|
|
s.mux.HandleFunc("PUT /api/alarms/{id}", s.updateAlarm)
|
|
s.mux.HandleFunc("DELETE /api/alarms/{id}", s.deleteAlarm)
|
|
s.mux.HandleFunc("PATCH /api/alarms/{id}/toggle", s.toggleAlarm)
|
|
return s
|
|
}
|
|
|
|
func (s *Server) Handler() http.Handler {
|
|
if s.password == "" {
|
|
return s.mux
|
|
}
|
|
// Wrap with auth middleware
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(auth, "Bearer ") || auth[7:] != s.password {
|
|
writeError(w, http.StatusUnauthorized, "invalid or missing authorization")
|
|
return
|
|
}
|
|
s.mux.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
type alarmRequest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Time string `json:"time"`
|
|
Trigger string `json:"trigger"`
|
|
SoundPath string `json:"sound_path"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
|
|
type alarmResponse struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Time string `json:"time"`
|
|
Trigger string `json:"trigger"`
|
|
SoundPath string `json:"sound_path"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
LastTriggered *string `json:"last_triggered"`
|
|
SnoozeCount int `json:"snooze_count"`
|
|
}
|
|
|
|
func toResponse(a db.Alarm) alarmResponse {
|
|
r := alarmResponse{
|
|
ID: a.ID,
|
|
Name: a.Name,
|
|
Description: a.Description,
|
|
Time: a.Time,
|
|
Trigger: a.Trigger,
|
|
SoundPath: a.SoundPath,
|
|
Enabled: a.Enabled,
|
|
CreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
|
UpdatedAt: a.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
|
SnoozeCount: a.SnoozeCount,
|
|
}
|
|
if a.LastTriggered != nil {
|
|
t := a.LastTriggered.Format("2006-01-02T15:04:05Z")
|
|
r.LastTriggered = &t
|
|
}
|
|
return r
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|
|
|
|
func (s *Server) listAlarms(w http.ResponseWriter, r *http.Request) {
|
|
alarms, err := s.store.ListAlarms()
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list alarms")
|
|
return
|
|
}
|
|
resp := make([]alarmResponse, len(alarms))
|
|
for i, a := range alarms {
|
|
resp[i] = toResponse(a)
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) getAlarm(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
alarm, err := s.store.GetAlarm(id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "alarm not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, toResponse(alarm))
|
|
}
|
|
|
|
func (s *Server) createAlarm(w http.ResponseWriter, r *http.Request) {
|
|
var req alarmRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
|
return
|
|
}
|
|
if req.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
if req.Trigger == "" {
|
|
writeError(w, http.StatusBadRequest, "trigger is required")
|
|
return
|
|
}
|
|
|
|
alarm := db.Alarm{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Time: req.Time,
|
|
Trigger: req.Trigger,
|
|
SoundPath: req.SoundPath,
|
|
Enabled: true,
|
|
}
|
|
if alarm.SoundPath == "" {
|
|
alarm.SoundPath = "default"
|
|
}
|
|
if req.Enabled != nil {
|
|
alarm.Enabled = *req.Enabled
|
|
}
|
|
|
|
id, err := s.store.CreateAlarm(alarm)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create alarm")
|
|
return
|
|
}
|
|
|
|
created, err := s.store.GetAlarm(id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read created alarm")
|
|
return
|
|
}
|
|
s.notify()
|
|
writeJSON(w, http.StatusCreated, toResponse(created))
|
|
}
|
|
|
|
func (s *Server) updateAlarm(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
existing, err := s.store.GetAlarm(id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "alarm not found")
|
|
return
|
|
}
|
|
|
|
var req alarmRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
|
return
|
|
}
|
|
|
|
if req.Name != "" {
|
|
existing.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
existing.Description = req.Description
|
|
}
|
|
if req.Time != "" {
|
|
existing.Time = req.Time
|
|
}
|
|
if req.Trigger != "" {
|
|
existing.Trigger = req.Trigger
|
|
}
|
|
if req.SoundPath != "" {
|
|
existing.SoundPath = req.SoundPath
|
|
}
|
|
if req.Enabled != nil {
|
|
existing.Enabled = *req.Enabled
|
|
}
|
|
|
|
if err := s.store.UpdateAlarm(existing); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update alarm")
|
|
return
|
|
}
|
|
|
|
updated, _ := s.store.GetAlarm(id)
|
|
s.notify()
|
|
writeJSON(w, http.StatusOK, toResponse(updated))
|
|
}
|
|
|
|
func (s *Server) deleteAlarm(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
if _, err := s.store.GetAlarm(id); err != nil {
|
|
writeError(w, http.StatusNotFound, "alarm not found")
|
|
return
|
|
}
|
|
|
|
if err := s.store.DeleteAlarm(id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to delete alarm")
|
|
return
|
|
}
|
|
s.notify()
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) toggleAlarm(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
if _, err := s.store.GetAlarm(id); err != nil {
|
|
writeError(w, http.StatusNotFound, "alarm not found")
|
|
return
|
|
}
|
|
|
|
if err := s.store.ToggleAlarm(id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to toggle alarm")
|
|
return
|
|
}
|
|
|
|
toggled, _ := s.store.GetAlarm(id)
|
|
s.notify()
|
|
writeJSON(w, http.StatusOK, toResponse(toggled))
|
|
}
|