Move all sync pages to authenticatedHandler

Still need to fix many templates
This commit is contained in:
Eli Ribble 2026-02-24 15:34:53 +00:00
parent 85d2d0b95b
commit dac52a879a
No known key found for this signature in database
23 changed files with 409 additions and 476 deletions

View file

@ -51,7 +51,7 @@ func (ts templateSystemDisk) renderOrError(w http.ResponseWriter, template_name
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err = t.Execute(buf, context) err = t.Execute(buf, context)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to render template") log.Error().Err(err).Str("name", template_name).Msg("Failed to render template")
RespondError(w, "Failed to render template", err, http.StatusInternalServerError) RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
return return
} }

View file

@ -29,7 +29,7 @@
<div <div
class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end" class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end"
> >
{{ if .IsSyncOngoing }} {{ if .C.IsSyncOngoing }}
<p class="last-refreshed mb-0"> <p class="last-refreshed mb-0">
<i class="fas fa-sync-alt me-2 syncing"></i>Syncing now... <i class="fas fa-sync-alt me-2 syncing"></i>Syncing now...
</p> </p>
@ -37,7 +37,7 @@
<p class="last-refreshed mb-0"> <p class="last-refreshed mb-0">
<i class="fas fa-sync-alt me-2"></i>Last updated: <i class="fas fa-sync-alt me-2"></i>Last updated:
<span id="last-refreshed-time" <span id="last-refreshed-time"
>{{ .LastSync | timeRelativePtr }}</span >{{ .C.LastSync | timeRelativePtr }}</span
> >
<button class="btn btn-sm btn-outline-primary ms-3"> <button class="btn btn-sm btn-outline-primary ms-3">
Refresh Data Refresh Data
@ -57,7 +57,7 @@
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
</div> </div>
<h5 class="card-title">Last Data Refresh</h5> <h5 class="card-title">Last Data Refresh</h5>
<p class="metric-value">{{ .LastSync | timeRelativePtr }}</p> <p class="metric-value">{{ .C.LastSync | timeRelativePtr }}</p>
<!-- <p class="card-text text-muted">Last sync: 12:45 PM</p> --> <!-- <p class="card-text text-muted">Last sync: 12:45 PM</p> -->
</div> </div>
</div> </div>
@ -71,13 +71,13 @@
<i class="fas fa-ticket-alt"></i> <i class="fas fa-ticket-alt"></i>
</div> </div>
<h5 class="card-title">Service Requests</h5> <h5 class="card-title">Service Requests</h5>
{{ if .IsSyncOngoing }} {{ if .C.IsSyncOngoing }}
<p class="metric-value"> <p class="metric-value">
{{ .CountServiceRequests | bigNumber }}...? {{ .C.CountServiceRequests | bigNumber }}...?
</p> </p>
{{ else }} {{ else }}
<p class="metric-value"> <p class="metric-value">
{{ .CountServiceRequests | bigNumber }} {{ .C.CountServiceRequests | bigNumber }}
</p> </p>
{{ end }} {{ end }}
<!--<p class="card-text text-muted"> <!--<p class="card-text text-muted">
@ -97,13 +97,13 @@
<i class="fas fa-bug"></i> <i class="fas fa-bug"></i>
</div> </div>
<h5 class="card-title">Mosquito Sources</h5> <h5 class="card-title">Mosquito Sources</h5>
{{ if .IsSyncOngoing }} {{ if .C.IsSyncOngoing }}
<p class="metric-value"> <p class="metric-value">
{{ .CountMosquitoSources | bigNumber }}..? {{ .C.CountMosquitoSources | bigNumber }}..?
</p> </p>
{{ else }} {{ else }}
<p class="metric-value"> <p class="metric-value">
{{ .CountMosquitoSources | bigNumber }} {{ .C.CountMosquitoSources | bigNumber }}
</p> </p>
{{ end }} {{ end }}
<!-- <p class="card-text text-muted"> <!-- <p class="card-text text-muted">
@ -123,10 +123,10 @@
<i class="fas fa-clipboard-check"></i> <i class="fas fa-clipboard-check"></i>
</div> </div>
<h5 class="card-title">Traps</h5> <h5 class="card-title">Traps</h5>
{{ if .IsSyncOngoing }} {{ if .C.IsSyncOngoing }}
<p class="metric-value">{{ .CountTraps | bigNumber }}...?</p> <p class="metric-value">{{ .C.CountTraps | bigNumber }}...?</p>
{{ else }} {{ else }}
<p class="metric-value">{{ .CountTraps | bigNumber }}</p> <p class="metric-value">{{ .C.CountTraps | bigNumber }}</p>
{{ end }} {{ end }}
<!-- <p class="card-text text-muted"> <!-- <p class="card-text text-muted">
<span class="text-success"> <span class="text-success">
@ -142,17 +142,21 @@
<h3 class="section-title">Mosquito Activity Heatmap</h3> <h3 class="section-title">Mosquito Activity Heatmap</h3>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<map-aggregate {{ if not (eq .Organization nil) }}
centroid="{{ if .Organization.ServiceAreaCentroidGeojson.IsValue }} {{ if not (eq .Organization.ServiceAreaCentroidGeojson nil) }}
{{ .Organization.ServiceAreaCentroidGeojson.MustGet|json }} <map-aggregate
{{ end }}" centroid="{{ if .Organization.ServiceAreaCentroidGeojson.IsValue }}
organization-id="{{ .Organization.ID }}" {{ .Organization.ServiceAreaCentroidGeojson.MustGet|json }}
tegola="{{ .URL.Tegola }}" {{ end }}"
xmin="{{ .Organization.ServiceAreaXmin.GetOr 0 }}" organization-id="{{ .Organization.ID }}"
ymin="{{ .Organization.ServiceAreaYmin.GetOr 0 }}" tegola="{{ .URL.Tegola }}"
xmax="{{ .Organization.ServiceAreaXmax.GetOr 0 }}" xmin="{{ .Organization.ServiceAreaXmin.GetOr 0 }}"
ymax="{{ .Organization.ServiceAreaYmax.GetOr 0 }}" ymin="{{ .Organization.ServiceAreaYmin.GetOr 0 }}"
/> xmax="{{ .Organization.ServiceAreaXmax.GetOr 0 }}"
ymax="{{ .Organization.ServiceAreaYmax.GetOr 0 }}"
/>
{{ end }}
{{ end }}
</div> </div>
</div> </div>
@ -174,7 +178,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range $i, $sr := .RecentRequests }} {{ range $i, $sr := .C.RecentRequests }}
<tr> <tr>
<td>{{ $sr.Date | timeRelativePtr }}</td> <td>{{ $sr.Date | timeRelativePtr }}</td>
<td>Service Request</td> <td>Service Request</td>

