From dac52a879acb02252ab0342dd27ed60f946143f4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 24 Feb 2026 15:34:53 +0000 Subject: [PATCH] Move all sync pages to authenticatedHandler Still need to fix many templates --- html/filesystem.go | 2 +- html/template/sync/dashboard.html | 52 ++-- html/template/sync/layout/authenticated.html | 4 + html/template/sync/layout/base.html | 4 + main.go | 7 +- sync/admin.go | 5 +- sync/cell.go | 52 +--- sync/config.go | 15 ++ sync/dash.go | 270 ++++++++----------- sync/download.go | 7 +- sync/messages.go | 5 +- sync/notification.go | 19 +- sync/oauth.go | 13 +- sync/pool.go | 119 +------- sync/radar.go | 7 +- sync/routes.go | 107 ++++++-- sync/service-request.go | 9 +- sync/setting.go | 68 ++--- sync/signin.go | 8 +- sync/sudo.go | 10 +- sync/text.go | 19 +- sync/types.go | 4 - sync/upload.go | 79 +++++- 23 files changed, 409 insertions(+), 476 deletions(-) create mode 100644 sync/config.go diff --git a/html/filesystem.go b/html/filesystem.go index 12fc9263..bb186ded 100644 --- a/html/filesystem.go +++ b/html/filesystem.go @@ -51,7 +51,7 @@ func (ts templateSystemDisk) renderOrError(w http.ResponseWriter, template_name buf := &bytes.Buffer{} err = t.Execute(buf, context) 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) return } diff --git a/html/template/sync/dashboard.html b/html/template/sync/dashboard.html index bd68b413..55ae9432 100644 --- a/html/template/sync/dashboard.html +++ b/html/template/sync/dashboard.html @@ -29,7 +29,7 @@
- {{ if .IsSyncOngoing }} + {{ if .C.IsSyncOngoing }}

Syncing now...

@@ -37,7 +37,7 @@

Last updated: {{ .LastSync | timeRelativePtr }}{{ .C.LastSync | timeRelativePtr }}

Last Data Refresh
-

{{ .LastSync | timeRelativePtr }}

+

{{ .C.LastSync | timeRelativePtr }}

@@ -71,13 +71,13 @@
Service Requests
- {{ if .IsSyncOngoing }} + {{ if .C.IsSyncOngoing }}

- {{ .CountServiceRequests | bigNumber }}...? + {{ .C.CountServiceRequests | bigNumber }}...?

{{ else }}

- {{ .CountServiceRequests | bigNumber }} + {{ .C.CountServiceRequests | bigNumber }}

