diff --git a/sync/dash.go b/sync/dash.go new file mode 100644 index 00000000..7add4752 --- /dev/null +++ b/sync/dash.go @@ -0,0 +1,314 @@ +package sync + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/auth" + "github.com/Gleipnir-Technology/nidus-sync/background" + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/h3utils" + "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stephenafamo/bob/dialect/psql/sm" + "github.com/uber/h3-go/v4" +) + +// Authenticated pages +var ( + cellT = buildTemplate("cell", "authenticated") + dashboardT = buildTemplate("dashboard", "authenticated") + districtT = buildTemplate("district", "base") + settingsT = buildTemplate("settings", "authenticated") + sourceT = buildTemplate("source", "authenticated") +) + +type ContextDistrict struct { + MapboxToken string +} + +func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) { + cell_str := chi.URLParam(r, "cell") + if cell_str == "" { + respondError(w, "There should always be a cell", nil, http.StatusBadRequest) + return + } + c, err := HexToInt64(cell_str) + if err != nil { + respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest) + return + } + cell(r.Context(), w, user, c) +} + +func getDistrict(w http.ResponseWriter, r *http.Request) { + context := ContextDistrict{ + MapboxToken: config.MapboxToken, + } + htmlpage.RenderOrError(w, districtT, &context) +} + +func getFavicon(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-type", "image/x-icon") + + http.ServeFile(w, r, "static/favicon.ico") +} + +func getRoot(w http.ResponseWriter, r *http.Request) { + user, err := auth.GetAuthenticatedUser(r) + if err != nil { + // No credentials or user not found: go to login + if errors.Is(err, &auth.NoCredentialsError{}) || errors.Is(err, &auth.NoUserError{}) { + http.Redirect(w, r, "/signin", http.StatusFound) + return + } else { + respondError(w, "Failed to get root", err, http.StatusInternalServerError) + return + } + } + if user == nil { + errorCode := r.URL.Query().Get("error") + signin(w, errorCode) + return + } else { + has, err := background.HasFieldseekerConnection(r.Context(), user) + if err != nil { + respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError) + return + } + if has { + dashboard(r.Context(), w, user) + return + } else { + oauthPrompt(w, user) + return + } + } + if err != nil { + respondError(w, "Failed to render root", err, http.StatusInternalServerError) + } +} + +func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) { + settings(w, r, u) +} + +func getSource(w http.ResponseWriter, r *http.Request, u *models.User) { + globalid_s := chi.URLParam(r, "globalid") + if globalid_s == "" { + respondError(w, "No globalid provided", nil, http.StatusBadRequest) + return + } + globalid, err := uuid.Parse(globalid_s) + if err != nil { + respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) + return + } + source(w, r, u, globalid) +} + +func cell(ctx context.Context, w http.ResponseWriter, user *models.User, c int64) { + 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() + if err != nil { + respondError(w, "Failed to get center", err, http.StatusInternalServerError) + return + } + boundary, err := h3.Cell(c).Boundary() + if err != nil { + respondError(w, "Failed to get boundary", err, http.StatusInternalServerError) + return + } + inspections, err := inspectionsByCell(ctx, org, h3.Cell(c)) + if err != nil { + respondError(w, "Failed to get inspections by cell", err, http.StatusInternalServerError) + return + } + geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)}) + if err != nil { + respondError(w, "Failed to get boundaries", err, http.StatusInternalServerError) + return + } + 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 + } + treatments, err := treatmentsByCell(ctx, org, h3.Cell(c)) + if err != nil { + respondError(w, "Failed to get treatments", err, http.StatusInternalServerError) + return + } + data := ContentCell{ + BreedingSources: sources, + CellBoundary: boundary, + Inspections: inspections, + MapData: ComponentMap{ + Center: h3.LatLng{ + Lat: center.Lat, + Lng: center.Lng, + }, + GeoJSON: geojson, + MapboxToken: config.MapboxToken, + Zoom: resolution + 5, + }, + Treatments: treatments, + User: userContent, + } + htmlpage.RenderOrError(w, cellT, &data) +} + +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 + } + var lastSync *time.Time + sync, err := org.FieldseekerSyncs(sm.OrderBy("created").Desc()).One(ctx, db.PGInstance.BobDB) + if err != nil { + if err.Error() != "sql: no rows in result set" { + respondError(w, "Failed to get syncs", err, http.StatusInternalServerError) + return + } + } else { + lastSync = &sync.Created + } + is_syncing := background.IsSyncOngoing(org.ID) + inspectionCount, err := org.Mosquitoinspections().Count(ctx, db.PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to get inspection count", err, http.StatusInternalServerError) + return + } + sourceCount, err := org.Pointlocations().Count(ctx, db.PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to get source count", err, http.StatusInternalServerError) + return + } + serviceCount, err := org.Servicerequests().Count(ctx, db.PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to get service count", err, http.StatusInternalServerError) + return + } + recentRequests, err := org.Servicerequests(sm.OrderBy("creationdate").Desc(), sm.Limit(10)).All(ctx, db.PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to get recent service", err, http.StatusInternalServerError) + return + } + + requests := make([]ServiceRequestSummary, 0) + for _, r := range recentRequests { + requests = append(requests, ServiceRequestSummary{ + Date: r.Creationdate.MustGet(), + Location: r.Reqaddr1.MustGet(), + Status: "Completed", + }) + } + userContent, err := contentForUser(ctx, user) + if err != nil { + respondError(w, "Failed to get user context", err, http.StatusInternalServerError) + return + } + data := ContentDashboard{ + CountInspections: int(inspectionCount), + CountMosquitoSources: int(sourceCount), + CountServiceRequests: int(serviceCount), + IsSyncOngoing: is_syncing, + LastSync: lastSync, + MapData: ComponentMap{ + MapboxToken: config.MapboxToken, + }, + Org: org.Name.MustGet(), + RecentRequests: requests, + User: userContent, + } + htmlpage.RenderOrError(w, dashboardT, data) +} + +func settings(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 := ContentAuthenticatedPlaceholder{ + User: userContent, + } + htmlpage.RenderOrError(w, settingsT, 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) + 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 + } + 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, + } + + htmlpage.RenderOrError(w, sourceT, data) +} diff --git a/sync/endpoint.go b/sync/endpoint.go deleted file mode 100644 index 4e289f92..00000000 --- a/sync/endpoint.go +++ /dev/null @@ -1,392 +0,0 @@ -package sync - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "strings" - - "github.com/Gleipnir-Technology/nidus-sync/api" - "github.com/Gleipnir-Technology/nidus-sync/auth" - "github.com/Gleipnir-Technology/nidus-sync/background" - "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/zerolog/log" - "github.com/skip2/go-qrcode" -) - -func Router() chi.Router { - r := chi.NewRouter() - // Root is a special endpoint that is neither authenticated nor unauthenticated - r.Get("/", getRoot) - - // Unauthenticated endpoints - r.Get("/arcgis/oauth/begin", getArcgisOauthBegin) - r.Get("/arcgis/oauth/callback", getArcgisOauthCallback) - r.Get("/favicon.ico", getFavicon) - - r.Get("/mock", renderMock("mock-root")) - r.Get("/mock/admin", renderMock("admin")) - r.Get("/mock/admin/service-request", renderMock("admin-service-request")) - r.Get("/mock/data-entry", renderMock("data-entry")) - r.Get("/mock/data-entry/bad", renderMock("data-entry-bad")) - r.Get("/mock/data-entry/good", renderMock("data-entry-good")) - r.Get("/mock/dispatch", renderMock("dispatch")) - r.Get("/mock/dispatch-results", renderMock("dispatch-results")) - r.Get("/mock/report", renderMock("report")) - r.Get("/mock/report/{code}", renderMock("report-detail")) - r.Get("/mock/report/{code}/confirm", renderMock("report-confirmation")) - r.Get("/mock/report/{code}/contribute", renderMock("report-contribute")) - r.Get("/mock/report/{code}/evidence", renderMock("report-evidence")) - r.Get("/mock/report/{code}/schedule", renderMock("report-schedule")) - r.Get("/mock/report/{code}/update", renderMock("report-update")) - r.Get("/mock/service-request", renderMock("service-request")) - r.Get("/mock/service-request/{code}", renderMock("service-request-detail")) - r.Get("/mock/service-request-location", renderMock("service-request-location")) - r.Get("/mock/service-request-mosquito", renderMock("service-request-mosquito")) - r.Get("/mock/service-request-pool", renderMock("service-request-pool")) - r.Get("/mock/service-request-quick", renderMock("service-request-quick")) - r.Get("/mock/service-request-quick-confirmation", renderMock("service-request-quick-confirmation")) - r.Get("/mock/service-request-updates", renderMock("service-request-updates")) - r.Get("/mock/setting", renderMock("setting-mock")) - r.Get("/mock/setting/integration", renderMock("setting-integration")) - r.Get("/mock/setting/pesticide", renderMock("setting-pesticide")) - r.Get("/mock/setting/pesticide/add", renderMock("setting-pesticide-add")) - r.Get("/mock/setting/user", renderMock("setting-user")) - r.Get("/mock/setting/user/add", renderMock("setting-user-add")) - - r.Get("/oauth/refresh", getOAuthRefresh) - - r.Get("/qr-code/report/{code}", getQRCodeReport) - r.Get("/signin", getSignin) - r.Post("/signin", postSignin) - r.Get("/signup", getSignup) - r.Post("/signup", postSignup) - r.Get("/sms", getSMS) - r.Post("/sms", postSMS) - r.Get("/sms.php", getSMS) - r.Get("/sms/{org}", getSMS) - r.Post("/sms/{org}", postSMS) - - // Authenticated endpoints - r.Route("/api", api.AddRoutes) - r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) - r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings)) - r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) - //r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", auth.NewEnsureAuth(getVectorTiles)) - - localFS := http.Dir("./sync/static") - htmlpage.FileServer(r, "/static", localFS, EmbeddedStaticFS, "static") - return r -} - -func getArcgisOauthBegin(w http.ResponseWriter, r *http.Request) { - authURL := config.BuildArcGISAuthURL(config.ClientID) - http.Redirect(w, r, authURL, http.StatusFound) -} - -func getArcgisOauthCallback(w http.ResponseWriter, r *http.Request) { - code := r.URL.Query().Get("code") - log.Info().Str("code", code).Msg("Handling oauth callback") - if code == "" { - respondError(w, "Access code is empty", nil, http.StatusBadRequest) - return - } - user, err := auth.GetAuthenticatedUser(r) - if err != nil { - respondError(w, "You're not currently authenticated, which really shouldn't happen.", err, http.StatusUnauthorized) - return - } - err = background.HandleOauthAccessCode(r.Context(), user, code) - if err != nil { - respondError(w, "Failed to handle access code", err, http.StatusInternalServerError) - return - } - http.Redirect(w, r, config.MakeURLSync("/"), http.StatusFound) -} - -func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) { - cell_str := chi.URLParam(r, "cell") - if cell_str == "" { - respondError(w, "There should always be a cell", nil, http.StatusBadRequest) - return - } - cell, err := HexToInt64(cell_str) - if err != nil { - respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest) - return - } - Cell(r.Context(), w, user, cell) -} - -func getFavicon(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-type", "image/x-icon") - - http.ServeFile(w, r, "static/favicon.ico") -} - -func getOAuthRefresh(w http.ResponseWriter, r *http.Request) { - user, err := auth.GetAuthenticatedUser(r) - if err != nil { - http.Redirect(w, r, "/?next=/oauth/refresh", http.StatusFound) - return - } - OauthPrompt(w, user) -} - -func getQRCodeReport(w http.ResponseWriter, r *http.Request) { - code := chi.URLParam(r, "code") - if code == "" { - respondError(w, "There should always be a code", nil, http.StatusBadRequest) - } - content := config.MakeURLSync("/report/" + code) - // Get optional size parameter (default to 256) - size := 256 - if sizeStr := r.URL.Query().Get("size"); sizeStr != "" { - var err error - size, err = strconv.Atoi(sizeStr) - if err != nil { - http.Error(w, "Invalid 'size' parameter, must be an integer", http.StatusBadRequest) - return - } - } - - // Get optional error correction level (default to Medium) - level := qrcode.Medium - if levelStr := r.URL.Query().Get("level"); levelStr != "" { - switch levelStr { - case "L", "l": - level = qrcode.Low - case "M", "m": - level = qrcode.Medium - case "Q", "q": - level = qrcode.High - case "H", "h": - level = qrcode.Highest - default: - respondError(w, "Invalid 'level' parameter, must be L, M, Q, or H", nil, http.StatusBadRequest) - return - } - } - - // Generate the QR code - var qr *qrcode.QRCode - var err error - qr, err = qrcode.New(content, level) - if err != nil { - respondError(w, "Error generating QR code", err, http.StatusInternalServerError) - return - } - - // Set the appropriate content type - w.Header().Set("Content-Type", "image/png") - - // Generate PNG and write directly to the response writer - png, err := qr.PNG(size) - if err != nil { - respondError(w, "Error encoding QR code to PNG", err, http.StatusInternalServerError) - return - } - - _, err = w.Write(png) - if err != nil { - respondError(w, "Error writing response", err, http.StatusInternalServerError) - } -} - -func getRoot(w http.ResponseWriter, r *http.Request) { - user, err := auth.GetAuthenticatedUser(r) - if err != nil { - // No credentials or user not found: go to login - if errors.Is(err, &auth.NoCredentialsError{}) || errors.Is(err, &auth.NoUserError{}) { - http.Redirect(w, r, "/signin", http.StatusFound) - return - } else { - respondError(w, "Failed to get root", err, http.StatusInternalServerError) - return - } - } - if user == nil { - errorCode := r.URL.Query().Get("error") - Signin(w, errorCode) - return - } else { - has, err := background.HasFieldseekerConnection(r.Context(), user) - if err != nil { - respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError) - return - } - if has { - Dashboard(r.Context(), w, user) - return - } else { - OauthPrompt(w, user) - return - } - } - if err != nil { - respondError(w, "Failed to render root", err, http.StatusInternalServerError) - } -} - -func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) { - Settings(w, r, u) -} - -func getSignin(w http.ResponseWriter, r *http.Request) { - errorCode := r.URL.Query().Get("error") - Signin(w, errorCode) -} - -func getSignup(w http.ResponseWriter, r *http.Request) { - Signup(w, r.URL.Path) -} - -func getSource(w http.ResponseWriter, r *http.Request, u *models.User) { - globalid_s := chi.URLParam(r, "globalid") - if globalid_s == "" { - respondError(w, "No globalid provided", nil, http.StatusBadRequest) - return - } - globalid, err := uuid.Parse(globalid_s) - if err != nil { - respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) - return - } - Source(w, r, u, globalid) -} - -func postSMS(w http.ResponseWriter, r *http.Request) { - // Log all request headers - for name, values := range r.Header { - for _, value := range values { - log.Info().Str("name", name).Str("value", value).Msg("header") - } - } - - // Read the request body - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - //return nil, fmt.Errorf("failed to read request body: %w", err) - respondError(w, "Failed to read request body", err, http.StatusInternalServerError) - return - } - log.Info().Str("body", string(bodyBytes)).Msg("body") - // Close the original body - defer r.Body.Close() - - // Parse JSON into webhook struct - var body SMSWebhookBody - if err := json.Unmarshal(bodyBytes, &body); err != nil { - respondError(w, "Failed to parse JSON", err, http.StatusBadRequest) - return - } - - if err := handleSMSMessage(&body.Data); err != nil { - log.Error().Err(err).Msg("Failed to handle SMS Message") - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) -} -func getSMS(w http.ResponseWriter, r *http.Request) { - org := chi.URLParam(r, "org") - - to := r.URL.Query().Get("error") - from := r.URL.Query().Get("error") - message := r.URL.Query().Get("error") - files := r.URL.Query().Get("error") - id := r.URL.Query().Get("error") - date := r.URL.Query().Get("error") - - log.Info().Str("org", org).Str("to", to).Str("from", from).Str("message", message).Str("files", files).Str("id", id).Str("date", date).Msg("Got SMS Message") - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-type", "text/plain") - // Signifies to Voip.ms that the callback worked. - fmt.Fprintf(w, "ok") -} -func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) { - org_id := chi.URLParam(r, "org_id") - tileset_id := chi.URLParam(r, "tileset_id") - zoom := chi.URLParam(r, "zoom") - x := chi.URLParam(r, "x") - y := chi.URLParam(r, "y") - format := chi.URLParam(r, "format") - - log.Info().Str("org_id", org_id).Str("tileset_id", tileset_id).Str("zoom", zoom).Str("x", x).Str("y", y).Str("format", format).Msg("Get vector tiles") - -} - -func postSignin(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - respondError(w, "Could not parse form", err, http.StatusBadRequest) - return - } - - username := r.FormValue("username") - password := r.FormValue("password") - - log.Info().Str("username", username).Msg("Signin") - - _, err := auth.SigninUser(r, username, password) - if err != nil { - if errors.Is(err, auth.InvalidCredentials{}) { - http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound) - return - } - if errors.Is(err, auth.InvalidUsername{}) { - http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound) - return - } - respondError(w, "Failed to signin user", err, http.StatusInternalServerError) - return - } - - http.Redirect(w, r, "/", http.StatusFound) -} - -func postSignup(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - respondError(w, "Could not parse form", err, http.StatusBadRequest) - return - } - - username := r.FormValue("username") - name := r.FormValue("name") - password := r.FormValue("password") - terms := r.FormValue("terms") - - log.Info().Str("username", username).Str("name", name).Str("password", strings.Repeat("*", len(password))).Msg("Signup") - - if terms != "on" { - log.Warn().Msg("Terms not agreed") - http.Error(w, "You must agree to the terms to register", http.StatusBadRequest) - return - } - - user, err := auth.SignupUser(r.Context(), username, name, password) - if err != nil { - respondError(w, "Failed to signup user", err, http.StatusInternalServerError) - return - } - - auth.AddUserSession(r, user) - - http.Redirect(w, r, "/", http.StatusFound) -} - -func renderMock(templateName string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - code := chi.URLParam(r, "code") - if code == "" { - code = "abc-123" - } - Mock(templateName, w, code) - } -} diff --git a/sync/mock.go b/sync/mock.go new file mode 100644 index 00000000..0425e17d --- /dev/null +++ b/sync/mock.go @@ -0,0 +1,145 @@ +package sync + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" + "github.com/skip2/go-qrcode" +) + +// Unauthenticated pages +var ( + admin = buildTemplate("admin", "base") + dataEntry = buildTemplate("data-entry", "base") + dataEntryGood = buildTemplate("data-entry-good", "base") + dataEntryBad = buildTemplate("data-entry-bad", "base") + dispatch = buildTemplate("dispatch", "base") + dispatchResults = buildTemplate("dispatch-results", "base") + mockRoot = buildTemplate("mock-root", "base") + reportPage = buildTemplate("report", "base") + reportConfirmation = buildTemplate("report-confirmation", "base") + reportContribute = buildTemplate("report-contribute", "base") + reportDetail = buildTemplate("report-detail", "base") + reportEvidence = buildTemplate("report-evidence", "base") + reportSchedule = buildTemplate("report-schedule", "base") + reportUpdate = buildTemplate("report-update", "base") + serviceRequest = buildTemplate("service-request", "base") + serviceRequestDetail = buildTemplate("service-request-detail", "base") + serviceRequestLocation = buildTemplate("service-request-location", "base") + serviceRequestMosquito = buildTemplate("service-request-mosquito", "base") + serviceRequestPool = buildTemplate("service-request-pool", "base") + serviceRequestQuick = buildTemplate("service-request-quick", "base") + serviceRequestQuickConfirmation = buildTemplate("service-request-quick-confirmation", "base") + serviceRequestUpdates = buildTemplate("service-request-updates", "base") + settingRoot = buildTemplate("setting-mock", "base") + settingIntegration = buildTemplate("setting-integration", "base") + settingPesticide = buildTemplate("setting-pesticide", "base") + settingPesticideAdd = buildTemplate("setting-pesticide-add", "base") + settingUsers = buildTemplate("setting-user", "base") + settingUsersAdd = buildTemplate("setting-user-add", "base") +) + +func getQRCodeReport(w http.ResponseWriter, r *http.Request) { + code := chi.URLParam(r, "code") + if code == "" { + respondError(w, "There should always be a code", nil, http.StatusBadRequest) + } + content := config.MakeURLSync("/report/" + code) + // Get optional size parameter (default to 256) + size := 256 + if sizeStr := r.URL.Query().Get("size"); sizeStr != "" { + var err error + size, err = strconv.Atoi(sizeStr) + if err != nil { + http.Error(w, "Invalid 'size' parameter, must be an integer", http.StatusBadRequest) + return + } + } + + // Get optional error correction level (default to Medium) + level := qrcode.Medium + if levelStr := r.URL.Query().Get("level"); levelStr != "" { + switch levelStr { + case "L", "l": + level = qrcode.Low + case "M", "m": + level = qrcode.Medium + case "Q", "q": + level = qrcode.High + case "H", "h": + level = qrcode.Highest + default: + respondError(w, "Invalid 'level' parameter, must be L, M, Q, or H", nil, http.StatusBadRequest) + return + } + } + + // Generate the QR code + var qr *qrcode.QRCode + var err error + qr, err = qrcode.New(content, level) + if err != nil { + respondError(w, "Error generating QR code", err, http.StatusInternalServerError) + return + } + + // Set the appropriate content type + w.Header().Set("Content-Type", "image/png") + + // Generate PNG and write directly to the response writer + png, err := qr.PNG(size) + if err != nil { + respondError(w, "Error encoding QR code to PNG", err, http.StatusInternalServerError) + return + } + + _, err = w.Write(png) + if err != nil { + respondError(w, "Error writing response", err, http.StatusInternalServerError) + } +} + +func mock(t string, w http.ResponseWriter, code string) { + data := ContentMock{ + DistrictName: "Delta MVCD", + URLs: ContentMockURLs{ + Dispatch: "/mock/dispatch", + DispatchResults: "/mock/dispatch-results", + ReportConfirmation: fmt.Sprintf("/mock/report/%s/confirm", code), + ReportDetail: fmt.Sprintf("/mock/report/%s", code), + ReportContribute: fmt.Sprintf("/mock/report/%s/contribute", code), + ReportEvidence: fmt.Sprintf("/mock/report/%s/evidence", code), + ReportSchedule: fmt.Sprintf("/mock/report/%s/schedule", code), + ReportUpdate: fmt.Sprintf("/mock/report/%s/update", code), + Root: "/mock", + Setting: "/mock/setting", + SettingIntegration: "/mock/setting/integration", + SettingPesticide: "/mock/setting/pesticide", + SettingPesticideAdd: "/mock/setting/pesticide/add", + SettingUser: "/mock/setting/user", + SettingUserAdd: "/mock/setting/user/add", + }, + } + template, ok := htmlpage.TemplatesByFilename[t+".html"] + if !ok { + log.Error().Str("template", t).Msg("Failed to find template") + respondError(w, "Failed to render template", nil, http.StatusInternalServerError) + return + } + htmlpage.RenderOrError(w, &template, data) +} + +func renderMock(templateName string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + code := chi.URLParam(r, "code") + if code == "" { + code = "abc-123" + } + mock(templateName, w, code) + } +} diff --git a/sync/oauth.go b/sync/oauth.go new file mode 100644 index 00000000..d581b9e1 --- /dev/null +++ b/sync/oauth.go @@ -0,0 +1,62 @@ +package sync + +import ( + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/auth" + "github.com/Gleipnir-Technology/nidus-sync/background" + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/rs/zerolog/log" +) + +var ( + oauthPromptT = buildTemplate("oauth-prompt", "authenticated") +) + +func getArcgisOauthBegin(w http.ResponseWriter, r *http.Request) { + authURL := config.BuildArcGISAuthURL(config.ClientID) + http.Redirect(w, r, authURL, http.StatusFound) +} + +func getArcgisOauthCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + log.Info().Str("code", code).Msg("Handling oauth callback") + if code == "" { + respondError(w, "Access code is empty", nil, http.StatusBadRequest) + return + } + user, err := auth.GetAuthenticatedUser(r) + if err != nil { + respondError(w, "You're not currently authenticated, which really shouldn't happen.", err, http.StatusUnauthorized) + return + } + err = background.HandleOauthAccessCode(r.Context(), user, code) + if err != nil { + respondError(w, "Failed to handle access code", err, http.StatusInternalServerError) + return + } + http.Redirect(w, r, config.MakeURLSync("/"), http.StatusFound) +} + +func getOAuthRefresh(w http.ResponseWriter, r *http.Request) { + user, err := auth.GetAuthenticatedUser(r) + if err != nil { + http.Redirect(w, r, "/?next=/oauth/refresh", http.StatusFound) + return + } + oauthPrompt(w, user) +} + +func oauthPrompt(w http.ResponseWriter, user *models.User) { + dp := user.DisplayName + data := ContentDashboard{ + User: User{ + DisplayName: dp, + Initials: extractInitials(dp), + Username: user.Username, + }, + } + htmlpage.RenderOrError(w, oauthPromptT, data) +} diff --git a/sync/page.go b/sync/page.go index 0e35bd02..c0d0e3de 100644 --- a/sync/page.go +++ b/sync/page.go @@ -2,32 +2,11 @@ package sync import ( "embed" + "fmt" + "net/http" "github.com/Gleipnir-Technology/nidus-sync/htmlpage" - - //"bytes" - "context" - //"errors" - "fmt" - //"html/template" - //"io" - //"math" - "net/http" - //"os" - //"strconv" - //"strings" - "time" - - "github.com/Gleipnir-Technology/nidus-sync/background" - "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db" - "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/h3utils" - //"github.com/aarondl/opt/null" - "github.com/google/uuid" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/uber/h3-go/v4" ) //go:embed template/* @@ -36,49 +15,6 @@ var embeddedFiles embed.FS //go:embed static/* var EmbeddedStaticFS embed.FS -// Authenticated pages -var ( - cell = buildTemplate("cell", "authenticated") - dashboard = buildTemplate("dashboard", "authenticated") - oauthPrompt = buildTemplate("oauth-prompt", "authenticated") - settings = buildTemplate("settings", "authenticated") - source = buildTemplate("source", "authenticated") -) - -// Unauthenticated pages -var ( - admin = buildTemplate("admin", "base") - dataEntry = buildTemplate("data-entry", "base") - dataEntryGood = buildTemplate("data-entry-good", "base") - dataEntryBad = buildTemplate("data-entry-bad", "base") - dispatch = buildTemplate("dispatch", "base") - dispatchResults = buildTemplate("dispatch-results", "base") - mockRoot = buildTemplate("mock-root", "base") - reportPage = buildTemplate("report", "base") - reportConfirmation = buildTemplate("report-confirmation", "base") - reportContribute = buildTemplate("report-contribute", "base") - reportDetail = buildTemplate("report-detail", "base") - reportEvidence = buildTemplate("report-evidence", "base") - reportSchedule = buildTemplate("report-schedule", "base") - reportUpdate = buildTemplate("report-update", "base") - serviceRequest = buildTemplate("service-request", "base") - serviceRequestDetail = buildTemplate("service-request-detail", "base") - serviceRequestLocation = buildTemplate("service-request-location", "base") - serviceRequestMosquito = buildTemplate("service-request-mosquito", "base") - serviceRequestPool = buildTemplate("service-request-pool", "base") - serviceRequestQuick = buildTemplate("service-request-quick", "base") - serviceRequestQuickConfirmation = buildTemplate("service-request-quick-confirmation", "base") - serviceRequestUpdates = buildTemplate("service-request-updates", "base") - settingRoot = buildTemplate("setting-mock", "base") - settingIntegration = buildTemplate("setting-integration", "base") - settingPesticide = buildTemplate("setting-pesticide", "base") - settingPesticideAdd = buildTemplate("setting-pesticide-add", "base") - settingUsers = buildTemplate("setting-user", "base") - settingUsersAdd = buildTemplate("setting-user-add", "base") - signin = buildTemplate("signin", "base") - signup = buildTemplate("signup", "base") -) - var components = [...]string{"header", "map"} func buildTemplate(files ...string) *htmlpage.BuiltTemplate { @@ -93,261 +29,6 @@ func buildTemplate(files ...string) *htmlpage.BuiltTemplate { return htmlpage.NewBuiltTemplate(embeddedFiles, "sync/", full_files...) } -func Cell(ctx context.Context, w http.ResponseWriter, user *models.User, c int64) { - 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() - if err != nil { - respondError(w, "Failed to get center", err, http.StatusInternalServerError) - return - } - boundary, err := h3.Cell(c).Boundary() - if err != nil { - respondError(w, "Failed to get boundary", err, http.StatusInternalServerError) - return - } - inspections, err := inspectionsByCell(ctx, org, h3.Cell(c)) - if err != nil { - respondError(w, "Failed to get inspections by cell", err, http.StatusInternalServerError) - return - } - geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)}) - if err != nil { - respondError(w, "Failed to get boundaries", err, http.StatusInternalServerError) - return - } - 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 - } - treatments, err := treatmentsByCell(ctx, org, h3.Cell(c)) - if err != nil { - respondError(w, "Failed to get treatments", err, http.StatusInternalServerError) - return - } - data := ContentCell{ - BreedingSources: sources, - CellBoundary: boundary, - Inspections: inspections, - MapData: ComponentMap{ - Center: h3.LatLng{ - Lat: center.Lat, - Lng: center.Lng, - }, - GeoJSON: geojson, - MapboxToken: config.MapboxToken, - Zoom: resolution + 5, - }, - Treatments: treatments, - User: userContent, - } - htmlpage.RenderOrError(w, cell, &data) -} - -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 - } - var lastSync *time.Time - sync, err := org.FieldseekerSyncs(sm.OrderBy("created").Desc()).One(ctx, db.PGInstance.BobDB) - if err != nil { - if err.Error() != "sql: no rows in result set" { - respondError(w, "Failed to get syncs", err, http.StatusInternalServerError) - return - } - } else { - lastSync = &sync.Created - } - is_syncing := background.IsSyncOngoing(org.ID) - inspectionCount, err := org.Mosquitoinspections().Count(ctx, db.PGInstance.BobDB) - if err != nil { - respondError(w, "Failed to get inspection count", err, http.StatusInternalServerError) - return - } - sourceCount, err := org.Pointlocations().Count(ctx, db.PGInstance.BobDB) - if err != nil { - respondError(w, "Failed to get source count", err, http.StatusInternalServerError) - return - } - serviceCount, err := org.Servicerequests().Count(ctx, db.PGInstance.BobDB) - if err != nil { - respondError(w, "Failed to get service count", err, http.StatusInternalServerError) - return - } - recentRequests, err := org.Servicerequests(sm.OrderBy("creationdate").Desc(), sm.Limit(10)).All(ctx, db.PGInstance.BobDB) - if err != nil { - respondError(w, "Failed to get recent service", err, http.StatusInternalServerError) - return - } - - requests := make([]ServiceRequestSummary, 0) - for _, r := range recentRequests { - requests = append(requests, ServiceRequestSummary{ - Date: r.Creationdate.MustGet(), - Location: r.Reqaddr1.MustGet(), - Status: "Completed", - }) - } - userContent, err := contentForUser(ctx, user) - if err != nil { - respondError(w, "Failed to get user context", err, http.StatusInternalServerError) - return - } - data := ContentDashboard{ - CountInspections: int(inspectionCount), - CountMosquitoSources: int(sourceCount), - CountServiceRequests: int(serviceCount), - IsSyncOngoing: is_syncing, - LastSync: lastSync, - MapData: ComponentMap{ - MapboxToken: config.MapboxToken, - }, - Org: org.Name.MustGet(), - RecentRequests: requests, - User: userContent, - } - htmlpage.RenderOrError(w, dashboard, data) -} - -func Mock(t string, w http.ResponseWriter, code string) { - data := ContentMock{ - DistrictName: "Delta MVCD", - URLs: ContentMockURLs{ - Dispatch: "/mock/dispatch", - DispatchResults: "/mock/dispatch-results", - ReportConfirmation: fmt.Sprintf("/mock/report/%s/confirm", code), - ReportDetail: fmt.Sprintf("/mock/report/%s", code), - ReportContribute: fmt.Sprintf("/mock/report/%s/contribute", code), - ReportEvidence: fmt.Sprintf("/mock/report/%s/evidence", code), - ReportSchedule: fmt.Sprintf("/mock/report/%s/schedule", code), - ReportUpdate: fmt.Sprintf("/mock/report/%s/update", code), - Root: "/mock", - Setting: "/mock/setting", - SettingIntegration: "/mock/setting/integration", - SettingPesticide: "/mock/setting/pesticide", - SettingPesticideAdd: "/mock/setting/pesticide/add", - SettingUser: "/mock/setting/user", - SettingUserAdd: "/mock/setting/user/add", - }, - } - template, ok := htmlpage.TemplatesByFilename[t+".html"] - if !ok { - log.Error().Str("template", t).Msg("Failed to find template") - respondError(w, "Failed to render template", nil, http.StatusInternalServerError) - return - } - htmlpage.RenderOrError(w, &template, data) -} - -func OauthPrompt(w http.ResponseWriter, user *models.User) { - dp := user.DisplayName - data := ContentDashboard{ - User: User{ - DisplayName: dp, - Initials: extractInitials(dp), - Username: user.Username, - }, - } - htmlpage.RenderOrError(w, oauthPrompt, data) -} - -func Settings(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 := ContentAuthenticatedPlaceholder{ - User: userContent, - } - htmlpage.RenderOrError(w, settings, data) -} - -func Signin(w http.ResponseWriter, errorCode string) { - data := ContentSignin{ - InvalidCredentials: errorCode == "invalid-credentials", - } - htmlpage.RenderOrError(w, signin, data) -} - -func Signup(w http.ResponseWriter, path string) { - data := ContentSignup{} - htmlpage.RenderOrError(w, signup, 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) - 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 - } - 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, - } - - htmlpage.RenderOrError(w, source, data) -} - // Respond with an error that is visible to the user func respondError(w http.ResponseWriter, m string, e error, s int) { log.Warn().Int("status", s).Err(e).Str("user message", m).Msg("Responding with an error from sync pages") diff --git a/sync/routes.go b/sync/routes.go new file mode 100644 index 00000000..0a24a87f --- /dev/null +++ b/sync/routes.go @@ -0,0 +1,76 @@ +package sync + +import ( + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/api" + "github.com/Gleipnir-Technology/nidus-sync/auth" + "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/go-chi/chi/v5" +) + +func Router() chi.Router { + r := chi.NewRouter() + // Root is a special endpoint that is neither authenticated nor unauthenticated + r.Get("/", getRoot) + + // Unauthenticated endpoints + r.Get("/arcgis/oauth/begin", getArcgisOauthBegin) + r.Get("/arcgis/oauth/callback", getArcgisOauthCallback) + r.Get("/district", getDistrict) + r.Get("/favicon.ico", getFavicon) + + r.Get("/mock", renderMock("mock-root")) + r.Get("/mock/admin", renderMock("admin")) + r.Get("/mock/admin/service-request", renderMock("admin-service-request")) + r.Get("/mock/data-entry", renderMock("data-entry")) + r.Get("/mock/data-entry/bad", renderMock("data-entry-bad")) + r.Get("/mock/data-entry/good", renderMock("data-entry-good")) + r.Get("/mock/dispatch", renderMock("dispatch")) + r.Get("/mock/dispatch-results", renderMock("dispatch-results")) + r.Get("/mock/report", renderMock("report")) + r.Get("/mock/report/{code}", renderMock("report-detail")) + r.Get("/mock/report/{code}/confirm", renderMock("report-confirmation")) + r.Get("/mock/report/{code}/contribute", renderMock("report-contribute")) + r.Get("/mock/report/{code}/evidence", renderMock("report-evidence")) + r.Get("/mock/report/{code}/schedule", renderMock("report-schedule")) + r.Get("/mock/report/{code}/update", renderMock("report-update")) + r.Get("/mock/service-request", renderMock("service-request")) + r.Get("/mock/service-request/{code}", renderMock("service-request-detail")) + r.Get("/mock/service-request-location", renderMock("service-request-location")) + r.Get("/mock/service-request-mosquito", renderMock("service-request-mosquito")) + r.Get("/mock/service-request-pool", renderMock("service-request-pool")) + r.Get("/mock/service-request-quick", renderMock("service-request-quick")) + r.Get("/mock/service-request-quick-confirmation", renderMock("service-request-quick-confirmation")) + r.Get("/mock/service-request-updates", renderMock("service-request-updates")) + r.Get("/mock/setting", renderMock("setting-mock")) + r.Get("/mock/setting/integration", renderMock("setting-integration")) + r.Get("/mock/setting/pesticide", renderMock("setting-pesticide")) + r.Get("/mock/setting/pesticide/add", renderMock("setting-pesticide-add")) + r.Get("/mock/setting/user", renderMock("setting-user")) + r.Get("/mock/setting/user/add", renderMock("setting-user-add")) + + r.Get("/oauth/refresh", getOAuthRefresh) + + r.Get("/qr-code/report/{code}", getQRCodeReport) + r.Get("/signin", getSignin) + r.Post("/signin", postSignin) + r.Get("/signup", getSignup) + r.Post("/signup", postSignup) + r.Get("/sms", getSMS) + r.Post("/sms", postSMS) + r.Get("/sms.php", getSMS) + r.Get("/sms/{org}", getSMS) + r.Post("/sms/{org}", postSMS) + + // Authenticated endpoints + r.Route("/api", api.AddRoutes) + r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) + r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings)) + r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) + //r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", auth.NewEnsureAuth(getVectorTiles)) + + localFS := http.Dir("./sync/static") + htmlpage.FileServer(r, "/static", localFS, EmbeddedStaticFS, "static") + return r +} diff --git a/sync/signin.go b/sync/signin.go new file mode 100644 index 00000000..180b119c --- /dev/null +++ b/sync/signin.go @@ -0,0 +1,95 @@ +package sync + +import ( + "errors" + "net/http" + "strings" + + "github.com/Gleipnir-Technology/nidus-sync/auth" + "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/rs/zerolog/log" +) + +var ( + signinT = buildTemplate("signin", "base") + signupT = buildTemplate("signup", "base") +) + +func getSignin(w http.ResponseWriter, r *http.Request) { + errorCode := r.URL.Query().Get("error") + signin(w, errorCode) +} + +func getSignup(w http.ResponseWriter, r *http.Request) { + signup(w, r.URL.Path) +} + +func postSignin(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + respondError(w, "Could not parse form", err, http.StatusBadRequest) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + log.Info().Str("username", username).Msg("Signin") + + _, err := auth.SigninUser(r, username, password) + if err != nil { + if errors.Is(err, auth.InvalidCredentials{}) { + http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound) + return + } + if errors.Is(err, auth.InvalidUsername{}) { + http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound) + return + } + respondError(w, "Failed to signin user", err, http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusFound) +} + +func postSignup(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + respondError(w, "Could not parse form", err, http.StatusBadRequest) + return + } + + username := r.FormValue("username") + name := r.FormValue("name") + password := r.FormValue("password") + terms := r.FormValue("terms") + + log.Info().Str("username", username).Str("name", name).Str("password", strings.Repeat("*", len(password))).Msg("Signup") + + if terms != "on" { + log.Warn().Msg("Terms not agreed") + http.Error(w, "You must agree to the terms to register", http.StatusBadRequest) + return + } + + user, err := auth.SignupUser(r.Context(), username, name, password) + if err != nil { + respondError(w, "Failed to signup user", err, http.StatusInternalServerError) + return + } + + auth.AddUserSession(r, user) + + http.Redirect(w, r, "/", http.StatusFound) +} + +func signin(w http.ResponseWriter, errorCode string) { + data := ContentSignin{ + InvalidCredentials: errorCode == "invalid-credentials", + } + htmlpage.RenderOrError(w, signinT, data) +} + +func signup(w http.ResponseWriter, path string) { + data := ContentSignup{} + htmlpage.RenderOrError(w, signupT, data) +} diff --git a/sync/sms.go b/sync/sms.go index 3e7ebd7c..4bb6d439 100644 --- a/sync/sms.go +++ b/sync/sms.go @@ -1,6 +1,7 @@ package sync import ( + "encoding/json" "fmt" "io" "net/http" @@ -10,6 +11,7 @@ import ( "strings" "time" + "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" ) @@ -148,3 +150,52 @@ func sanitizeFilename(name string) string { } return result } +func postSMS(w http.ResponseWriter, r *http.Request) { + // Log all request headers + for name, values := range r.Header { + for _, value := range values { + log.Info().Str("name", name).Str("value", value).Msg("header") + } + } + + // Read the request body + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + //return nil, fmt.Errorf("failed to read request body: %w", err) + respondError(w, "Failed to read request body", err, http.StatusInternalServerError) + return + } + log.Info().Str("body", string(bodyBytes)).Msg("body") + // Close the original body + defer r.Body.Close() + + // Parse JSON into webhook struct + var body SMSWebhookBody + if err := json.Unmarshal(bodyBytes, &body); err != nil { + respondError(w, "Failed to parse JSON", err, http.StatusBadRequest) + return + } + + if err := handleSMSMessage(&body.Data); err != nil { + log.Error().Err(err).Msg("Failed to handle SMS Message") + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} +func getSMS(w http.ResponseWriter, r *http.Request) { + org := chi.URLParam(r, "org") + + to := r.URL.Query().Get("error") + from := r.URL.Query().Get("error") + message := r.URL.Query().Get("error") + files := r.URL.Query().Get("error") + id := r.URL.Query().Get("error") + date := r.URL.Query().Get("error") + + log.Info().Str("org", org).Str("to", to).Str("from", from).Str("message", message).Str("files", files).Str("id", id).Str("date", date).Msg("Got SMS Message") + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-type", "text/plain") + // Signifies to Voip.ms that the callback worked. + fmt.Fprintf(w, "ok") +} diff --git a/sync/template/district.html b/sync/template/district.html new file mode 100644 index 00000000..3540a5f7 --- /dev/null +++ b/sync/template/district.html @@ -0,0 +1,174 @@ +{{template "authenticated.html" .}} + +{{define "title"}}Dash{{end}} +{{define "extraheader"}} + + + + +{{end}} +{{define "content"}} +
District page placeholder
+{{end}}