View file

@ -48,6 +48,9 @@
setTooltipsForSidebar(); setTooltipsForSidebar();
}); });
</script> </script>
{{ if not .Config.IsProductionEnvironment }}
<script src="/.flogo/injector.js"></script>
{{ end }}
</head> </head>
<body> <body>
{{ template "sync/component/icons.html" }} {{ template "sync/component/icons.html" }}
@ -60,6 +63,7 @@
<div id="content"> <div id="content">
{{ template "content" . }} {{ template "content" . }}
</div> </div>
<div id="flogo"></div>
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script> <script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
<script> <script>
document.getElementById("sidebarToggle").addEventListener("click", () => { document.getElementById("sidebarToggle").addEventListener("click", () => {

View file

@ -11,9 +11,13 @@
<!-- favicon --> <!-- favicon -->
<link rel="icon" href="/static/favicon-sync.ico" type="image/x-icon" /> <link rel="icon" href="/static/favicon-sync.ico" type="image/x-icon" />
{{ block "extraheader" . }}{{ end }} {{ block "extraheader" . }}{{ end }}
{{ if not .Config.IsProductionEnvironment }}
<script src="/.flogo/injector.js"></script>
{{ end }}
</head> </head>
<body> <body>
{{ template "content" . }} {{ template "content" . }}
<div id="flogo"></div>
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script> <script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View file

@ -75,7 +75,12 @@ func main() {
defer sentryWriter.Close() defer sentryWriter.Close()
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.MultiLevelWriter(zerolog.ConsoleWriter{Out: os.Stderr}, sentryWriter)).Level(zerolog.InfoLevel) log.Logger = log.Output(zerolog.MultiLevelWriter(zerolog.ConsoleWriter{Out: os.Stderr}, sentryWriter))
if os.Getenv("VERBOSE") != "" {
log.Logger = log.Logger.Level(zerolog.DebugLevel)
} else {
log.Logger = log.Logger.Level(zerolog.InfoLevel)
}
err = db.InitializeDatabase(context.TODO(), config.PGDSN) err = db.InitializeDatabase(context.TODO(), config.PGDSN)
if err != nil { if err != nil {

View file

@ -2,13 +2,14 @@ package sync
import ( import (
"context" "context"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
) )
type contentAdminDash struct{} type contentAdminDash struct{}
func getAdminDash(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getAdminDash(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentAdminDash], *errorWithStatus) {
content := contentAdminDash{} content := contentAdminDash{}
return "sync/admin-dash.html", content, nil return newResponse("sync/admin-dash.html", content), nil
} }

View file

@ -1,12 +1,11 @@
package sync package sync
import ( import (
"context"
"net/http" "net/http"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/h3utils" "github.com/Gleipnir-Technology/nidus-sync/h3utils"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/uber/h3-go/v4" "github.com/uber/h3-go/v4"
) )
@ -18,70 +17,48 @@ type contentCell struct {
MapData ComponentMap MapData ComponentMap
Traps []TrapSummary Traps []TrapSummary
Treatments []Treatment Treatments []Treatment
URL ContentURL
User User
} }
func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) { func getCellDetails(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentCell], *errorWithStatus) {
cell_str := chi.URLParam(r, "cell") cell_str := chi.URLParam(r, "cell")
if cell_str == "" { if cell_str == "" {
respondError(w, "There should always be a cell", nil, http.StatusBadRequest) return nil, newErrorStatus(http.StatusBadRequest, "There should always be a cell")
return
} }
c, err := HexToInt64(cell_str) c, err := HexToInt64(cell_str)
if err != nil { if err != nil {
respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest) return nil, newErrorStatus(http.StatusBadRequest, "Cannot convert provided cell to uint64")
return
}
ctx := r.Context()
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
return
}
userContent, err := contentForUser(ctx, user)
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
} }
center, err := h3.Cell(c).LatLng() center, err := h3.Cell(c).LatLng()
if err != nil { if err != nil {
respondError(w, "Failed to get center", err, http.StatusInternalServerError) return nil, newError("Failed to get center: %w", err)
return
} }
boundary, err := h3.Cell(c).Boundary() boundary, err := h3.Cell(c).Boundary()
if err != nil { if err != nil {
respondError(w, "Failed to get boundary", err, http.StatusInternalServerError) return nil, newError("Failed to get boundary: %w", err)
return
} }
inspections, err := inspectionsByCell(ctx, org, h3.Cell(c)) inspections, err := inspectionsByCell(ctx, org, h3.Cell(c))
if err != nil { if err != nil {
respondError(w, "Failed to get inspections by cell", err, http.StatusInternalServerError) return nil, newError("Failed to get inspections by cell: %w", err)
return
} }
geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)}) geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)})
if err != nil { if err != nil {
respondError(w, "Failed to get boundaries", err, http.StatusInternalServerError) return nil, newError("Failed to get boundaries: %w", err)
return
} }
resolution := h3.Cell(c).Resolution() resolution := h3.Cell(c).Resolution()
sources, err := breedingSourcesByCell(ctx, org, h3.Cell(c)) sources, err := breedingSourcesByCell(ctx, org, h3.Cell(c))
if err != nil { if err != nil {
respondError(w, "Failed to get sources", err, http.StatusInternalServerError) return nil, newError("Failed to get sources: %w", err)
return
} }
traps, err := trapsByCell(ctx, org, h3.Cell(c)) traps, err := trapsByCell(ctx, org, h3.Cell(c))
if err != nil { if err != nil {
respondError(w, "Failed to get traps", err, http.StatusInternalServerError) return nil, newError("Failed to get traps: %w", err)
return
} }
treatments, err := treatmentsByCell(ctx, org, h3.Cell(c)) treatments, err := treatmentsByCell(ctx, org, h3.Cell(c))
if err != nil { if err != nil {
respondError(w, "Failed to get treatments", err, http.StatusInternalServerError) return nil, newError("Failed to get treatments: %w", err)
return
} }
data := contentCell{ return newResponse("sync/cell.html", contentCell{
BreedingSources: sources, BreedingSources: sources,
CellBoundary: boundary, CellBoundary: boundary,
Inspections: inspections, Inspections: inspections,
@ -95,8 +72,5 @@ func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) {
}, },
Traps: traps, Traps: traps,
Treatments: treatments, Treatments: treatments,
URL: newContentURL(), }), nil
User: userContent,
}
html.RenderOrError(w, "sync/cell.html", &data)
} }

15
sync/config.go Normal file
View file

@ -0,0 +1,15 @@
package sync
import (
"github.com/Gleipnir-Technology/nidus-sync/config"
)
type contentConfig struct {
IsProductionEnvironment bool
}
func newContentConfig() contentConfig {
return contentConfig{
IsProductionEnvironment: config.IsProductionEnvironment(),
}
}

