2025-12-16 16:37:53 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-01-21 03:30:03 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/background"
|
2025-12-16 16:37:53 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
2026-01-02 08:58:57 -07:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
2025-12-16 16:37:53 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/userfile"
|
2026-01-06 22:23:59 +00:00
|
|
|
"github.com/aarondl/opt/omit"
|
|
|
|
|
"github.com/aarondl/opt/omitnull"
|
2025-12-16 16:37:53 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/go-chi/render"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func apiAudioPost(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
id := chi.URLParam(r, "uuid")
|
|
|
|
|
noteUUID, err := uuid.Parse(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var payload NoteAudioPayload
|
|
|
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
2025-12-18 03:35:18 -07:00
|
|
|
debugSaveRequest(body, err, "Audio note POST JSON decode error")
|
2025-12-16 16:37:53 +00:00
|
|
|
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-06 22:23:59 +00:00
|
|
|
setter := models.NoteAudioSetter{
|
|
|
|
|
Created: omit.From(payload.Created),
|
|
|
|
|
CreatorID: omit.From(u.ID),
|
|
|
|
|
Deleted: omitnull.FromPtr(payload.Deleted),
|
|
|
|
|
DeletorID: omitnull.FromPtr(payload.DeletorID),
|
|
|
|
|
Duration: omit.From(payload.Duration),
|
|
|
|
|
Transcription: omitnull.FromPtr(payload.Transcription),
|
|
|
|
|
TranscriptionUserEdited: omit.From(payload.TranscriptionUserEdited),
|
|
|
|
|
Version: omit.From(payload.Version),
|
|
|
|
|
UUID: omit.From(noteUUID),
|
|
|
|
|
}
|
|
|
|
|
if err := db.NoteAudioCreate(context.Background(), u.R.Organization, u.ID, setter); err != nil {
|
2025-12-16 16:37:53 +00:00
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
u_str := chi.URLParam(r, "uuid")
|
|
|
|
|
audioUUID, err := uuid.Parse(u_str)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
err = userfile.AudioFileContentWrite(audioUUID, r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to write content file: %v", err)
|
|
|
|
|
http.Error(w, "failed to write content file", http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 03:30:03 +00:00
|
|
|
background.AudioTranscode(audioUUID)
|
2025-12-16 16:37:53 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 22:56:32 +00:00
|
|
|
func apiGetDistrict(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
var latStr, lngStr string
|
|
|
|
|
err := r.ParseForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(fmt.Errorf("Failed to parse GET form: %w", err)))
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
latStr = r.FormValue("lat")
|
|
|
|
|
lngStr = r.FormValue("lng")
|
|
|
|
|
}
|
|
|
|
|
lat, err := strconv.ParseFloat(latStr, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(fmt.Errorf("Failed to parse lat as float: %w", err)))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
lng, err := strconv.ParseFloat(lngStr, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(fmt.Errorf("Failed to parse lng as float: %w", err)))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
district, err := platform.DistrictForLocation(r.Context(), lng, lat)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(fmt.Errorf("Failed to get district: %w", err)))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-15 23:19:31 +00:00
|
|
|
if district == nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-15 22:56:32 +00:00
|
|
|
d := ResponseDistrict{
|
2026-01-15 23:19:31 +00:00
|
|
|
Agency: district.Agency.GetOr(""),
|
|
|
|
|
Manager: district.GeneralMG.GetOr(""),
|
|
|
|
|
Phone: district.Phone1.GetOr(""),
|
|
|
|
|
Website: district.Website.GetOr(""),
|
2026-01-15 22:56:32 +00:00
|
|
|
}
|
|
|
|
|
if err := render.Render(w, r, d); err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 08:58:57 -07:00
|
|
|
func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
var sinceStr string
|
|
|
|
|
err := r.ParseForm()
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
2026-01-02 08:58:57 -07:00
|
|
|
render.Render(w, r, errRender(fmt.Errorf("Failed to parse GET form: %w", err)))
|
2025-12-16 16:37:53 +00:00
|
|
|
return
|
2026-01-02 08:58:57 -07:00
|
|
|
} else {
|
|
|
|
|
sinceStr = r.FormValue("since")
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|
2026-01-02 08:58:57 -07:00
|
|
|
|
|
|
|
|
var since *time.Time
|
|
|
|
|
if sinceStr == "" {
|
|
|
|
|
since = nil
|
|
|
|
|
} else {
|
|
|
|
|
since, err = parseTime(sinceStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(fmt.Errorf("Failed to parse 'since' value: %w", err)))
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|
2026-01-02 08:58:57 -07:00
|
|
|
|
|
|
|
|
csync, err := platform.ContentClientIos(r.Context(), u, since)
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 22:23:59 +00:00
|
|
|
var since_used time.Time
|
|
|
|
|
if since == nil {
|
|
|
|
|
since_used = time.Unix(0, 0)
|
|
|
|
|
} else {
|
|
|
|
|
since_used = *since
|
|
|
|
|
}
|
2026-01-02 08:58:57 -07:00
|
|
|
response := ResponseClientIos{
|
2026-01-06 03:06:38 +00:00
|
|
|
Fieldseeker: toResponseFieldseeker(csync.Fieldseeker),
|
2026-01-06 22:23:59 +00:00
|
|
|
Since: since_used,
|
2026-01-02 08:58:57 -07:00
|
|
|
}
|
2025-12-16 16:37:53 +00:00
|
|
|
if err := render.Render(w, r, response); err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiImagePost(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
id := chi.URLParam(r, "uuid")
|
|
|
|
|
noteUUID, err := uuid.Parse(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var payload NoteImagePayload
|
|
|
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
2025-12-18 03:35:18 -07:00
|
|
|
debugSaveRequest(body, err, "Image note POST JSON decode error")
|
2025-12-16 16:37:53 +00:00
|
|
|
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-06 22:23:59 +00:00
|
|
|
setter := models.NoteImageSetter{
|
|
|
|
|
Created: omit.From(payload.Created),
|
|
|
|
|
CreatorID: omit.From(u.ID),
|
|
|
|
|
Deleted: omitnull.FromPtr(payload.Deleted),
|
|
|
|
|
DeletorID: omitnull.FromPtr(payload.DeletorID),
|
|
|
|
|
Version: omit.From(payload.Version),
|
|
|
|
|
UUID: omit.From(noteUUID),
|
|
|
|
|
}
|
|
|
|
|
err = db.NoteImageCreate(context.Background(), u.R.Organization, u.ID, setter)
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiImageContentPost(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
u_str := chi.URLParam(r, "uuid")
|
|
|
|
|
imageUUID, err := uuid.Parse(u_str)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("Failed to parse image UUID")
|
|
|
|
|
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
|
|
|
|
|
}
|
2025-12-18 03:36:52 -07:00
|
|
|
err = userfile.ImageFileContentWrite(imageUUID, r.Body)
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
2025-12-18 03:36:52 -07:00
|
|
|
render.Render(w, r, errRender(err))
|
2025-12-16 16:37:53 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
log.Printf("Saved image file %s\n", imageUUID)
|
2025-12-18 03:36:52 -07:00
|
|
|
fmt.Fprintf(w, "PNG uploaded successfully")
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
bounds, err := parseBounds(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := db.NewGeoQuery()
|
|
|
|
|
query.Bounds = *bounds
|
|
|
|
|
query.Limit = 100
|
2026-01-05 02:06:34 +00:00
|
|
|
sources, err := platform.MosquitoSourceQuery()
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data := []render.Renderer{}
|
2026-01-06 22:23:59 +00:00
|
|
|
for _, s := range sources {
|
2025-12-16 16:37:53 +00:00
|
|
|
data = append(data, NewResponseMosquitoSource(s))
|
|
|
|
|
}
|
|
|
|
|
if err := render.RenderList(w, r, data); err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiTrapData(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
bounds, err := parseBounds(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := db.NewGeoQuery()
|
|
|
|
|
query.Bounds = *bounds
|
|
|
|
|
query.Limit = 100
|
2026-01-05 02:06:34 +00:00
|
|
|
trap_data, err := platform.TrapDataQuery()
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data := []render.Renderer{}
|
2026-01-06 22:23:59 +00:00
|
|
|
for _, td := range trap_data {
|
2025-12-16 16:37:53 +00:00
|
|
|
data = append(data, NewResponseTrapDatum(td))
|
|
|
|
|
}
|
|
|
|
|
if err := render.RenderList(w, r, data); err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiServiceRequest(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
bounds, err := parseBounds(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
query := db.NewGeoQuery()
|
|
|
|
|
query.Bounds = *bounds
|
|
|
|
|
query.Limit = 100
|
2026-01-05 02:06:34 +00:00
|
|
|
requests, err := platform.ServiceRequestQuery()
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data := []render.Renderer{}
|
2026-01-06 22:23:59 +00:00
|
|
|
for _, sr := range requests {
|
2025-12-16 16:37:53 +00:00
|
|
|
data = append(data, NewResponseServiceRequest(sr))
|
|
|
|
|
}
|
|
|
|
|
if err := render.RenderList(w, r, data); err != nil {
|
|
|
|
|
render.Render(w, r, errRender(err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseBounds(r *http.Request) (*db.GeoBounds, error) {
|
|
|
|
|
err := r.ParseForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
east := r.FormValue("east")
|
|
|
|
|
north := r.FormValue("north")
|
|
|
|
|
south := r.FormValue("south")
|
|
|
|
|
west := r.FormValue("west")
|
|
|
|
|
|
|
|
|
|
bounds := db.GeoBounds{}
|
|
|
|
|
|
|
|
|
|
var temp float64
|
|
|
|
|
temp, err = strconv.ParseFloat(east, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
bounds.East = temp
|
|
|
|
|
temp, err = strconv.ParseFloat(north, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
bounds.North = temp
|
|
|
|
|
temp, err = strconv.ParseFloat(south, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
bounds.South = temp
|
|
|
|
|
temp, err = strconv.ParseFloat(west, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
bounds.West = temp
|
|
|
|
|
return &bounds, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func errRender(err error) render.Renderer {
|
|
|
|
|
log.Error().Err(err).Msg("Rendering error")
|
|
|
|
|
return &ResponseErr{
|
|
|
|
|
Error: err,
|
|
|
|
|
HTTPStatusCode: 500,
|
|
|
|
|
StatusText: "Error rendering response",
|
|
|
|
|
ErrorText: err.Error(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func webhookFieldseeker(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Create or open the log file
|
|
|
|
|
file, err := os.OpenFile("webhook/request.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Error opening log file: %v", err)
|
|
|
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
// Write timestamp
|
|
|
|
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
|
fmt.Fprintf(file, "\n=== Request logged at %s ===\n", timestamp)
|
|
|
|
|
|
|
|
|
|
// Write request line
|
|
|
|
|
fmt.Fprintf(file, "%s %s %s\n", r.Method, r.RequestURI, r.Proto)
|
|
|
|
|
|
|
|
|
|
// Write all headers
|
|
|
|
|
fmt.Fprintf(file, "\nHeaders:\n")
|
|
|
|
|
for name, values := range r.Header {
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
fmt.Fprintf(file, "%s: %s\n", name, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write body
|
|
|
|
|
fmt.Fprintf(file, "\nBody:\n")
|
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Error reading request body: %v", err)
|
|
|
|
|
fmt.Fprintf(file, "Error reading body: %v\n", err)
|
|
|
|
|
} else {
|
|
|
|
|
file.Write(body)
|
|
|
|
|
if len(body) == 0 {
|
|
|
|
|
fmt.Fprintf(file, "(empty body)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Fprintf(file, "\n=== End of request ===\n\n")
|
|
|
|
|
|
|
|
|
|
// Extract the crc_token value for the signature portion
|
|
|
|
|
|
|
|
|
|
// Respond with 204 No Content
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 08:58:57 -07:00
|
|
|
func parseTime(x string) (*time.Time, error) {
|
2025-12-16 16:37:53 +00:00
|
|
|
created_epoch, err := strconv.ParseInt(x, 10, 64)
|
|
|
|
|
if err != nil {
|
2026-01-02 08:58:57 -07:00
|
|
|
return &time.Time{}, fmt.Errorf("Failed to parse time '%s': %w", x, err)
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|
|
|
|
|
created := time.UnixMilli(created_epoch)
|
2026-01-02 08:58:57 -07:00
|
|
|
return &created, nil
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|