2026-01-07 22:35:28 +00:00
|
|
|
package publicreport
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-01-08 15:00:30 +00:00
|
|
|
"fmt"
|
2026-01-07 22:35:28 +00:00
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2026-01-08 15:00:30 +00:00
|
|
|
"strconv"
|
|
|
|
|
"time"
|
2026-01-07 22:35:28 +00:00
|
|
|
|
2026-01-08 15:00:30 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
2026-01-07 22:35:28 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/htmlpage"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/htmlpage/public-reports"
|
2026-01-08 15:00:30 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/userfile"
|
|
|
|
|
"github.com/aarondl/opt/omit"
|
|
|
|
|
"github.com/aarondl/opt/omitnull"
|
2026-01-07 22:35:28 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2026-01-08 15:00:30 +00:00
|
|
|
"github.com/google/uuid"
|
2026-01-07 22:35:28 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2026-01-08 15:00:30 +00:00
|
|
|
"github.com/stephenafamo/bob/dialect/psql"
|
|
|
|
|
"github.com/stephenafamo/bob/dialect/psql/um"
|
2026-01-07 22:35:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func Router() chi.Router {
|
|
|
|
|
r := chi.NewRouter()
|
|
|
|
|
r.Get("/", getRoot)
|
|
|
|
|
r.Get("/nuisance", getNuisance)
|
|
|
|
|
r.Get("/pool", getPool)
|
|
|
|
|
r.Get("/quick", getQuick)
|
|
|
|
|
r.Post("/quick-submit", postQuick)
|
|
|
|
|
r.Get("/quick-submit-complete", getQuickSubmitComplete)
|
2026-01-08 15:34:48 +00:00
|
|
|
r.Post("/register-notifications", postRegisterNotifications)
|
|
|
|
|
r.Get("/register-notifications-complete", getRegisterNotificationsComplete)
|
2026-01-07 22:35:28 +00:00
|
|
|
r.Get("/status", getStatus)
|
|
|
|
|
localFS := http.Dir("./static")
|
|
|
|
|
htmlpage.FileServer(r, "/static", localFS, publicreports.EmbeddedStaticFS, "static")
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getRoot(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.Root,
|
|
|
|
|
publicreports.ContextRoot{},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getNuisance(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.Nuisance,
|
|
|
|
|
publicreports.ContextNuisance{},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
func getPool(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.Pool,
|
|
|
|
|
publicreports.ContextPool{},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
func getQuick(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.Quick,
|
|
|
|
|
publicreports.ContextQuick{},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
report := r.URL.Query().Get("report")
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.QuickSubmitComplete,
|
|
|
|
|
publicreports.ContextQuickSubmitComplete{
|
|
|
|
|
ReportID: report,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-08 15:34:48 +00:00
|
|
|
func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
report := r.URL.Query().Get("report")
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.RegisterNotificationsComplete,
|
|
|
|
|
publicreports.ContextRegisterNotificationsComplete{
|
|
|
|
|
ReportID: report,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-07 22:35:28 +00:00
|
|
|
func getStatus(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
htmlpage.RenderOrError(
|
|
|
|
|
w,
|
|
|
|
|
publicreports.Status,
|
|
|
|
|
publicreports.ContextStatus{},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
func postQuick(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-08 15:00:30 +00:00
|
|
|
lat := r.FormValue("latitude")
|
|
|
|
|
lng := r.FormValue("longitude")
|
|
|
|
|
comments := r.FormValue("comments")
|
2026-01-07 22:35:28 +00:00
|
|
|
//photos := r.FormValue("photos")
|
|
|
|
|
|
2026-01-08 15:00:30 +00:00
|
|
|
latitude, err := strconv.ParseFloat(lat, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create parse latitude", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
longitude, err := strconv.ParseFloat(lng, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create parse longitude", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-08 15:34:48 +00:00
|
|
|
u, err := GenerateReportID()
|
2026-01-08 15:00:30 +00:00
|
|
|
if err != nil {
|
2026-01-08 15:34:48 +00:00
|
|
|
respondError(w, "Failed to create quick report public ID", err, http.StatusInternalServerError)
|
2026-01-08 15:00:30 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c, err := h3utils.GetCell(longitude, latitude, 15)
|
|
|
|
|
setter := models.PublicreportQuickSetter{
|
|
|
|
|
Created: omit.From(time.Now()),
|
|
|
|
|
Comments: omit.From(comments),
|
|
|
|
|
//Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)),
|
2026-01-08 15:34:48 +00:00
|
|
|
H3cell: omitnull.From(c.String()),
|
|
|
|
|
PublicID: omit.From(u),
|
|
|
|
|
ReporterEmail: omit.From(""),
|
|
|
|
|
ReporterPhone: omit.From(""),
|
2026-01-08 15:00:30 +00:00
|
|
|
}
|
|
|
|
|
quick, err := models.PublicreportQuicks.Insert(&setter).One(r.Context(), db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create database record", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = psql.Update(
|
|
|
|
|
um.Table("publicreport.quick"),
|
|
|
|
|
um.SetCol("location").To(fmt.Sprintf("ST_GeometryFromText('Point(%f %f)')", longitude, latitude)),
|
|
|
|
|
um.Where(psql.Quote("id").EQ(psql.Arg(quick.ID))),
|
|
|
|
|
).Exec(r.Context(), db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to insert publicreport", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-08 15:34:48 +00:00
|
|
|
log.Info().Float64("latitude", latitude).Float64("longitude", longitude).Msg("Got upload")
|
2026-01-08 15:00:30 +00:00
|
|
|
photoSetters := make([]*models.PublicreportQuickPhotoSetter, 0)
|
2026-01-07 22:35:28 +00:00
|
|
|
for _, fheaders := range r.MultipartForm.File {
|
|
|
|
|
for _, headers := range fheaders {
|
|
|
|
|
file, err := headers.Open()
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to open header", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
buff := make([]byte, 512)
|
|
|
|
|
file.Read(buff)
|
|
|
|
|
|
|
|
|
|
file.Seek(0, 0)
|
|
|
|
|
contentType := http.DetectContentType(buff)
|
|
|
|
|
var sizeBuff bytes.Buffer
|
|
|
|
|
fileSize, err := sizeBuff.ReadFrom(file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to read file", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
file.Seek(0, 0)
|
|
|
|
|
contentBuf := bytes.NewBuffer(nil)
|
|
|
|
|
if _, err := io.Copy(contentBuf, file); err != nil {
|
|
|
|
|
respondError(w, "Failed to save file", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
log.Info().Int64("size", fileSize).Str("filename", headers.Filename).Str("content-type", contentType).Msg("Got an uploaded file")
|
2026-01-08 15:00:30 +00:00
|
|
|
u, err := uuid.NewUUID()
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create quick report photo uuid", err, http.StatusInternalServerError)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
err = userfile.PublicImageFileContentWrite(u, file)
|
|
|
|
|
photoSetters = append(photoSetters, &models.PublicreportQuickPhotoSetter{
|
|
|
|
|
Size: omit.From(fileSize),
|
|
|
|
|
Filename: omit.From(headers.Filename),
|
|
|
|
|
UUID: omit.From(u),
|
|
|
|
|
})
|
2026-01-07 22:35:28 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-08 15:34:48 +00:00
|
|
|
err = quick.InsertQuickPhotos(r.Context(), db.PGInstance.BobDB, photoSetters...)
|
2026-01-08 15:00:30 +00:00
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create photo records", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
2026-01-08 15:34:48 +00:00
|
|
|
}
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/quick-submit-complete?report=%s", u), http.StatusFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func postRegisterNotifications(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
err := r.ParseForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
consent := r.PostFormValue("consent")
|
|
|
|
|
email := r.PostFormValue("email")
|
|
|
|
|
phone := r.PostFormValue("phone")
|
|
|
|
|
report_id := r.PostFormValue("report_id")
|
|
|
|
|
if consent != "on" {
|
|
|
|
|
respondError(w, "You must consent", nil, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
result, err := psql.Update(
|
|
|
|
|
um.Table("publicreport.quick"),
|
|
|
|
|
um.SetCol("reporter_email").ToArg(email),
|
|
|
|
|
um.SetCol("reporter_phone").ToArg(phone),
|
|
|
|
|
um.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))),
|
|
|
|
|
).Exec(r.Context(), db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to update report", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
rowcount, err := result.RowsAffected()
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to get rows affected", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if rowcount == 0 {
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/error?code=no-rows-affected&report=%s", report_id), http.StatusFound)
|
|
|
|
|
} else {
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/register-notifications-complete?report=%s", report_id), http.StatusFound)
|
|
|
|
|
}
|
2026-01-07 22:35:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
http.Error(w, m, s)
|
|
|
|
|
}
|