View file

@ -21,10 +21,7 @@ import (
// Authenticated pages // Authenticated pages
var () var ()
type Config struct { type contentSource struct {
}
type ContentSource struct {
Inspections []Inspection Inspections []Inspection
MapData ComponentMap MapData ComponentMap
Source *BreedingSourceDetail Source *BreedingSourceDetail
@ -34,12 +31,12 @@ type ContentSource struct {
TreatmentModels []TreatmentModel TreatmentModels []TreatmentModel
User User User User
} }
type ContentTrap struct { type contentTrap struct {
MapData ComponentMap MapData ComponentMap
Trap Trap Trap Trap
User User User User
} }
type ContentDashboard struct { type contentDashboard struct {
CountTraps int CountTraps int
CountMosquitoSources int CountMosquitoSources int
CountServiceRequests int CountServiceRequests int
@ -47,13 +44,10 @@ type ContentDashboard struct {
IsSyncOngoing bool IsSyncOngoing bool
LastSync *time.Time LastSync *time.Time
MapData ComponentMap MapData ComponentMap
Organization *models.Organization
RecentRequests []ServiceRequestSummary RecentRequests []ServiceRequestSummary
URL ContentURL
User User
} }
type ContentLayoutTest struct { type contentLayoutTest struct {
User User User User
} }
type ContentDistrict struct { type ContentDistrict struct {
@ -67,16 +61,12 @@ func getDistrict(w http.ResponseWriter, r *http.Request) {
html.RenderOrError(w, "sync/district.html", &context) html.RenderOrError(w, "sync/district.html", &context)
} }
func getLayoutTest(w http.ResponseWriter, r *http.Request, u *models.User) { func getLayoutTest(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentLayoutTest], *errorWithStatus) {
userContent, err := contentForUser(r.Context(), u) return newResponse("sync/layout-test.html", contentLayoutTest{}), nil
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
html.RenderOrError(w, "sync/layout-test.html", &ContentLayoutTest{User: userContent})
} }
func getRoot(w http.ResponseWriter, r *http.Request) { func getRoot(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := auth.GetAuthenticatedUser(r) user, err := auth.GetAuthenticatedUser(r)
if err != nil { if err != nil {
// No credentials or user not found: go to login // No credentials or user not found: go to login
@ -93,77 +83,135 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
signin(w, errorCode, "/") signin(w, errorCode, "/")
return return
} else { } else {
has, err := background.HasFieldseekerConnection(r.Context(), user) has, err := background.HasFieldseekerConnection(ctx, user)
if err != nil { if err != nil {
respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError) respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError)
return return
} }
if has { if has {
dashboard(r.Context(), w, user) org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get organization", err, http.StatusInternalServerError)
return
}
dashboard(ctx, w, org, user)
return return
} else { } else {
oauthPrompt(w, r, user) oauthPrompt(w, r, user)
return return
} }
} }
if err != nil {
respondError(w, "Failed to render root", err, http.StatusInternalServerError)
}
} }
func getSource(w http.ResponseWriter, r *http.Request, u *models.User) { func getSource(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentSource], *errorWithStatus) {
globalid_s := chi.URLParam(r, "globalid") globalid_s := chi.URLParam(r, "globalid")
if globalid_s == "" { if globalid_s == "" {
respondError(w, "No globalid provided", nil, http.StatusBadRequest) return nil, newError("No globalid provided: %w", nil)
return
} }
globalid, err := uuid.Parse(globalid_s) globalid, err := uuid.Parse(globalid_s)
if err != nil { if err != nil {
respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) return nil, newError("globalid is not a UUID: %w", nil)
return
} }
source(w, r, u, globalid) userContent, err := contentForUser(r.Context(), user)
if err != nil {
return nil, newError("Failed to get user content: %w", err)
}
s, err := sourceByGlobalId(r.Context(), org, globalid)
if err != nil {
return nil, newError("Failed to get source: %w", err)
}
inspections, err := inspectionsBySource(r.Context(), org, globalid)
if err != nil {
return nil, newError("Failed to get inspections: %w", err)
}
traps, err := trapsBySource(r.Context(), org, globalid)
if err != nil {
return nil, newError("Failed to get traps: %w", err)
}
treatments, err := treatmentsBySource(r.Context(), org, globalid)
if err != nil {
return nil, newError("Failed to get treatments: %w", err)
}
treatment_models := modelTreatment(treatments)
latlng, err := s.H3Cell.LatLng()
if err != nil {
return nil, newError("Failed to get latlng: %w", err)
}
data := contentSource{
Inspections: inspections,
MapData: ComponentMap{
Center: latlng,
//GeoJSON:
MapboxToken: config.MapboxToken,
Markers: []MapMarker{
MapMarker{
LatLng: latlng,
},
},
Zoom: 13,
},
Source: s,
Traps: traps,
Treatments: treatments,
TreatmentModels: treatment_models,
User: userContent,
}
return newResponse("sync/source.html", data), nil
} }
func getStadia(w http.ResponseWriter, r *http.Request, u *models.User) { func getStadia(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentDashboard], *errorWithStatus) {
userContent, err := contentForUser(r.Context(), u) data := contentDashboard{
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
data := ContentDashboard{
MapData: ComponentMap{ MapData: ComponentMap{
MapboxToken: config.MapboxToken, MapboxToken: config.MapboxToken,
}, },
URL: newContentURL(),
User: userContent,
} }
html.RenderOrError(w, "sync/stadia.html", data) return newResponse("sync/stadia.html", data), nil
} }
func getTemplateTest(w http.ResponseWriter, r *http.Request) { func getTemplateTest(w http.ResponseWriter, r *http.Request) {
html.RenderOrError(w, "sync/template-test.html", nil) html.RenderOrError(w, "sync/template-test.html", nil)
} }
func getTrap(w http.ResponseWriter, r *http.Request, u *models.User) { func getTrap(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentTrap], *errorWithStatus) {
globalid_s := chi.URLParam(r, "globalid") globalid_s := chi.URLParam(r, "globalid")
if globalid_s == "" { if globalid_s == "" {
respondError(w, "No globalid provided", nil, http.StatusBadRequest) return nil, newError("No globalid provided: %w", nil)
return
} }
globalid, err := uuid.Parse(globalid_s) globalid, err := uuid.Parse(globalid_s)
if err != nil { if err != nil {
respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) return nil, newError("globalid is not a UUID: %w", nil)
return
} }
trap(w, r, u, globalid) userContent, err := contentForUser(r.Context(), user)
if err != nil {
return nil, newError("Failed to get user content: %w", err)
}
t, err := trapByGlobalId(r.Context(), org, globalid)
if err != nil {
return nil, newError("Failed to get trap: %w", err)
}
latlng, err := t.H3Cell.LatLng()
if err != nil {
return nil, newError("Failed to get latlng: %w", err)
}
data := contentTrap{
MapData: ComponentMap{
Center: latlng,
//GeoJSON:
MapboxToken: config.MapboxToken,
Markers: []MapMarker{
MapMarker{
LatLng: latlng,
},
},
Zoom: 13,
},
Trap: t,
User: userContent,
}
return newResponse("sync/trap.html", data), nil
} }
func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) { func dashboard(ctx context.Context, w http.ResponseWriter, org *models.Organization, user *models.User) {
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
return
}
var lastSync *time.Time var lastSync *time.Time
sync, err := org.FieldseekerSyncs(sm.OrderBy("created").Desc()).One(ctx, db.PGInstance.BobDB) sync, err := org.FieldseekerSyncs(sm.OrderBy("created").Desc()).One(ctx, db.PGInstance.BobDB)
if err != nil { if err != nil {
@ -204,12 +252,7 @@ func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
Status: "Completed", Status: "Completed",
}) })
} }
userContent, err := contentForUser(ctx, user) content := contentDashboard{
if err != nil {
respondError(w, "Failed to get user context", err, http.StatusInternalServerError)
return
}
data := ContentDashboard{
CountTraps: int(trapCount), CountTraps: int(trapCount),
CountMosquitoSources: int(sourceCount), CountMosquitoSources: int(sourceCount),
CountServiceRequests: int(serviceCount), CountServiceRequests: int(serviceCount),
@ -218,111 +261,22 @@ func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
MapData: ComponentMap{ MapData: ComponentMap{
MapboxToken: config.MapboxToken, MapboxToken: config.MapboxToken,
}, },
Organization: org,
RecentRequests: requests, RecentRequests: requests,
URL: newContentURL(),
User: userContent,
} }
html.RenderOrError(w, "sync/dashboard.html", data) userContent, err := contentForUser(ctx, user)
}
func source(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUID) {
org, err := user.Organization().One(r.Context(), db.PGInstance.BobDB)
if err != nil { if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
userContent, err := contentForUser(r.Context(), user) html.RenderOrError(w, "sync/dashboard.html", contentAuthenticated[contentDashboard]{
if err != nil { C: content,
respondError(w, "Failed to get user content", err, http.StatusInternalServerError) URL: newContentURL(),
return
}
s, err := sourceByGlobalId(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get source", err, http.StatusInternalServerError)
return
}
inspections, err := inspectionsBySource(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get inspections", err, http.StatusInternalServerError)
return
}
traps, err := trapsBySource(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get traps", err, http.StatusInternalServerError)
return
}
treatments, err := treatmentsBySource(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get treatments", err, http.StatusInternalServerError)
return
}
treatment_models := modelTreatment(treatments)
latlng, err := s.H3Cell.LatLng()
if err != nil {
respondError(w, "Failed to get latlng", err, http.StatusInternalServerError)
return
}
data := ContentSource{
Inspections: inspections,
MapData: ComponentMap{
Center: latlng,
//GeoJSON:
MapboxToken: config.MapboxToken,
Markers: []MapMarker{
MapMarker{
LatLng: latlng,
},
},
Zoom: 13,
},
Source: s,
Traps: traps,
Treatments: treatments,
TreatmentModels: treatment_models,
User: userContent,
}
html.RenderOrError(w, "sync/source.html", data)
}
func trap(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUID) {
org, err := user.Organization().One(r.Context(), db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
return
}
userContent, err := contentForUser(r.Context(), user)
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
t, err := trapByGlobalId(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get trap", err, http.StatusInternalServerError)
return
}
latlng, err := t.H3Cell.LatLng()
if err != nil {
respondError(w, "Failed to get latlng", err, http.StatusInternalServerError)
return
}
data := ContentTrap{
MapData: ComponentMap{
Center: latlng,
//GeoJSON:
MapboxToken: config.MapboxToken,
Markers: []MapMarker{
MapMarker{
LatLng: latlng,
},
},
Zoom: 13,
},
Trap: t,
User: userContent, User: userContent,
} })
}
html.RenderOrError(w, "sync/trap.html", data)
func source(w http.ResponseWriter, r *http.Request, org *models.Organization, user *models.User, id uuid.UUID) {
}
func trap(w http.ResponseWriter, r *http.Request, org *models.Organization, user *models.User, id uuid.UUID) {
} }

