Skip to content

Commit

Permalink
Merge pull request #167 from 0x2142/initial-rest-api
Browse files Browse the repository at this point in the history
Initial REST API
  • Loading branch information
0x2142 authored Dec 5, 2024
2 parents 76fc3c3 + a362c2a commit 6662753
Show file tree
Hide file tree
Showing 49 changed files with 1,367 additions and 266 deletions.
38 changes: 38 additions & 0 deletions api/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package api

import (
"fmt"
"net"
"net/http"

apiv1 "github.com/0x2142/frigate-notify/api/v1"
"github.com/0x2142/frigate-notify/config"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humago"
"github.com/rs/zerolog/log"
)

func RunAPIServer() error {

router := http.NewServeMux()

// Configure API
apiConfig := huma.DefaultConfig("Frigate-Notify", config.Internal.AppVersion)
apiConfig.Info.License = &huma.License{Name: "MIT",
URL: "https://github.com/0x2142/frigate-notify/blob/main/LICENSE"}
apiConfig.Info.Contact = &huma.Contact{Name: "Matt Schmitz",
URL: "https://github.com/0x2142/frigate-notify",
}
api := humago.New(router, apiConfig)

apiv1.Registerv1Routes(api)

log.Debug().Msg("Starting API server...")
listenAddr := fmt.Sprintf("0.0.0.0:%v", config.ConfigData.App.API.Port)
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
go http.Serve(listener, router)
return nil
}
113 changes: 113 additions & 0 deletions api/v1/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package apiv1

import (
"context"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/events"
"github.com/danielgtaylor/huma/v2"
"github.com/rs/zerolog/log"
)

type ConfigOutput struct {
Body struct {
Config config.Config `json:"config"`
}
}

type PutConfigInput struct {
Body struct {
Config config.Config `json:"config"`
SkipSave bool `json:"skipsave,omitempty" doc:"Skip writing new config to file" default:"false"`
SkipBackup bool `json:"skipbackup,omitempty" doc:"Skip creating config file backup" default:"false"`
SkipValidate bool `json:"skipvalidate,omitempty" doc:"Skip config validation checking"`
SkipReload bool `json:"skipreload,omitempty" doc:"Skip config reload after updating settings" hidden:"true"`
}
}

type PutConfigOutput struct {
Body struct {
Status string `json:"status"`
Errors []string `json:"errors,omitempty"`
}
}

// GetConfig returns the current running configuration
func GetConfig(ctx context.Context, input *struct{}) (*ConfigOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/config").
Str("method", "GET").
Msg("Received API request")

resp := &ConfigOutput{}
resp.Body.Config = config.ConfigData

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}

// PutConfig replaces the current running configuration
func PutConfig(ctx context.Context, input *PutConfigInput) (*PutConfigOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/config").
Str("method", "PUT").
Msg("Received API request")

resp := &PutConfigOutput{}

newConfig := input.Body.Config

var validationErrors []string
if !input.Body.SkipValidate {
log.Trace().Msg("Skipping config validation checks")
validationErrors = newConfig.Validate()
}

if len(validationErrors) == 0 {
resp.Body.Status = "ok"
if !input.Body.SkipReload {
go reloadCfg(newConfig, input.Body.SkipSave, input.Body.SkipBackup)
}

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")
return resp, nil
} else {
resp.Body.Status = "validation error"
resp.Body.Errors = validationErrors

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, huma.Error422UnprocessableEntity("config validation failed")
}
}

func reloadCfg(newconfig config.Config, skipSave bool, skipBackup bool) {
log.Info().Msg("Reloading app config...")
log.Trace().
Bool("skipSave", skipSave).
Bool("skipBackup", skipBackup).
Msg("Config reload via API")
if config.ConfigData.Frigate.MQTT.Enabled {
events.DisconnectMQTT()
}

config.ConfigData = newconfig
if !skipSave {
config.Save(skipBackup)
}

if config.ConfigData.Frigate.MQTT.Enabled {
events.SubscribeMQTT()
}
log.Info().Msg("Config reload completed")
}
50 changes: 50 additions & 0 deletions api/v1/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package apiv1

