Massive rework of platform layer user/organization

The goal of this rework is to make it so I can pass around platform.User
instead of a pair of models.Organization and models.User. This is useful
for reason I kind of forget now, but it started with working on
notifications and ballooned massively from there into refactoring a
number of things that were bugging me.

This also includes a tiny amount of work on server-side events (SSE).

 * background stuff lives inside the platform now, which I need for
   having it push updates through SSE
 * userfile now lives in the platform, under file, so other platform
   functions can safely use it
 * oauth is broken into pieces and inside platform because other stuff
   was calling it already, but badly.
 * notifications go into the platform as well
This commit is contained in:
Eli Ribble 2026-03-12 23:49:16 +00:00
parent 32dcc50c94
commit 44c4f17f32
No known key found for this signature in database
85 changed files with 1492 additions and 1384 deletions

View file

@ -1,7 +1,6 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io"
@ -11,11 +10,11 @@ import (
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/background"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/Gleipnir-Technology/nidus-sync/platform/background"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/go-chi/chi/v5"
@ -24,7 +23,7 @@ import (
"github.com/rs/zerolog/log"
)
func apiAudioPost(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
func apiAudioPost(w http.ResponseWriter, r *http.Request, u platform.User) {
id := chi.URLParam(r, "uuid")
noteUUID, err := uuid.Parse(id)
if err != nil {
@ -43,9 +42,10 @@ func apiAudioPost(w http.ResponseWriter, r *http.Request, org *models.Organizati
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
ctx := r.Context()
setter := models.NoteAudioSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
CreatorID: omit.From(int32(u.ID)),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Duration: omit.From(payload.Duration),
@ -54,21 +54,21 @@ func apiAudioPost(w http.ResponseWriter, r *http.Request, org *models.Organizati
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
if err := db.NoteAudioCreate(context.Background(), u.R.Organization, u.ID, setter); err != nil {
if err := platform.NoteAudioCreate(ctx, u, setter); err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, u platform.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.FileContentWrite(r.Body, userfile.CollectionAudioRaw, audioUUID)
err = file.FileContentWrite(r.Body, file.CollectionAudioRaw, audioUUID)
if err != nil {
log.Printf("Failed to write content file: %v", err)
http.Error(w, "failed to write content file", http.StatusInternalServerError)
@ -78,7 +78,7 @@ func apiAudioContentPost(w http.ResponseWriter, r *http.Request, org *models.Org
w.WriteHeader(http.StatusOK)
}
func handleClientIos(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
func handleClientIos(w http.ResponseWriter, r *http.Request, u platform.User) {
var sinceStr string
err := r.ParseForm()
if err != nil {
@ -121,69 +121,7 @@ func handleClientIos(w http.ResponseWriter, r *http.Request, org *models.Organiz
}
}
func apiImagePost(w http.ResponseWriter, r *http.Request, org *models.Organization, 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 {
//debugSaveRequest(body, err, "Image note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
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)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiImageContentGet(w http.ResponseWriter, r *http.Request, org *models.Organization, 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)
}
userfile.PublicImageFileToResponse(w, imageUUID)
w.WriteHeader(http.StatusOK)
}
func apiImageContentPost(w http.ResponseWriter, r *http.Request, org *models.Organization, 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)
}
err = userfile.ImageFileContentWrite(imageUUID, r.Body)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusOK)
log.Printf("Saved image file %s\n", imageUUID)
fmt.Fprintf(w, "PNG uploaded successfully")
}
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
@ -208,7 +146,7 @@ func apiMosquitoSource(w http.ResponseWriter, r *http.Request, org *models.Organ
}
}
func apiTrapData(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
func apiTrapData(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
@ -233,7 +171,7 @@ func apiTrapData(w http.ResponseWriter, r *http.Request, org *models.Organizatio
}
}
func apiServiceRequest(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
func apiServiceRequest(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))

View file

@ -7,8 +7,8 @@ import (
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/publicreport"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/google/uuid"
@ -35,12 +35,12 @@ type contentListCommunication struct {
Communications []communication `json:"communications"`
}
func listCommunication(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListCommunication, *nhttp.ErrorWithStatus) {
nreports, err := publicreport.NuisanceReportForOrganization(ctx, org.ID)
func listCommunication(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListCommunication, *nhttp.ErrorWithStatus) {
nreports, err := publicreport.NuisanceReportForOrganization(ctx, user.Organization.ID())
if err != nil {
return nil, nhttp.NewError("nuisance report query: %w", err)
}
wreports, err := publicreport.WaterReportForOrganization(ctx, org.ID)
wreports, err := publicreport.WaterReportForOrganization(ctx, user.Organization.ID())
if err != nil {
return nil, nhttp.NewError("water report query: %w", err)
}

View file

@ -12,9 +12,7 @@ import (
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
//"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/imagetile"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/go-chi/chi/v5"
"github.com/paulmach/orb/geojson"
"github.com/rs/zerolog/log"
@ -61,10 +59,7 @@ func getComplianceRequestImagePool(w http.ResponseWriter, r *http.Request) {
psql.Quote("organization.id"),
),
sm.InnerJoin("site").On(
psql.And(
psql.Quote("lead.site_id").EQ(psql.Quote("site.id")),
psql.Quote("lead.site_version").EQ(psql.Quote("site.version")),
),
psql.Quote("lead.site_id").EQ(psql.Quote("site.id")),
),
sm.InnerJoin("parcel").OnEQ(
psql.Quote("site.parcel_id"),
@ -72,9 +67,13 @@ func getComplianceRequestImagePool(w http.ResponseWriter, r *http.Request) {
),
sm.Where(psql.Quote("compliance_report_request").EQ(psql.Arg(code))),
), scan.StructMapper[_Row]())
org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, row.OrganizationID)
org, err := platform.OrganizationByID(ctx, int(row.OrganizationID))
if err != nil {
http.Error(w, "no org", http.StatusInternalServerError)
http.Error(w, "org err", http.StatusInternalServerError)
return
}
if org == nil {
http.Error(w, "no org", http.StatusBadRequest)
return
}
var polygon geojson.Polygon
@ -86,15 +85,15 @@ func getComplianceRequestImagePool(w http.ResponseWriter, r *http.Request) {
}
ring := polygon[0]
p := ring[0]
err = writeImage(ctx, w, org, 19, p[1], p[0])
err = writeImage(ctx, w, *org, 19, p[1], p[0])
if err != nil {
log.Error().Err(err).Msg("write image")
http.Error(w, "failed to write image", http.StatusInternalServerError)
return
}
}
func writeImage(ctx context.Context, w http.ResponseWriter, org *models.Organization, level uint, lat, lng float64) error {
img, err := imagetile.ImageAtPoint(ctx, org, level, lat, lng)
func writeImage(ctx context.Context, w http.ResponseWriter, org platform.Organization, level uint, lat, lng float64) error {
img, err := platform.ImageAtPoint(ctx, org, level, lat, lng)
if err != nil {
return fmt.Errorf("image at point: %w", err)
}

View file

@ -8,7 +8,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
@ -73,7 +73,7 @@ func apiGetDistrictLogo(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Logo not found", http.StatusNotFound)
return
}
userfile.ImageFileContentWriteLogo(w, org.LogoUUID.MustGet())
file.ImageFileContentWriteLogo(w, org.LogoUUID.MustGet())
return
default:
http.Error(w, "Too many organizations, this is a programmer error", http.StatusInternalServerError)

103
api/event.go Normal file
View file

@ -0,0 +1,103 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/rs/zerolog/log"
)
func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) {
}
type MessageHeartbeat struct {
Time time.Time `json:"time"`
}
type MessageSSE struct {
Content any `json:"content"`
Type string `json:"type"`
}
type ConnectionSSE struct {
chanState chan MessageSSE
id string
}
func (c *ConnectionSSE) SendMessage(w http.ResponseWriter, m MessageSSE) error {
return send(w, MessageSSE{
Type: "heartbeat",
})
}
func (c *ConnectionSSE) SendHeartbeat(w http.ResponseWriter, t time.Time) error {
return send(w, MessageSSE{
Content: MessageHeartbeat{
Time: t,
},
Type: "heartbeat",
})
}
func send[T any](w http.ResponseWriter, msg T) error {
jsonData, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("marshaling json: %w", err)
}
// Write in SSE format: "data: <json>\n\n"
_, err = fmt.Fprintf(w, "data: %s\n\n", jsonData)
if err != nil {
return fmt.Errorf("writing SSE message: %w", err)
}
w.(http.Flusher).Flush()
return nil
}
type Webserver struct {
connections map[*ConnectionSSE]bool
}
// sseHandler handles the Server-Sent Events connection
func (web *Webserver) sseHandler(w http.ResponseWriter, r *http.Request) {
// Set headers for SSE
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
connection := ConnectionSSE{
chanState: make(chan MessageSSE),
id: fmt.Sprintf("%d", time.Now().UnixNano()),
}
web.connections[&connection] = true
// Send an initial connected event
fmt.Fprintf(w, "event: connected\ndata: {\"status\": \"connected\", \"time\": \"%s\"}\n\n", time.Now().Format(time.RFC3339))
w.(http.Flusher).Flush()
// Keep the connection open with a ticker sending periodic events
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Use a channel to detect when the client disconnects
done := r.Context().Done()
// Keep connection open until client disconnects
var err error
for {
err = nil
select {
case <-done:
log.Info().Msg("Client closed connection")
return
case t := <-ticker.C:
// Send a heartbeat message
err = connection.SendHeartbeat(w, t)
//case state := <-connection.chanState:
//log.Debug().Msg("Sending new state to connection")
//err = connection.SendState(w, state)
}
if err != nil {
log.Error().Err(err).Msg("Failed to send state from webserver")
}
}
}

View file

@ -8,8 +8,6 @@ import (
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/auth"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
@ -19,7 +17,7 @@ import (
var decoder = schema.NewDecoder()
type handlerFunctionGet[T any] func(context.Context, *http.Request, *models.Organization, *models.User, queryParams) (*T, *nhttp.ErrorWithStatus)
type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User, queryParams) (*T, *nhttp.ErrorWithStatus)
type wrappedHandler func(http.ResponseWriter, *http.Request)
type contentAuthenticated[T any] struct {
C T
@ -32,26 +30,17 @@ type ErrorAPI struct {
}
func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
org, err := u.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if org == nil {
http.Error(w, "nil org", http.StatusInternalServerError)
return
}
var body []byte
var params queryParams
err = decoder.Decode(&params, r.URL.Query())
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
log.Error().Err(err).Msg("decode query failure")
http.Error(w, "failed to decode query", http.StatusInternalServerError)
return
}
resp, e := f(ctx, r, org, u, params)
resp, e := f(ctx, r, u, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
@ -74,10 +63,10 @@ func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler {
})
}
type handlerFunctionPost[ReqType any, ResponseType any] func(context.Context, *http.Request, *models.Organization, *models.User, ReqType) (ResponseType, *nhttp.ErrorWithStatus)
type handlerFunctionPost[ReqType any, ResponseType any] func(context.Context, *http.Request, platform.User, ReqType) (ResponseType, *nhttp.ErrorWithStatus)
func authenticatedHandlerJSONPost[ReqType any, ResponseType any](f handlerFunctionPost[ReqType, ResponseType]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, org *models.Organization, u *models.User) {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
w.Header().Set("Content-Type", "application/json")
var req ReqType
body, err := io.ReadAll(r.Body)
@ -91,7 +80,7 @@ func authenticatedHandlerJSONPost[ReqType any, ResponseType any](f handlerFuncti
return
}
ctx := r.Context()
response, e := f(ctx, r, org, u, req)
response, e := f(ctx, r, u, req)
if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err = json.Marshal(ErrorAPI{Message: e.Error()})

81
api/image.go Normal file
View file

@ -0,0 +1,81 @@
package api
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
func apiImagePost(w http.ResponseWriter, r *http.Request, u platform.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 {
//debugSaveRequest(body, err, "Image note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
ctx := r.Context()
setter := models.NoteImageSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(int32(u.ID)),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
err = platform.NoteImageCreate(ctx, u, setter)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiImageContentGet(w http.ResponseWriter, r *http.Request, u platform.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)
}
file.PublicImageFileToResponse(w, imageUUID)
w.WriteHeader(http.StatusOK)
}
func apiImageContentPost(w http.ResponseWriter, r *http.Request, u platform.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)
}
err = file.ImageFileContentWrite(imageUUID, r.Body)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusOK)
log.Printf("Saved image file %s\n", imageUUID)
fmt.Fprintf(w, "PNG uploaded successfully")
}

View file

@ -13,6 +13,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/geom"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
@ -34,12 +35,12 @@ type lead struct {
ID int32 `json:"id"`
}
func listLead(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListLead, *nhttp.ErrorWithStatus) {
func listLead(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListLead, *nhttp.ErrorWithStatus) {
return &contentListLead{
Leads: make([]lead, 0),
}, nil
}
func postLeads(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, req createLead) (*createdLead, *nhttp.ErrorWithStatus) {
func postLeads(ctx context.Context, r *http.Request, user platform.User, req createLead) (*createdLead, *nhttp.ErrorWithStatus) {
if len(req.SignalIDs) == 0 {
return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with no signals")
}
@ -54,13 +55,11 @@ func postLeads(ctx context.Context, r *http.Request, org *models.Organization, u
return nil, nhttp.NewError("start transaction: %w", err)
}
type _Row struct {
ID int32 `db:"site_id"`
Version int32 `db:"site_version"`
ID int32 `db:"site_id"`
}
site, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
sm.Columns(
"pool.site_id AS site_id",
"pool.site_version AS site_version",
),
sm.From("signal_pool"),
sm.InnerJoin("pool").OnEQ(
@ -68,13 +67,10 @@ func postLeads(ctx context.Context, r *http.Request, org *models.Organization, u
psql.Quote("pool", "id"),
),
sm.InnerJoin("site").On(
psql.And(
psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")),
psql.Quote("pool", "site_version").EQ(psql.Quote("site", "version")),
),
psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")),
),
sm.Where(psql.Quote("signal_pool", "signal_id").EQ(psql.Arg(signal_id))),
sm.Where(psql.Quote("site", "organization_id").EQ(psql.Arg(org.ID))),
sm.Where(psql.Quote("site", "organization_id").EQ(psql.Arg(user.Organization.ID()))),
), scan.StructMapper[_Row]())
if err != nil {
if err.Error() == "sql: no rows in result set" {
@ -85,11 +81,10 @@ func postLeads(ctx context.Context, r *http.Request, org *models.Organization, u
lead, err := models.Leads.Insert(&models.LeadSetter{
Created: omit.From(time.Now()),
Creator: omit.From(user.ID),
Creator: omit.From(int32(user.ID)),
// ID
OrganizationID: omit.From(org.ID),
OrganizationID: omit.From(int32(user.Organization.ID())),
SiteID: omitnull.From(site.ID),
SiteVersion: omitnull.From(site.Version),
Type: omit.From(enums.LeadtypeGreenPool),
}).One(ctx, txn)
if err != nil {

View file

@ -12,6 +12,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/rs/zerolog/log"
@ -35,7 +36,7 @@ type createReviewPool struct {
}
type createdReviewPool struct{}
func postReviewPool(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, req createReviewPool) (*createdReviewPool, *nhttp.ErrorWithStatus) {
func postReviewPool(ctx context.Context, r *http.Request, user platform.User, req createReviewPool) (*createdReviewPool, *nhttp.ErrorWithStatus) {
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
if err != nil {
return nil, nhttp.NewError("start txn: %w", err)
@ -43,7 +44,7 @@ func postReviewPool(ctx context.Context, r *http.Request, org *models.Organizati
defer txn.Rollback(ctx)
review_task, err := models.ReviewTasks.Query(
models.SelectWhere.ReviewTasks.ID.EQ(req.TaskID),
models.SelectWhere.ReviewTasks.OrganizationID.EQ(org.ID),
models.SelectWhere.ReviewTasks.OrganizationID.EQ(user.Organization.ID()),
).One(ctx, txn)
if err != nil {
return nil, nhttp.NewErrorStatus(http.StatusNotFound, "review task %d not found", req.TaskID)
@ -56,7 +57,7 @@ func postReviewPool(ctx context.Context, r *http.Request, org *models.Organizati
review_task.Update(ctx, txn, &models.ReviewTaskSetter{
Resolution: omitnull.From(resolution),
Reviewed: omitnull.From(time.Now()),
ReviewerID: omitnull.From(user.ID),
ReviewerID: omitnull.From(int32(user.ID)),
})
review_task_pool, err := models.ReviewTaskPools.Query(
models.SelectWhere.ReviewTaskPools.ReviewTaskID.EQ(review_task.ID),
@ -77,10 +78,10 @@ func postReviewPool(ctx context.Context, r *http.Request, org *models.Organizati
log.Info().Int32("id", review_task.ID).Str("status", req.Status).Msg("committed")
return &createdReviewPool{}, e
}
func discardReviewPool(ctx context.Context, txn bob.Tx, user *models.User, req createReviewPool, review_task_pool *models.ReviewTaskPool) *nhttp.ErrorWithStatus {
func discardReviewPool(ctx context.Context, txn bob.Tx, user platform.User, req createReviewPool, review_task_pool *models.ReviewTaskPool) *nhttp.ErrorWithStatus {
return nil
}
func commitReviewPool(ctx context.Context, txn bob.Tx, user *models.User, req createReviewPool, review_task_pool *models.ReviewTaskPool) *nhttp.ErrorWithStatus {
func commitReviewPool(ctx context.Context, txn bob.Tx, user platform.User, req createReviewPool, review_task_pool *models.ReviewTaskPool) *nhttp.ErrorWithStatus {
if req.Updates == nil {
return nil
}

View file

@ -9,7 +9,6 @@ import (
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
@ -32,7 +31,7 @@ type contentListReviewTaskPool struct {
Total int32 `json:"total"`
}
func listReviewTaskPool(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListReviewTaskPool, *nhttp.ErrorWithStatus) {
func listReviewTaskPool(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListReviewTaskPool, *nhttp.ErrorWithStatus) {
limit := 20
if query.Limit != nil {
limit = *query.Limit
@ -45,7 +44,7 @@ func listReviewTaskPool(ctx context.Context, r *http.Request, org *models.Organi
"COUNT(*) AS total",
),
sm.From("review_task"),
sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(org.ID))),
sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(user.Organization.ID()))),
sm.Where(psql.Quote("review_task", "reviewed").IsNull()),
), scan.StructMapper[_RowTotal]())
if err != nil {
@ -98,23 +97,20 @@ func listReviewTaskPool(ctx context.Context, r *http.Request, org *models.Organi
psql.Quote("feature", "id"),
),
sm.InnerJoin("site").On(
psql.And(
psql.Quote("feature", "site_id").EQ(psql.Quote("site", "id")),
psql.Quote("feature", "site_version").EQ(psql.Quote("site", "version")),
),
psql.Quote("feature", "site_id").EQ(psql.Quote("site", "id")),
),
sm.InnerJoin("address").OnEQ(
psql.Quote("site", "address_id"),
psql.Quote("address", "id"),
),
sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(org.ID))),
sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(user.Organization.ID()))),
sm.Where(psql.Quote("review_task", "reviewed").IsNull()),
sm.Limit(limit),
), scan.StructMapper[_Row]())
if err != nil {
return nil, nhttp.NewError("failed to get review tasks: %w", err)
}
users_by_id, err := platform.UsersByID(ctx, org)
users_by_id, err := platform.UsersByOrg(ctx, user.Organization)
if err != nil {
return nil, nhttp.NewError("users by id: %w", err)
}

View file

@ -14,6 +14,7 @@ func AddRoutes(r chi.Router) {
r.Method("POST", "/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost))
r.Method("GET", "/client/ios", auth.NewEnsureAuth(handleClientIos))
r.Method("GET", "/communication", authenticatedHandlerJSON(listCommunication))
r.Method("GET", "/events", auth.NewEnsureAuth(streamEvents))
r.Method("POST", "/image/{uuid}", auth.NewEnsureAuth(apiImagePost))
r.Method("GET", "/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentGet))
r.Method("POST", "/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost))

View file

@ -9,7 +9,6 @@ import (
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
@ -33,7 +32,7 @@ type contentListSignal struct {
Signals []signal `json:"signals"`
}
func listSignal(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListSignal, *nhttp.ErrorWithStatus) {
func listSignal(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListSignal, *nhttp.ErrorWithStatus) {
type _Row struct {
Address types.Address `db:"address"`
Addressed *time.Time `db:"addressed"`
@ -82,16 +81,13 @@ func listSignal(ctx context.Context, r *http.Request, org *models.Organization,
psql.Quote("pool", "id"),
),
sm.InnerJoin("site").On(
psql.And(
psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")),
psql.Quote("pool", "site_version").EQ(psql.Quote("site", "version")),
),
psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")),
),
sm.InnerJoin("address").OnEQ(
psql.Quote("site", "address_id"),
psql.Quote("address", "id"),
),
sm.Where(psql.Quote("signal", "organization_id").EQ(psql.Arg(org.ID))),
sm.Where(psql.Quote("signal", "organization_id").EQ(psql.Arg(user.Organization.ID()))),
sm.Where(psql.Quote("signal", "addressed").IsNull()),
sm.Limit(limit),
), scan.StructMapper[_Row]())
@ -105,7 +101,7 @@ func listSignal(ctx context.Context, r *http.Request, org *models.Organization,
if err != nil {
return nil, nhttp.NewError("failed to get signals: %w", err)
}
users_by_id, err := platform.UsersByID(ctx, org)
users_by_id, err := platform.UsersByOrg(ctx, user.Organization)
if err != nil {
return nil, nhttp.NewError("users by id: %w", err)
}

View file

@ -1,27 +1,15 @@
package api
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/aarondl/opt/omit"
//"github.com/Gleipnir-Technology/bob"
//"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/imagetile"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
func getTile(w http.ResponseWriter, r *http.Request, org *models.Organization, user *models.User) {
func getTile(w http.ResponseWriter, r *http.Request, user platform.User) {
x_str := chi.URLParam(r, "x")
y_str := chi.URLParam(r, "y")
z_str := chi.URLParam(r, "z")
@ -41,101 +29,10 @@ func getTile(w http.ResponseWriter, r *http.Request, org *models.Organization, u
http.Error(w, "can't parse x as an integer", http.StatusBadRequest)
return
}
err = handleTile(r.Context(), w, org, uint(z), uint(y), uint(x))
err = platform.GetTile(r.Context(), w, user.Organization, uint(z), uint(y), uint(x))
if err != nil {
log.Error().Err(err).Msg("failed to do tile")
http.Error(w, "failed to do tile", http.StatusInternalServerError)
return
}
}
func handleTile(ctx context.Context, w http.ResponseWriter, org *models.Organization, z, y, x uint) error {
if org.ArcgisMapServiceID.IsNull() {
return fmt.Errorf("no map service ID set")
}
map_service_id := org.ArcgisMapServiceID.MustGet()
tile_path := tilePath(map_service_id, z, y, x)
tile_row, err := models.TileCachedImages.Query(
models.SelectWhere.TileCachedImages.ArcgisID.EQ(map_service_id),
models.SelectWhere.TileCachedImages.X.EQ(int32(x)),
models.SelectWhere.TileCachedImages.Y.EQ(int32(y)),
models.SelectWhere.TileCachedImages.Z.EQ(int32(z)),
).One(ctx, db.PGInstance.BobDB)
if err == nil {
var tile *imagetile.TileRaster
if tile_row.IsEmpty {
tile = imagetile.TileRasterPlaceholder()
} else {
tile, err = loadTileFromDisk(tile_path)
if err != nil {
return fmt.Errorf("load tile from disk: %w", err)
}
}
log.Debug().Uint("z", z).Uint("y", y).Uint("x", x).Bool("is empty", tile_row.IsEmpty).Msg("tile from cache")
return writeTile(w, tile)
}
if err.Error() != "sql: no rows in result set" {
return fmt.Errorf("query db: %w", err)
}
image, err := imagetile.ImageAtTile(ctx, org, uint(z), uint(y), uint(x))
if err != nil {
return fmt.Errorf("image at tile: %w", err)
}
if !image.IsPlaceholder {
err = saveTileToDisk(image, tile_path)
if err != nil {
return fmt.Errorf("save tile: %w", err)
}
}
_, err = models.TileCachedImages.Insert(&models.TileCachedImageSetter{
ArcgisID: omit.From(map_service_id),
X: omit.From(int32(x)),
Y: omit.From(int32(y)),
Z: omit.From(int32(z)),
IsEmpty: omit.From(image.IsPlaceholder),
}).One(ctx, db.PGInstance.BobDB)
if err != nil {
return fmt.Errorf("save to db: %w", err)
}
log.Debug().Uint("z", z).Uint("y", y).Uint("x", x).Bool("placeholder", image.IsPlaceholder).Msg("caching tile")
return writeTile(w, image)
}
func loadTileFromDisk(tile_path string) (*imagetile.TileRaster, error) {
file, err := os.Open(tile_path)
if err != nil {
return nil, fmt.Errorf("open: %w", err)
}
defer file.Close()
img, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("readall from %s: %w", tile_path, err)
}
return &imagetile.TileRaster{
Content: img,
IsPlaceholder: false,
}, nil
}
func saveTileToDisk(image *imagetile.TileRaster, tile_path string) error {
parent := filepath.Dir(tile_path)
err := os.MkdirAll(parent, 0750)
if err != nil {
return fmt.Errorf("mkdirall: %w", err)
}
err = os.WriteFile(tile_path, image.Content, 0644)
if err != nil {
return fmt.Errorf("write image file: %w", err)
}
return nil
}
func tilePath(map_service_id string, z, y, x uint) string {
return fmt.Sprintf("%s/tile-cache/%s/%d/%d/%d.raw", config.FilesDirectory, map_service_id, z, y, x)
}
func writeTile(w http.ResponseWriter, image *imagetile.TileRaster) error {
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(image.Content)))
_, err := io.Copy(w, bytes.NewBuffer(image.Content))
if err != nil {
return fmt.Errorf("io.copy: %w", err)
}
return nil
}