View file

@ -2,13 +2,14 @@ package sync
import ( import (
"context" "context"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
) )
type contentDownloadPlaceholder struct{} type contentDownloadPlaceholder struct{}
func getDownloadList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getDownloadList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentDownloadPlaceholder], *errorWithStatus) {
content := contentSettingPlaceholder{} content := contentDownloadPlaceholder{}
return "sync/download-list.html", content, nil return newResponse("sync/download-list.html", content), nil
} }

View file

@ -2,13 +2,14 @@ package sync
import ( import (
"context" "context"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
) )
type contentMessageList struct{} type contentMessageList struct{}
func getMessageList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getMessageList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentMessageList], *errorWithStatus) {
content := contentMessageList{} content := contentMessageList{}
return "sync/message-list.html", content, nil return newResponse("sync/message-list.html", content), nil
} }

View file

@ -1,14 +1,13 @@
package sync package sync
import ( import (
//"context" "context"
//"fmt" //"fmt"
"net/http" "net/http"
//"strings" //"strings"
//"time" //"time"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/Gleipnir-Technology/nidus-sync/notification" "github.com/Gleipnir-Technology/nidus-sync/notification"
//"github.com/Gleipnir-Technology/bob" //"github.com/Gleipnir-Technology/bob"
//"github.com/Gleipnir-Technology/bob/dialect/psql" //"github.com/Gleipnir-Technology/bob/dialect/psql"
@ -21,22 +20,14 @@ import (
type contentNotificationList struct { type contentNotificationList struct {
Notifications []notification.Notification Notifications []notification.Notification
User User
} }
func getNotificationList(w http.ResponseWriter, r *http.Request, u *models.User) { func getNotificationList(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentNotificationList], *errorWithStatus) {
userContent, err := contentForUser(r.Context(), u)
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
ctx := r.Context()
notifications, err := notification.ForUser(ctx, u) notifications, err := notification.ForUser(ctx, u)
if err != nil { if err != nil {
respondError(w, "Failed to get notifications", err, http.StatusInternalServerError) return nil, newError("Failed to get notifications: %w", err)
} }
html.RenderOrError(w, "sync/notification-list.html", &contentNotificationList{ return newResponse("sync/notification-list.html", contentNotificationList{
Notifications: notifications, Notifications: notifications,
User: userContent, }), nil
})
} }

View file

@ -13,9 +13,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type ContextOauthPrompt struct { type ContextOauthPrompt struct{}
User User
}
// Build the ArcGIS authorization URL with PKCE // Build the ArcGIS authorization URL with PKCE
func buildArcGISAuthURL(clientID string) string { func buildArcGISAuthURL(clientID string) string {
@ -77,13 +75,6 @@ func getOAuthRefresh(w http.ResponseWriter, r *http.Request) {
} }
func oauthPrompt(w http.ResponseWriter, r *http.Request, user *models.User) { func oauthPrompt(w http.ResponseWriter, r *http.Request, user *models.User) {
userContent, err := contentForUser(r.Context(), user) data := ContextOauthPrompt{}
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
data := ContextOauthPrompt{
User: userContent,
}
html.RenderOrError(w, "sync/oauth-prompt.html", data) html.RenderOrError(w, "sync/oauth-prompt.html", data)
} }

View file

@ -1,123 +1,20 @@
package sync package sync
import ( import (
"fmt" "context"
"net/http" "net/http"
"strconv"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/go-chi/chi/v5"
) )
type contentPoolDetail struct { type contentPoolList struct{}
CSVFileID int32
Organization *models.Organization
Upload platform.UploadPoolDetail
URL ContentURL
User User
}
type ContentPoolList struct {
Uploads []platform.PoolUpload
URL ContentURL
User User
}
type ContentPoolUpload struct {
URL ContentURL
User User
}
func getPoolList(w http.ResponseWriter, r *http.Request, u *models.User) { func getPoolList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentPoolList], *errorWithStatus) {
ctx := r.Context() return newResponse("sync/pool-list.html", contentPoolList{}), nil
userContent, err := contentForUser(ctx, u)
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
uploads, err := platform.PoolUploadList(ctx, u.OrganizationID)
if err != nil {
respondError(w, "Failed to get uploads", err, http.StatusInternalServerError)
return
}
data := ContentPoolList{
Uploads: uploads,
URL: newContentURL(),
User: userContent,
}
html.RenderOrError(w, "sync/pool-list.html", data)
} }
func getPoolCreate(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentPoolList], *errorWithStatus) {
func getPoolUpload(w http.ResponseWriter, r *http.Request, u *models.User) { return newResponse("sync/pool-upload.html", contentPoolList{}), nil
userContent, err := contentForUser(r.Context(), u)
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
data := ContentPoolUpload{
URL: newContentURL(),
User: userContent,
}
html.RenderOrError(w, "sync/pool-csv-upload.html", data)
} }
func getPoolUploadByID(w http.ResponseWriter, r *http.Request, u *models.User) { func getPoolByID(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentPoolList], *errorWithStatus) {
ctx := r.Context() return newResponse("sync/pool-by-id.html", contentPoolList{}), nil
userContent, err := contentForUser(ctx, u)
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
org, err := u.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get organization", err, http.StatusInternalServerError)
return
}
file_id_str := chi.URLParam(r, "id")
file_id, err := strconv.ParseInt(file_id_str, 10, 32)
if err != nil {
respondError(w, "Failed to parse file_id", err, http.StatusInternalServerError)
return
}
detail, err := platform.GetUploadPoolDetail(ctx, u.OrganizationID, int32(file_id))
if err != nil {
respondError(w, "Failed to get pool", err, http.StatusInternalServerError)
return
}
data := contentPoolDetail{
CSVFileID: int32(file_id),
Organization: org,
Upload: detail,
URL: newContentURL(),
User: userContent,
}
html.RenderOrError(w, "sync/pool-by-id.html", data)
}
func postPoolUpload(w http.ResponseWriter, r *http.Request, u *models.User) {
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
if err != nil {
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
return
}
uploads, err := userfile.SaveFileUpload(r, "csvfile", userfile.CollectionCSV)
if err != nil {
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
return
}
if len(uploads) == 0 {
respondError(w, "No upload found", nil, http.StatusBadRequest)
return
}
if len(uploads) != 1 {
respondError(w, "You must only submit one file at a time", nil, http.StatusBadRequest)
return
}
upload := uploads[0]
pool_upload, err := platform.NewPoolUpload(r.Context(), u, upload)
if err != nil {
respondError(w, "Failed to create new pool", err, http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/pool/upload/%d", pool_upload.ID), http.StatusFound)
} }

View file

@ -2,6 +2,7 @@ package sync
import ( import (
"context" "context"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
@ -11,13 +12,13 @@ type contentRadar struct {
Organization *models.Organization Organization *models.Organization
} }
func getRadar(ctx context.Context, user *models.User) (string, contentRadar, *errorWithStatus) { func getRadar(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentRadar], *errorWithStatus) {
org, err := user.Organization().One(ctx, db.PGInstance.BobDB) org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil { if err != nil {
return "", contentRadar{}, newError("get org: %w", err) return nil, newError("get org: %w", err)
} }
data := contentRadar{ data := contentRadar{
Organization: org, Organization: org,
} }
return "sync/radar.html", data, nil return newResponse("sync/radar.html", data), nil
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/api" "github.com/Gleipnir-Technology/nidus-sync/api"
"github.com/Gleipnir-Technology/nidus-sync/auth" "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/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html" "github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -46,34 +47,37 @@ func Router() chi.Router {
// Authenticated endpoints // Authenticated endpoints
r.Route("/api", api.AddRoutes) r.Route("/api", api.AddRoutes)
r.Method("GET", "/admin", authenticatedHandler(getAdminDash)) r.Method("GET", "/admin", authenticatedHandler(getAdminDash))
r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) r.Method("GET", "/cell/{cell}", authenticatedHandler(getCellDetails))
r.Method("GET", "/download", authenticatedHandler(getDownloadList)) r.Method("GET", "/download", authenticatedHandler(getDownloadList))
r.Method("GET", "/layout-test", auth.NewEnsureAuth(getLayoutTest)) r.Method("GET", "/layout-test", authenticatedHandler(getLayoutTest))
r.Method("GET", "/message", authenticatedHandler(getMessageList)) r.Method("GET", "/message", authenticatedHandler(getMessageList))
r.Method("GET", "/notification", auth.NewEnsureAuth(getNotificationList)) r.Method("GET", "/notification", authenticatedHandler(getNotificationList))
r.Method("GET", "/pool", auth.NewEnsureAuth(getPoolList)) r.Method("GET", "/pool", authenticatedHandler(getPoolList))
r.Method("GET", "/pool/upload", auth.NewEnsureAuth(getPoolUpload)) r.Method("GET", "/pool/create", authenticatedHandler(getPoolCreate))
r.Method("GET", "/pool/upload/{id}", auth.NewEnsureAuth(getPoolUploadByID)) r.Method("GET", "/pool/{id}", authenticatedHandler(getPoolByID))
r.Method("POST", "/pool/upload", auth.NewEnsureAuth(postPoolUpload))
r.Method("GET", "/radar", authenticatedHandler(getRadar)) r.Method("GET", "/radar", authenticatedHandler(getRadar))
r.Method("GET", "/service-request", authenticatedHandler(getServiceRequestList)) r.Method("GET", "/service-request", authenticatedHandler(getServiceRequestList))
r.Method("GET", "/service-request/{id}", authenticatedHandler(getServiceRequestDetail)) r.Method("GET", "/service-request/{id}", authenticatedHandler(getServiceRequestDetail))
r.Method("GET", "/setting", auth.NewEnsureAuth(getSetting)) r.Method("GET", "/setting", authenticatedHandler(getSetting))
r.Method("GET", "/setting/organization", auth.NewEnsureAuth(getSettingOrganization)) r.Method("GET", "/setting/organization", authenticatedHandler(getSettingOrganization))
r.Method("GET", "/setting/integration", auth.NewEnsureAuth(getSettingIntegration)) r.Method("GET", "/setting/integration", authenticatedHandler(getSettingIntegration))
r.Method("GET", "/setting/pesticide", authenticatedHandler(getSettingPesticide)) r.Method("GET", "/setting/pesticide", authenticatedHandler(getSettingPesticide))
r.Method("GET", "/setting/pesticide/add", authenticatedHandler(getSettingPesticideAdd)) r.Method("GET", "/setting/pesticide/add", authenticatedHandler(getSettingPesticideAdd))
r.Method("GET", "/setting/user", authenticatedHandler(getSettingUserList)) r.Method("GET", "/setting/user", authenticatedHandler(getSettingUserList))
r.Method("GET", "/setting/user/add", authenticatedHandler(getSettingUserAdd)) r.Method("GET", "/setting/user/add", authenticatedHandler(getSettingUserAdd))
r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout))
r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) r.Method("GET", "/source/{globalid}", authenticatedHandler(getSource))
r.Method("GET", "/stadia", auth.NewEnsureAuth(getStadia)) r.Method("GET", "/stadia", authenticatedHandler(getStadia))
r.Method("GET", "/sudo", authenticatedHandler(getSudo)) r.Method("GET", "/sudo", authenticatedHandler(getSudo))
r.Method("POST", "/sudo/email", authenticatedHandlerPost(postSudoEmail)) r.Method("POST", "/sudo/email", authenticatedHandlerPost(postSudoEmail))
r.Method("POST", "/sudo/sms", authenticatedHandlerPost(postSudoSMS)) r.Method("POST", "/sudo/sms", authenticatedHandlerPost(postSudoSMS))
r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap)) r.Method("GET", "/trap/{globalid}", authenticatedHandler(getTrap))
r.Method("GET", "/text/{destination}", auth.NewEnsureAuth(getTextMessages)) r.Method("GET", "/text/{destination}", authenticatedHandler(getTextMessages))
r.Method("GET", "/upload", authenticatedHandler(getUploadList)) r.Method("GET", "/upload", authenticatedHandler(getUploadList))
r.Method("GET", "/upload/pool", authenticatedHandler(getUploadPoolList))
r.Method("GET", "/upload/pool/create", authenticatedHandler(getUploadPoolCreate))
r.Method("POST", "/upload/pool/create", authenticatedHandlerPostMultipart(postUploadPoolCreate))
r.Method("GET", "/upload/pool/upload/{id}", authenticatedHandler(getUploadPoolByID))
html.AddStaticRoute(r, "/static") html.AddStaticRoute(r, "/static")
return r return r
@ -88,19 +92,36 @@ func (e *errorWithStatus) Error() string {
return e.Message return e.Message
} }
func newError(mesg_format string, args ...interface{}) *errorWithStatus { func newError(mesg_format string, args ...interface{}) *errorWithStatus {
return newErrorStatus(http.StatusInternalServerError, mesg_format, args...)
}
func newErrorStatus(status int, mesg_format string, args ...interface{}) *errorWithStatus {
w := fmt.Errorf(mesg_format, args...) w := fmt.Errorf(mesg_format, args...)
return &errorWithStatus{ return &errorWithStatus{
Message: w.Error(), Message: w.Error(),
Status: http.StatusInternalServerError, Status: status,
} }
} }
type handlerFunctionGet[T any] func(context.Context, *models.User) (string, T, *errorWithStatus) type response[T any] struct {
content T
template string
}
func newResponse[T any](template string, content T) *response[T] {
return &response[T]{
content: content,
template: template,
}
}
type handlerFunctionGet[T any] func(context.Context, *http.Request, *models.Organization, *models.User) (*response[T], *errorWithStatus)
type wrappedHandler func(http.ResponseWriter, *http.Request) type wrappedHandler func(http.ResponseWriter, *http.Request)
type contentAuthenticated[T any] struct { type contentAuthenticated[T any] struct {
C T C T
URL ContentURL Config contentConfig
User User Organization *models.Organization
URL ContentURL
User User
} }
// w http.ResponseWriter, r *http.Request, u *models.User) { // w http.ResponseWriter, r *http.Request, u *models.User) {
@ -112,22 +133,32 @@ func authenticatedHandler[T any](f handlerFunctionGet[T]) http.Handler {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
template, content, e := f(ctx, u) org, err := u.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, e := f(ctx, r, org, u)
//log.Info().Str("template", template).Err(e).Msg("handler done") //log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil { if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from sync pages") log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from sync pages")
http.Error(w, e.Error(), e.Status) http.Error(w, e.Error(), e.Status)
return return
} }
html.RenderOrError(w, template, contentAuthenticated[T]{ if org == nil {
C: content, http.Error(w, "nil org", http.StatusInternalServerError)
URL: newContentURL(), return
User: userContent, }
html.RenderOrError(w, resp.template, contentAuthenticated[T]{
C: resp.content,
Organization: org,
URL: newContentURL(),
User: userContent,
}) })
}) })
} }
type handlerFunctionPost[T any] func(context.Context, *models.User, T) (string, *errorWithStatus) type handlerFunctionPost[T any] func(context.Context, *http.Request, *models.User, T) (string, *errorWithStatus)
func authenticatedHandlerPost[T any](f handlerFunctionPost[T]) http.Handler { func authenticatedHandlerPost[T any](f handlerFunctionPost[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u *models.User) { return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u *models.User) {
@ -145,7 +176,31 @@ func authenticatedHandlerPost[T any](f handlerFunctionPost[T]) http.Handler {
return return
} }
ctx := r.Context() ctx := r.Context()
path, e := f(ctx, u, content) path, e := f(ctx, r, u, content)
if e != nil {
http.Error(w, e.Error(), e.Status)
return
}
http.Redirect(w, r, path, http.StatusFound)
})
}
func authenticatedHandlerPostMultipart[T any](f handlerFunctionPost[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u *models.User) {
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
if err != nil {
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
return
}
var content T
err = decoder.Decode(&content, r.PostForm)
if err != nil {
respondError(w, "Failed to decode form", err, http.StatusBadRequest)
return
}
ctx := r.Context()
path, e := f(ctx, r, u, content)
if e != nil { if e != nil {
http.Error(w, e.Error(), e.Status) http.Error(w, e.Error(), e.Status)
return return

View file

@ -2,6 +2,7 @@ package sync
import ( import (
"context" "context"
"net/http"
"time" "time"
"github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/config"
@ -31,11 +32,11 @@ type contentServiceRequestList struct {
ClosedRequests []contentClosedServiceRequest ClosedRequests []contentClosedServiceRequest
} }
func getServiceRequestDetail(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getServiceRequestDetail(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentServiceRequestDetail], *errorWithStatus) {
content := contentServiceRequestDetail{} content := contentServiceRequestDetail{}
return "sync/service-request-detail.html", content, nil return newResponse("sync/service-request-detail.html", content), nil
} }
func getServiceRequestList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getServiceRequestList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentServiceRequestList], *errorWithStatus) {
now := time.Now() now := time.Now()
content := contentServiceRequestList{ content := contentServiceRequestList{
ActiveRequests: []contentActiveServiceRequest{ ActiveRequests: []contentActiveServiceRequest{
@ -111,5 +112,5 @@ func getServiceRequestList(ctx context.Context, user *models.User) (string, inte
}, },
}, },
} }
return "sync/service-request-list.html", content, nil return newResponse("sync/service-request-list.html", content), nil
} }