import (
"bytes"
"net/http"
"testing"

"github.com/danielgtaylor/huma/v2/humatest"
)

func TestGetConfig(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

resp := api.Get("/api/v1/config")

if resp.Code != http.StatusOK {
t.Error("Expected HTTP 200, got ", resp.Code)
}
}

func TestPutConfig(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

newconfig := `{
"config":{
"frigate":{
"server":"http://192.0.2.10:5000",
"mqtt":{
"enabled": true
}
},
"alerts":{
}
},
"skipvalidate": true,
"skipbackup": true,
"skipsave": true,
"skipreload": true
}`

resp := api.Put("/api/v1/config", bytes.NewReader([]byte(newconfig)))

if resp.Code != http.StatusAccepted {
t.Error("Expected HTTP 202, got ", resp.Code)
}
}
32 changes: 32 additions & 0 deletions api/v1/healthz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package apiv1

import (
"context"

"github.com/rs/zerolog/log"
)

type HealthzOutput struct {
Body struct {
Status string `json:"status"`
}
}

// GetHealthz returns current app liveness state
func GetHealthz(ctx context.Context, input *struct{}) (*HealthzOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/healthz").
Str("method", "GET").
Msg("Received API request")

resp := &HealthzOutput{}

resp.Body.Status = "ok"

log.Trace().
Str("uri", V1_PREFIX+"/healthz").
Interface("response_json", resp.Body).
Msg("Sent API response")
return resp, nil

}
28 changes: 28 additions & 0 deletions api/v1/healthz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package apiv1

import (
"encoding/json"
"net/http"
"testing"

"github.com/danielgtaylor/huma/v2/humatest"
)

func TestGetHealthz(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

resp := api.Get("/api/v1/healthz")

if resp.Code != http.StatusOK {
t.Error("Expected HTTP 200, got ", resp.Code)
}

var healthzResponse map[string]interface{}
json.Unmarshal([]byte(resp.Body.Bytes()), &healthzResponse)

if healthzResponse["status"] != "ok" {
t.Error("Response body did not match expected result")
}
}
62 changes: 62 additions & 0 deletions api/v1/notif_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package apiv1

import (
"context"

"github.com/0x2142/frigate-notify/config"
"github.com/rs/zerolog/log"
)

type NotifStateInput struct {
Body struct {
Enabled bool `json:"enabled" enum:"true,false" doc:"Set state of notifications" required:"true"`
}
}

type NotifStateOutput struct {
Body struct {
Enabled bool `json:"enabled" enum:"true,false" doc:"Frigate-Notify enabled for notifications" default:"true"`
}
}

// GetNotifState returns whether app is enabled for sending notifications or not
func GetNotifState(ctx context.Context, input *struct{}) (*NotifStateOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
Str("method", "GET").
Msg("Received API request")

resp := &NotifStateOutput{}
resp.Body.Enabled = config.Internal.Status.Notifications.Enabled

log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}

// PostNotifState updates state to enable or disable app notifications
func PostNotifState(ctx context.Context, input *NotifStateInput) (*NotifStateOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
Str("method", "POST").
Msg("Received API request")

config.Internal.Status.Notifications.Enabled = input.Body.Enabled

log.Debug().
Bool("state", input.Body.Enabled).
Msg("App state changed via API")

resp := &NotifStateOutput{}
resp.Body.Enabled = config.Internal.Status.Notifications.Enabled

log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
//Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}
33 changes: 33 additions & 0 deletions api/v1/notif_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package apiv1

import (
"bytes"
"net/http"
"testing"

"github.com/danielgtaylor/huma/v2/humatest"
)

func TestGetNotifState(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

resp := api.Get("/api/v1/notif_state")

if resp.Code != http.StatusOK {
t.Error("Expected HTTP 200, got ", resp.Code)
}
}

func TestPostNotifState(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

resp := api.Post("/api/v1/notif_state", bytes.NewReader([]byte(`{"enabled": false}`)))

if resp.Code != http.StatusAccepted {
t.Error("Expected HTTP 202, got ", resp.Code)
}
}
Loading

0 comments on commit 6662753

Please sign in to comment.