{{ end }} {{ block "extraheader" . }}{{ end }} + {{ if not .Config.IsProductionEnvironment }} + + {{ end }} {{ template "content" . }} + diff --git a/main.go b/main.go index bdf7f294..0c9a132b 100644 --- a/main.go +++ b/main.go @@ -75,7 +75,12 @@ func main() { defer sentryWriter.Close() 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) if err != nil { diff --git a/sync/admin.go b/sync/admin.go index f16cefe6..bd30a135 100644 --- a/sync/admin.go +++ b/sync/admin.go @@ -2,13 +2,14 @@ package sync import ( "context" + "net/http" "github.com/Gleipnir-Technology/nidus-sync/db/models" ) 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{} - return "sync/admin-dash.html", content, nil + return newResponse("sync/admin-dash.html", content), nil } diff --git a/sync/cell.go b/sync/cell.go index 43a9bb16..4fc96b5a 100644 --- a/sync/cell.go +++ b/sync/cell.go @@ -1,12 +1,11 @@ package sync import ( + "context" "net/http" - "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" - "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" "github.com/uber/h3-go/v4" ) @@ -18,70 +17,48 @@ type contentCell struct { MapData ComponentMap Traps []TrapSummary 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") if cell_str == "" { - respondError(w, "There should always be a cell", nil, http.StatusBadRequest) - return + return nil, newErrorStatus(http.StatusBadRequest, "There should always be a cell") } c, err := HexToInt64(cell_str) if err != nil { - respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest) - 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 + return nil, newErrorStatus(http.StatusBadRequest, "Cannot convert provided cell to uint64") } center, err := h3.Cell(c).LatLng() if err != nil { - respondError(w, "Failed to get center", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get center: %w", err) } boundary, err := h3.Cell(c).Boundary() if err != nil { - respondError(w, "Failed to get boundary", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get boundary: %w", err) } inspections, err := inspectionsByCell(ctx, org, h3.Cell(c)) if err != nil { - respondError(w, "Failed to get inspections by cell", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get inspections by cell: %w", err) } geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)}) if err != nil { - respondError(w, "Failed to get boundaries", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get boundaries: %w", err) } resolution := h3.Cell(c).Resolution() sources, err := breedingSourcesByCell(ctx, org, h3.Cell(c)) if err != nil { - respondError(w, "Failed to get sources", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get sources: %w", err) } traps, err := trapsByCell(ctx, org, h3.Cell(c)) if err != nil { - respondError(w, "Failed to get traps", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get traps: %w", err) } treatments, err := treatmentsByCell(ctx, org, h3.Cell(c)) if err != nil { - respondError(w, "Failed to get treatments", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get treatments: %w", err) } - data := contentCell{ + return newResponse("sync/cell.html", contentCell{ BreedingSources: sources, CellBoundary: boundary, Inspections: inspections, @@ -95,8 +72,5 @@ func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) { }, Traps: traps, Treatments: treatments, - URL: newContentURL(), - User: userContent, - } - html.RenderOrError(w, "sync/cell.html", &data) + }), nil } diff --git a/sync/config.go b/sync/config.go new file mode 100644 index 00000000..dc1e975c --- /dev/null +++ b/sync/config.go @@ -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(), + } +} diff --git a/sync/dash.go b/sync/dash.go index c26b49c5..2d52ab35 100644 --- a/sync/dash.go +++ b/sync/dash.go @@ -21,10 +21,7 @@ import ( // Authenticated pages var () -type Config struct { -} - -type ContentSource struct { +type contentSource struct { Inspections []Inspection MapData ComponentMap Source *BreedingSourceDetail @@ -34,12 +31,12 @@ type ContentSource struct { TreatmentModels []TreatmentModel User User } -type ContentTrap struct { +type contentTrap struct { MapData ComponentMap Trap Trap User User } -type ContentDashboard struct { +type contentDashboard struct { CountTraps int CountMosquitoSources int CountServiceRequests int @@ -47,13 +44,10 @@ type ContentDashboard struct { IsSyncOngoing bool LastSync *time.Time MapData ComponentMap - Organization *models.Organization RecentRequests []ServiceRequestSummary - URL ContentURL - User User } -type ContentLayoutTest struct { +type contentLayoutTest struct { User User } type ContentDistrict struct { @@ -67,16 +61,12 @@ func getDistrict(w http.ResponseWriter, r *http.Request) { html.RenderOrError(w, "sync/district.html", &context) } -func getLayoutTest(w http.ResponseWriter, r *http.Request, u *models.User) { - userContent, err := contentForUser(r.Context(), u) - if err != nil { - respondError(w, "Failed to get user", err, http.StatusInternalServerError) - return - } - html.RenderOrError(w, "sync/layout-test.html", &ContentLayoutTest{User: userContent}) +func getLayoutTest(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentLayoutTest], *errorWithStatus) { + return newResponse("sync/layout-test.html", contentLayoutTest{}), nil } func getRoot(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() user, err := auth.GetAuthenticatedUser(r) if err != nil { // No credentials or user not found: go to login @@ -93,77 +83,135 @@ func getRoot(w http.ResponseWriter, r *http.Request) { signin(w, errorCode, "/") return } else { - has, err := background.HasFieldseekerConnection(r.Context(), user) + has, err := background.HasFieldseekerConnection(ctx, user) if err != nil { respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError) return } 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 } else { oauthPrompt(w, r, user) 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") if globalid_s == "" { - respondError(w, "No globalid provided", nil, http.StatusBadRequest) - return + return nil, newError("No globalid provided: %w", nil) } globalid, err := uuid.Parse(globalid_s) if err != nil { - respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) - return + return nil, newError("globalid is not a UUID: %w", nil) } - 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) { - userContent, err := contentForUser(r.Context(), u) - if err != nil { - respondError(w, "Failed to get user content", err, http.StatusInternalServerError) - return - } - data := ContentDashboard{ +func getStadia(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentDashboard], *errorWithStatus) { + data := contentDashboard{ MapData: ComponentMap{ 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) { 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") if globalid_s == "" { - respondError(w, "No globalid provided", nil, http.StatusBadRequest) - return + return nil, newError("No globalid provided: %w", nil) } globalid, err := uuid.Parse(globalid_s) if err != nil { - respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) - return + return nil, newError("globalid is not a UUID: %w", nil) } - 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) { - org, err := user.Organization().One(ctx, db.PGInstance.BobDB) - if err != nil { - respondError(w, "Failed to get org", err, http.StatusInternalServerError) - return - } +func dashboard(ctx context.Context, w http.ResponseWriter, org *models.Organization, user *models.User) { var lastSync *time.Time sync, err := org.FieldseekerSyncs(sm.OrderBy("created").Desc()).One(ctx, db.PGInstance.BobDB) if err != nil { @@ -204,12 +252,7 @@ func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) { Status: "Completed", }) } - userContent, err := contentForUser(ctx, user) - if err != nil { - respondError(w, "Failed to get user context", err, http.StatusInternalServerError) - return - } - data := ContentDashboard{ + content := contentDashboard{ CountTraps: int(trapCount), CountMosquitoSources: int(sourceCount), CountServiceRequests: int(serviceCount), @@ -218,111 +261,22 @@ func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) { MapData: ComponentMap{ MapboxToken: config.MapboxToken, }, - Organization: org, RecentRequests: requests, - URL: newContentURL(), - User: userContent, } - html.RenderOrError(w, "sync/dashboard.html", data) -} - -func source(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUID) { - org, err := user.Organization().One(r.Context(), db.PGInstance.BobDB) + userContent, err := contentForUser(ctx, user) if err != nil { - respondError(w, "Failed to get org", err, http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusInternalServerError) return } - userContent, err := contentForUser(r.Context(), user) - if err != nil { - respondError(w, "Failed to get user content", err, http.StatusInternalServerError) - 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, + html.RenderOrError(w, "sync/dashboard.html", contentAuthenticated[contentDashboard]{ + C: content, + URL: newContentURL(), 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) { } diff --git a/sync/download.go b/sync/download.go index a4f05d80..4a151b4e 100644 --- a/sync/download.go +++ b/sync/download.go @@ -2,13 +2,14 @@ package sync import ( "context" + "net/http" "github.com/Gleipnir-Technology/nidus-sync/db/models" ) type contentDownloadPlaceholder struct{} -func getDownloadList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) { - content := contentSettingPlaceholder{} - return "sync/download-list.html", content, nil +func getDownloadList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentDownloadPlaceholder], *errorWithStatus) { + content := contentDownloadPlaceholder{} + return newResponse("sync/download-list.html", content), nil } diff --git a/sync/messages.go b/sync/messages.go index 7db90a2b..6dc06422 100644 --- a/sync/messages.go +++ b/sync/messages.go @@ -2,13 +2,14 @@ package sync import ( "context" + "net/http" "github.com/Gleipnir-Technology/nidus-sync/db/models" ) 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{} - return "sync/message-list.html", content, nil + return newResponse("sync/message-list.html", content), nil } diff --git a/sync/notification.go b/sync/notification.go index dda61a46..54fc808b 100644 --- a/sync/notification.go +++ b/sync/notification.go @@ -1,14 +1,13 @@ package sync import ( - //"context" + "context" //"fmt" "net/http" //"strings" //"time" "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/bob" //"github.com/Gleipnir-Technology/bob/dialect/psql" @@ -21,22 +20,14 @@ import ( type contentNotificationList struct { Notifications []notification.Notification - User User } -func getNotificationList(w http.ResponseWriter, r *http.Request, u *models.User) { - userContent, err := contentForUser(r.Context(), u) - if err != nil { - respondError(w, "Failed to get user", err, http.StatusInternalServerError) - return - } - ctx := r.Context() +func getNotificationList(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentNotificationList], *errorWithStatus) { notifications, err := notification.ForUser(ctx, u) 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, - User: userContent, - }) + }), nil } diff --git a/sync/oauth.go b/sync/oauth.go index de73b2bb..baad1f6c 100644 --- a/sync/oauth.go +++ b/sync/oauth.go @@ -13,9 +13,7 @@ import ( "github.com/rs/zerolog/log" ) -type ContextOauthPrompt struct { - User User -} +type ContextOauthPrompt struct{} // Build the ArcGIS authorization URL with PKCE 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) { - userContent, err := contentForUser(r.Context(), user) - if err != nil { - respondError(w, "Failed to get user content", err, http.StatusInternalServerError) - return - } - data := ContextOauthPrompt{ - User: userContent, - } + data := ContextOauthPrompt{} html.RenderOrError(w, "sync/oauth-prompt.html", data) } diff --git a/sync/pool.go b/sync/pool.go index 3827a60d..b5c8ec56 100644 --- a/sync/pool.go +++ b/sync/pool.go @@ -1,123 +1,20 @@ package sync import ( - "fmt" + "context" "net/http" - "strconv" - "github.com/Gleipnir-Technology/nidus-sync/db" "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 { - 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 -} +type contentPoolList struct{} -func getPoolList(w http.ResponseWriter, r *http.Request, u *models.User) { - ctx := r.Context() - 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 getPoolList(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentPoolList], *errorWithStatus) { + return newResponse("sync/pool-list.html", contentPoolList{}), nil } - -func getPoolUpload(w http.ResponseWriter, r *http.Request, u *models.User) { - 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 getPoolCreate(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentPoolList], *errorWithStatus) { + return newResponse("sync/pool-upload.html", contentPoolList{}), nil } -func getPoolUploadByID(w http.ResponseWriter, r *http.Request, u *models.User) { - ctx := r.Context() - 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) +func getPoolByID(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentPoolList], *errorWithStatus) { + return newResponse("sync/pool-by-id.html", contentPoolList{}), nil } diff --git a/sync/radar.go b/sync/radar.go index ee25d15c..c2d74ea2 100644 --- a/sync/radar.go +++ b/sync/radar.go @@ -2,6 +2,7 @@ package sync import ( "context" + "net/http" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" @@ -11,13 +12,13 @@ type contentRadar struct { 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) if err != nil { - return "", contentRadar{}, newError("get org: %w", err) + return nil, newError("get org: %w", err) } data := contentRadar{ Organization: org, } - return "sync/radar.html", data, nil + return newResponse("sync/radar.html", data), nil } diff --git a/sync/routes.go b/sync/routes.go index cfc6ed9b..1978fa43 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -7,6 +7,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/api" "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" "github.com/go-chi/chi/v5" @@ -46,34 +47,37 @@ func Router() chi.Router { // Authenticated endpoints r.Route("/api", api.AddRoutes) 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", "/layout-test", auth.NewEnsureAuth(getLayoutTest)) + r.Method("GET", "/layout-test", authenticatedHandler(getLayoutTest)) r.Method("GET", "/message", authenticatedHandler(getMessageList)) - r.Method("GET", "/notification", auth.NewEnsureAuth(getNotificationList)) - r.Method("GET", "/pool", auth.NewEnsureAuth(getPoolList)) - r.Method("GET", "/pool/upload", auth.NewEnsureAuth(getPoolUpload)) - r.Method("GET", "/pool/upload/{id}", auth.NewEnsureAuth(getPoolUploadByID)) - r.Method("POST", "/pool/upload", auth.NewEnsureAuth(postPoolUpload)) + r.Method("GET", "/notification", authenticatedHandler(getNotificationList)) + r.Method("GET", "/pool", authenticatedHandler(getPoolList)) + r.Method("GET", "/pool/create", authenticatedHandler(getPoolCreate)) + r.Method("GET", "/pool/{id}", authenticatedHandler(getPoolByID)) r.Method("GET", "/radar", authenticatedHandler(getRadar)) r.Method("GET", "/service-request", authenticatedHandler(getServiceRequestList)) r.Method("GET", "/service-request/{id}", authenticatedHandler(getServiceRequestDetail)) - r.Method("GET", "/setting", auth.NewEnsureAuth(getSetting)) - r.Method("GET", "/setting/organization", auth.NewEnsureAuth(getSettingOrganization)) - r.Method("GET", "/setting/integration", auth.NewEnsureAuth(getSettingIntegration)) + r.Method("GET", "/setting", authenticatedHandler(getSetting)) + r.Method("GET", "/setting/organization", authenticatedHandler(getSettingOrganization)) + r.Method("GET", "/setting/integration", authenticatedHandler(getSettingIntegration)) r.Method("GET", "/setting/pesticide", authenticatedHandler(getSettingPesticide)) r.Method("GET", "/setting/pesticide/add", authenticatedHandler(getSettingPesticideAdd)) r.Method("GET", "/setting/user", authenticatedHandler(getSettingUserList)) r.Method("GET", "/setting/user/add", authenticatedHandler(getSettingUserAdd)) r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) - r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) - r.Method("GET", "/stadia", auth.NewEnsureAuth(getStadia)) + r.Method("GET", "/source/{globalid}", authenticatedHandler(getSource)) + r.Method("GET", "/stadia", authenticatedHandler(getStadia)) r.Method("GET", "/sudo", authenticatedHandler(getSudo)) r.Method("POST", "/sudo/email", authenticatedHandlerPost(postSudoEmail)) r.Method("POST", "/sudo/sms", authenticatedHandlerPost(postSudoSMS)) - r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap)) - r.Method("GET", "/text/{destination}", auth.NewEnsureAuth(getTextMessages)) + r.Method("GET", "/trap/{globalid}", authenticatedHandler(getTrap)) + r.Method("GET", "/text/{destination}", authenticatedHandler(getTextMessages)) 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") return r @@ -88,19 +92,36 @@ func (e *errorWithStatus) Error() string { return e.Message } 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...) return &errorWithStatus{ 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 contentAuthenticated[T any] struct { - C T - URL ContentURL - User User + C T + Config contentConfig + Organization *models.Organization + URL ContentURL + User 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) 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") if e != nil { 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) return } - html.RenderOrError(w, template, contentAuthenticated[T]{ - C: content, - URL: newContentURL(), - User: userContent, + if org == nil { + http.Error(w, "nil org", http.StatusInternalServerError) + return + } + 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 { 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 } 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 { http.Error(w, e.Error(), e.Status) return diff --git a/sync/service-request.go b/sync/service-request.go index 57a6ae3a..48b56944 100644 --- a/sync/service-request.go +++ b/sync/service-request.go @@ -2,6 +2,7 @@ package sync import ( "context" + "net/http" "time" "github.com/Gleipnir-Technology/nidus-sync/config" @@ -31,11 +32,11 @@ type contentServiceRequestList struct { 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{} - 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() content := contentServiceRequestList{ 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 } diff --git a/sync/setting.go b/sync/setting.go index 7d8e1727..cab6cf66 100644 --- a/sync/setting.go +++ b/sync/setting.go @@ -7,42 +7,29 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/arcgis" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/html" //"github.com/rs/zerolog/log" ) type contentSettingOrganization struct { Organization *models.Organization - URL ContentURL - User User } type contentSettingIntegration struct { ArcGISOAuth *models.OauthToken - URL ContentURL - User User } -func getSetting(w http.ResponseWriter, r *http.Request, u *models.User) { - 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) +type contentAuthenticatedPlaceholder struct { } -func getSettingOrganization(w http.ResponseWriter, r *http.Request, u *models.User) { - ctx := r.Context() - userContent, err := contentForUser(ctx, u) - if err != nil { - respondError(w, "Failed to get user content", err, http.StatusInternalServerError) - return - } + +func getSetting(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentAuthenticatedPlaceholder], *errorWithStatus) { + data := contentAuthenticatedPlaceholder{} + return newResponse("sync/settings.html", data), nil +} +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) + if err != nil { + return nil, newError("get organization: %w", err) + } /* var district contentDistrict 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{ 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) { - ctx := r.Context() - userContent, err := contentForUser(ctx, u) - if err != nil { - respondError(w, "Failed to get user content", err, http.StatusInternalServerError) - return - } +func getSettingIntegration(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentSettingIntegration], *errorWithStatus) { oauth, err := arcgis.GetOAuthForUser(ctx, u) if err != nil { - respondError(w, "Failed to get oauth", err, http.StatusInternalServerError) - return + return nil, newError("Failed to get oauth: %w", err) } data := contentSettingIntegration{ 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{} -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{} - 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{} - 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{} - 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{} - return "sync/setting-user-list.html", content, nil + return newResponse("sync/setting-user-list.html", content), nil } diff --git a/sync/signin.go b/sync/signin.go index ccdb3a46..4b013af9 100644 --- a/sync/signin.go +++ b/sync/signin.go @@ -30,7 +30,8 @@ func getSignout(w http.ResponseWriter, r *http.Request, user *models.User) { } 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) { @@ -105,8 +106,3 @@ func signin(w http.ResponseWriter, errorCode string, next string) { } html.RenderOrError(w, "sync/signin.html", data) } - -func signup(w http.ResponseWriter, path string) { - data := ContentSignup{} - html.RenderOrError(w, "sync/signup.html", data) -} diff --git a/sync/sudo.go b/sync/sudo.go index 7d61ccd8..a7e0c4da 100644 --- a/sync/sudo.go +++ b/sync/sudo.go @@ -19,9 +19,9 @@ type contentSudo struct { 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 { - return "", nil, &errorWithStatus{ + return nil, &errorWithStatus{ Message: "You have to be a root user to access this", Status: http.StatusForbidden, } @@ -30,7 +30,7 @@ func getSudo(ctx context.Context, user *models.User) (string, interface{}, *erro ForwardEmailRMOAddress: config.ForwardEmailRMOAddress, ForwardEmailNidusAddress: config.ForwardEmailNidusAddress, } - return "sync/sudo.html", content, nil + return newResponse("sync/sudo.html", content), nil } var decoder = schema.NewDecoder() @@ -42,7 +42,7 @@ type FormEmail struct { 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 { return "", &errorWithStatus{ Message: "You must have sudo powers to do this", @@ -71,7 +71,7 @@ type FormSMS struct { 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 { return "", &errorWithStatus{ Message: "You must have sudo powers to do this", diff --git a/sync/text.go b/sync/text.go index 49495597..6ddc774d 100644 --- a/sync/text.go +++ b/sync/text.go @@ -1,24 +1,15 @@ package sync import ( + "context" "net/http" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/html" ) -type ContentTextMessages struct { - User User -} +type contentTextMessages struct{} -func getTextMessages(w http.ResponseWriter, r *http.Request, u *models.User) { - userContent, err := contentForUser(r.Context(), u) - if err != nil { - respondError(w, "Failed to get user", err, http.StatusInternalServerError) - return - } - content := ContentTextMessages{ - User: userContent, - } - html.RenderOrError(w, "sync/text-messages.html", content) +func getTextMessages(ctx context.Context, r *http.Request, org *models.Organization, u *models.User) (*response[contentTextMessages], *errorWithStatus) { + content := contentTextMessages{} + return newResponse("sync/text-messages.html", content), nil } diff --git a/sync/types.go b/sync/types.go index 9ee2344d..789da5c8 100644 --- a/sync/types.go +++ b/sync/types.go @@ -25,10 +25,6 @@ type ComponentMap struct { Markers []MapMarker Zoom int } -type ContentAuthenticatedPlaceholder struct { - URL ContentURL - User User -} type ContentMockURLs struct { Dispatch string DispatchResults string diff --git a/sync/upload.go b/sync/upload.go index 23c072ab..8d2829bd 100644 --- a/sync/upload.go +++ b/sync/upload.go @@ -2,13 +2,88 @@ package sync import ( "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/platform" + "github.com/Gleipnir-Technology/nidus-sync/userfile" + "github.com/go-chi/chi/v5" ) 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{} - 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 }