View file

@ -7,42 +7,29 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/arcgis" "github.com/Gleipnir-Technology/nidus-sync/arcgis"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
//"github.com/rs/zerolog/log" //"github.com/rs/zerolog/log"
) )
type contentSettingOrganization struct { type contentSettingOrganization struct {
Organization *models.Organization Organization *models.Organization
URL ContentURL
User User
} }
type contentSettingIntegration struct { type contentSettingIntegration struct {
ArcGISOAuth *models.OauthToken ArcGISOAuth *models.OauthToken
URL ContentURL
User User
} }
func getSetting(w http.ResponseWriter, r *http.Request, u *models.User) { type contentAuthenticatedPlaceholder struct {
userContent, err := contentForUser(r.Context(), u)
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
data := ContentAuthenticatedPlaceholder{
URL: newContentURL(),
User: userContent,
}
html.RenderOrError(w, "sync/settings.html", data)
} }
func getSettingOrganization(w http.ResponseWriter, r *http.Request, u *models.User) {
ctx := r.Context() func getSetting(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentAuthenticatedPlaceholder], *errorWithStatus) {
userContent, err := contentForUser(ctx, u) data := contentAuthenticatedPlaceholder{}
if err != nil { return newResponse("sync/settings.html", data), nil
respondError(w, "Failed to get user content", err, http.StatusInternalServerError) }
return func getSettingOrganization(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentSettingOrganization], *errorWithStatus) {
}
org, err := u.Organization().One(ctx, db.PGInstance.BobDB) org, err := u.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
return nil, newError("get organization: %w", err)
}
/* /*
var district contentDistrict var district contentDistrict
district, err = bob.One[contentDistrict](ctx, db.PGInstance.BobDB, psql.Select( district, err = bob.One[contentDistrict](ctx, db.PGInstance.BobDB, psql.Select(
@ -76,46 +63,35 @@ func getSettingOrganization(w http.ResponseWriter, r *http.Request, u *models.Us
*/ */
data := contentSettingOrganization{ data := contentSettingOrganization{
Organization: org, Organization: org,
URL: newContentURL(),
User: userContent,
} }
html.RenderOrError(w, "sync/setting-organization.html", data) return newResponse("sync/setting-organization.html", data), nil
} }
func getSettingIntegration(w http.ResponseWriter, r *http.Request, u *models.User) { func getSettingIntegration(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentSettingIntegration], *errorWithStatus) {
ctx := r.Context()
userContent, err := contentForUser(ctx, u)
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
oauth, err := arcgis.GetOAuthForUser(ctx, u) oauth, err := arcgis.GetOAuthForUser(ctx, u)
if err != nil { if err != nil {
respondError(w, "Failed to get oauth", err, http.StatusInternalServerError) return nil, newError("Failed to get oauth: %w", err)
return
} }
data := contentSettingIntegration{ data := contentSettingIntegration{
ArcGISOAuth: oauth, ArcGISOAuth: oauth,
URL: newContentURL(),
User: userContent,
} }
html.RenderOrError(w, "sync/setting-integration.html", data) return newResponse("sync/setting-integration.html", data), nil
} }
type contentSettingPlaceholder struct{} type contentSettingPlaceholder struct{}
func getSettingPesticide(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getSettingPesticide(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentSettingPlaceholder], *errorWithStatus) {
content := contentSettingPlaceholder{} content := contentSettingPlaceholder{}
return "sync/setting-pesticide.html", content, nil return newResponse("sync/setting-pesticide.html", content), nil
} }
func getSettingPesticideAdd(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getSettingPesticideAdd(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentSettingPlaceholder], *errorWithStatus) {
content := contentSettingPlaceholder{} content := contentSettingPlaceholder{}
return "sync/setting-pesticide-add.html", content, nil return newResponse("sync/setting-pesticide-add.html", content), nil
} }
func getSettingUserAdd(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getSettingUserAdd(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentSettingPlaceholder], *errorWithStatus) {
content := contentSettingPlaceholder{} content := contentSettingPlaceholder{}
return "sync/setting-user-add.html", content, nil return newResponse("sync/setting-user-add.html", content), nil
} }
func getSettingUserList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getSettingUserList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentSettingPlaceholder], *errorWithStatus) {
content := contentSettingPlaceholder{} content := contentSettingPlaceholder{}
return "sync/setting-user-list.html", content, nil return newResponse("sync/setting-user-list.html", content), nil
} }

View file

@ -30,7 +30,8 @@ func getSignout(w http.ResponseWriter, r *http.Request, user *models.User) {
} }
func getSignup(w http.ResponseWriter, r *http.Request) { func getSignup(w http.ResponseWriter, r *http.Request) {
signup(w, r.URL.Path) data := ContentSignup{}
html.RenderOrError(w, "sync/signup.html", data)
} }
func postSignin(w http.ResponseWriter, r *http.Request) { func postSignin(w http.ResponseWriter, r *http.Request) {
@ -105,8 +106,3 @@ func signin(w http.ResponseWriter, errorCode string, next string) {
} }
html.RenderOrError(w, "sync/signin.html", data) html.RenderOrError(w, "sync/signin.html", data)
} }
func signup(w http.ResponseWriter, path string) {
data := ContentSignup{}
html.RenderOrError(w, "sync/signup.html", data)
}

View file

@ -19,9 +19,9 @@ type contentSudo struct {
ForwardEmailNidusAddress string ForwardEmailNidusAddress string
} }
func getSudo(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getSudo(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentSudo], *errorWithStatus) {
if user.Role != enums.UserroleRoot { if user.Role != enums.UserroleRoot {
return "", nil, &errorWithStatus{ return nil, &errorWithStatus{
Message: "You have to be a root user to access this", Message: "You have to be a root user to access this",
Status: http.StatusForbidden, Status: http.StatusForbidden,
} }
@ -30,7 +30,7 @@ func getSudo(ctx context.Context, user *models.User) (string, interface{}, *erro
ForwardEmailRMOAddress: config.ForwardEmailRMOAddress, ForwardEmailRMOAddress: config.ForwardEmailRMOAddress,
ForwardEmailNidusAddress: config.ForwardEmailNidusAddress, ForwardEmailNidusAddress: config.ForwardEmailNidusAddress,
} }
return "sync/sudo.html", content, nil return newResponse("sync/sudo.html", content), nil
} }
var decoder = schema.NewDecoder() var decoder = schema.NewDecoder()
@ -42,7 +42,7 @@ type FormEmail struct {
To string `schema:"emailTo"` To string `schema:"emailTo"`
} }
func postSudoEmail(ctx context.Context, u *models.User, e FormEmail) (string, *errorWithStatus) { func postSudoEmail(ctx context.Context, r *http.Request, u *models.User, e FormEmail) (string, *errorWithStatus) {
if u.Role != enums.UserroleRoot { if u.Role != enums.UserroleRoot {
return "", &errorWithStatus{ return "", &errorWithStatus{
Message: "You must have sudo powers to do this", Message: "You must have sudo powers to do this",
@ -71,7 +71,7 @@ type FormSMS struct {
Phone string `schema:"smsPhone"` Phone string `schema:"smsPhone"`
} }
func postSudoSMS(ctx context.Context, u *models.User, sms FormSMS) (string, *errorWithStatus) { func postSudoSMS(ctx context.Context, r *http.Request, u *models.User, sms FormSMS) (string, *errorWithStatus) {
if u.Role != enums.UserroleRoot { if u.Role != enums.UserroleRoot {
return "", &errorWithStatus{ return "", &errorWithStatus{
Message: "You must have sudo powers to do this", Message: "You must have sudo powers to do this",

View file

@ -1,24 +1,15 @@
package sync package sync
import ( import (
"context"
"net/http" "net/http"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
) )
type ContentTextMessages struct { type contentTextMessages struct{}
User User
}
func getTextMessages(w http.ResponseWriter, r *http.Request, u *models.User) { func getTextMessages(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentTextMessages], *errorWithStatus) {
userContent, err := contentForUser(r.Context(), u) content := contentTextMessages{}
if err != nil { return newResponse("sync/text-messages.html", content), nil
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
content := ContentTextMessages{
User: userContent,
}
html.RenderOrError(w, "sync/text-messages.html", content)
} }

View file

@ -25,10 +25,6 @@ type ComponentMap struct {
Markers []MapMarker Markers []MapMarker
Zoom int Zoom int
} }
type ContentAuthenticatedPlaceholder struct {
URL ContentURL
User User
}
type ContentMockURLs struct { type ContentMockURLs struct {
Dispatch string Dispatch string
DispatchResults string DispatchResults string

View file

@ -2,13 +2,88 @@ package sync
import ( import (
"context" "context"
"fmt"
"net/http"
"strconv"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/go-chi/chi/v5"
) )
type contentUploadPlaceholder struct{} type contentUploadPlaceholder struct{}
func getUploadList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { func getUploadList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentUploadPlaceholder], *errorWithStatus) {
content := contentUploadPlaceholder{} content := contentUploadPlaceholder{}
return "sync/upload-list.html", content, nil return newResponse("sync/upload-list.html", content), nil
}
type contentPoolDetail struct {
CSVFileID int32
Organization *models.Organization
Upload platform.UploadPoolDetail
}
type contentUploadPoolList struct {
Uploads []platform.PoolUpload
}
type contentUploadPoolCreate struct{}
func getUploadPoolList(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentUploadPoolList], *errorWithStatus) {
uploads, err := platform.PoolUploadList(ctx, u.OrganizationID)
if err != nil {
return nil, newError("Failed to get uploads: %w", err)
}
data := contentUploadPoolList{
Uploads: uploads,
}
return newResponse("sync/pool-list.html", data), nil
}
func getUploadPoolCreate(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentUploadPoolCreate], *errorWithStatus) {
data := contentUploadPoolCreate{}
return newResponse("sync/pool-csv-upload.html", data), nil
}
func getUploadPoolByID(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentPoolDetail], *errorWithStatus) {
org, err := u.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
return nil, newError("Failed to get organization: %w", err)
}
file_id_str := chi.URLParam(r, "id")
file_id, err := strconv.ParseInt(file_id_str, 10, 32)
if err != nil {
return nil, newError("Failed to parse file_id: %w", err)
}
detail, err := platform.GetUploadPoolDetail(ctx, u.OrganizationID, int32(file_id))
if err != nil {
return nil, newError("Failed to get pool: %w", err)
}
data := contentPoolDetail{
CSVFileID: int32(file_id),
Organization: org,
Upload: detail,
}
return newResponse("sync/pool-by-id.html", data), nil
}
type FormUploadPool struct{}
func postUploadPoolCreate(ctx context.Context, r *http.Request, u *models.User, f FormUploadPool) (string, *errorWithStatus) {
uploads, err := userfile.SaveFileUpload(r, "csvfile", userfile.CollectionCSV)
if err != nil {
return "", newError("Failed to extract image uploads: %s", err)
}
if len(uploads) == 0 {
return "", newErrorStatus(http.StatusBadRequest, "No upload found")
}
if len(uploads) != 1 {
return "", newErrorStatus(http.StatusBadRequest, "You must only submit one file at a time")
}
upload := uploads[0]
pool_upload, err := platform.NewPoolUpload(r.Context(), u, upload)
if err != nil {
return "", newError("Failed to create new pool: %w", err)
}
return fmt.Sprintf("/pool/upload/%d", pool_upload.ID), nil
} }