diff --git a/sync/configuration.go b/api/configuration.go similarity index 99% rename from sync/configuration.go rename to api/configuration.go index 3924bdfb..7bfddf2d 100644 --- a/sync/configuration.go +++ b/api/configuration.go @@ -1,4 +1,4 @@ -package sync +package api import ( "context" diff --git a/api/handler.go b/api/handler.go index f90a362a..abc3d172 100644 --- a/api/handler.go +++ b/api/handler.go @@ -64,44 +64,90 @@ func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler { }) } -type handlerFunctionPost[ReqType any, ResponseType any] func(context.Context, *http.Request, platform.User, ReqType) (ResponseType, *nhttp.ErrorWithStatus) +type handlerFunctionPost[ReqType any] func(context.Context, *http.Request, ReqType) (string, *nhttp.ErrorWithStatus) +type handlerFunctionPostAuthenticated[ReqType any] func(context.Context, *http.Request, platform.User, ReqType) (string, *nhttp.ErrorWithStatus) -func authenticatedHandlerJSONPost[ReqType any, ResponseType any](f handlerFunctionPost[ReqType, ResponseType]) http.Handler { +func authenticatedHandlerJSONPost[ReqType any](f handlerFunctionPostAuthenticated[ReqType]) http.Handler { return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { w.Header().Set("Content-Type", "application/json") - var req ReqType - body, err := io.ReadAll(r.Body) - if err != nil { - respondError(w, http.StatusInternalServerError, "Failed to read body: %w", err) - return - } - err = json.Unmarshal(body, &req) - if err != nil { - respondError(w, http.StatusBadRequest, "Failed to decode request: %w", err) + req, e := parseRequest[ReqType](r) + if e != nil { + serializeError(w, e) return } ctx := r.Context() - response, e := f(ctx, r, u, req) + path, e := f(ctx, r, u, *req) if e != nil { - log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api") - body, err = json.Marshal(ErrorAPI{Message: e.Error()}) - if err != nil { - log.Error().Err(err).Msg("failed to marshal error") - http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError) - return - } - http.Error(w, string(body), e.Status) + serializeError(w, e) return } - resp_body, err := json.Marshal(response) - if err != nil { - respondError(w, http.StatusInternalServerError, "Failed to marshal json response: %w", err) - return - } - w.Write(resp_body) + http.Redirect(w, r, path, http.StatusFound) }) } +func parseRequest[ReqType any](r *http.Request) (*ReqType, *nhttp.ErrorWithStatus) { + var req ReqType + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, nhttp.NewError("Failed to read body: %w", err) + } + err = json.Unmarshal(body, &req) + if err != nil { + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Failed to decode request: %w", err) + } + return &req, nil +} +func serializeError(w http.ResponseWriter, e *nhttp.ErrorWithStatus) { + log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api") + body, err := json.Marshal(ErrorAPI{Message: e.Error()}) + if err != nil { + log.Error().Err(err).Msg("failed to marshal error") + http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError) + return + } + http.Error(w, string(body), e.Status) + return +} +func handlerJSONPost[ReqType any](f handlerFunctionPost[ReqType]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + req, e := parseRequest[ReqType](r) + if e != nil { + serializeError(w, e) + return + } + ctx := r.Context() + path, e := f(ctx, r, *req) + if e != nil { + return + } + http.Redirect(w, r, path, http.StatusFound) + } +} +func authenticatedHandlerPostMultipart[RequestType any](f handlerFunctionPostAuthenticated[RequestType]) http.Handler { + return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { + err := r.ParseMultipartForm(32 << 10) // 32 MB buffer + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to parse form: %w ", err) + return + } + + var content RequestType + + err = decoder.Decode(&content, r.PostForm) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err) + return + } + ctx := r.Context() + 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 respondError(w http.ResponseWriter, status int, format string, args ...any) { outer_err := fmt.Errorf(format, args...) body, err := json.Marshal(ErrorAPI{ diff --git a/api/lead.go b/api/lead.go index af64a5ac..25b04f9a 100644 --- a/api/lead.go +++ b/api/lead.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" @@ -12,9 +13,6 @@ type createLead struct { PoolLocations map[int]platform.Location `json:"pool_locations"` SignalIDs []int `json:"signal_ids"` } -type createdLead struct { - ID int32 `json:"id"` -} type contentListLead struct { Leads []lead `json:"leads"` } @@ -27,12 +25,12 @@ func listLead(ctx context.Context, r *http.Request, user platform.User, query qu Leads: make([]lead, 0), }, nil } -func postLeads(ctx context.Context, r *http.Request, user platform.User, req createLead) (*createdLead, *nhttp.ErrorWithStatus) { +func postLeads(ctx context.Context, r *http.Request, user platform.User, req createLead) (string, *nhttp.ErrorWithStatus) { if len(req.SignalIDs) == 0 { - return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with no signals") + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with no signals") } if len(req.SignalIDs) > 1 { - return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet") + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet") } signal_id := req.SignalIDs[0] var pool_location *platform.Location @@ -42,14 +40,12 @@ func postLeads(ctx context.Context, r *http.Request, user platform.User, req cre } site_id, err := platform.SiteFromSignal(ctx, user, int32(signal_id)) if err != nil || site_id == nil { - return nil, nhttp.NewError("site from signal: %w", err) + return "", nhttp.NewError("site from signal: %w", err) } lead_id, err := platform.LeadCreate(ctx, user, int32(signal_id), *site_id, pool_location) if err != nil || lead_id == nil { - return nil, nhttp.NewError("lead create: %w", err) + return "", nhttp.NewError("lead create: %w", err) } - return &createdLead{ - ID: *lead_id, - }, nil + return fmt.Sprintf("/lead/%d", *lead_id), nil } diff --git a/api/publicreport.go b/api/publicreport.go index fc69d381..195c1a41 100644 --- a/api/publicreport.go +++ b/api/publicreport.go @@ -2,10 +2,9 @@ package api import ( "context" + "fmt" "net/http" - "strconv" - "github.com/Gleipnir-Technology/nidus-sync/config" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" ) @@ -13,54 +12,39 @@ import ( type formPublicreportSignal struct { ReportID string `json:"reportID"` } -type createdSignal struct { - ID int32 `json:"id"` -} -func postPublicreportSignal(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (*createdSignal, *nhttp.ErrorWithStatus) { +func postPublicreportSignal(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (string, *nhttp.ErrorWithStatus) { signal_id, err := platform.SignalCreateFromPublicreport(ctx, user, req.ReportID) if err != nil { - return nil, nhttp.NewError("create signal: %w", err) + return "", nhttp.NewError("create signal: %w", err) } - return &createdSignal{ - ID: *signal_id, - }, nil + return fmt.Sprintf("/signal/%d", *signal_id), nil } type formPublicreportInvalid struct { ReportID string `json:"reportID"` } -type createdReport struct { - URI string `json:"uri"` -} -func postPublicreportInvalid(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (*createdReport, *nhttp.ErrorWithStatus) { +func postPublicreportInvalid(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (string, *nhttp.ErrorWithStatus) { err := platform.PublicreportInvalid(ctx, user, req.ReportID) if err != nil { - return nil, nhttp.NewError("create signal: %w", err) + return "", nhttp.NewError("create signal: %w", err) } - return &createdReport{ - URI: config.MakeURLNidus("/publicreport/%s", req.ReportID), - }, nil + return fmt.Sprintf("/publicreport/%s", req.ReportID), nil } type formPublicreportMessage struct { Message string `json:"message"` ReportID string `json:"reportID"` } -type createdMessage struct { - URI string `json:"uri"` -} -func postPublicreportMessage(ctx context.Context, r *http.Request, user platform.User, req formPublicreportMessage) (*createdMessage, *nhttp.ErrorWithStatus) { +func postPublicreportMessage(ctx context.Context, r *http.Request, user platform.User, req formPublicreportMessage) (string, *nhttp.ErrorWithStatus) { msg_id, err := platform.PublicReportMessageCreate(ctx, user, req.ReportID, req.Message) if err != nil { - return nil, nhttp.NewError("failed to create message: %s", err) + return "", nhttp.NewError("failed to create message: %s", err) } if msg_id == nil { - return nil, nhttp.NewError("nil message id") + return "", nhttp.NewError("nil message id") } - return &createdMessage{ - URI: config.MakeURLNidus("/message/%s", strconv.Itoa(int(*msg_id))), - }, nil + return fmt.Sprintf("/message/%d", *msg_id), nil } diff --git a/api/review.go b/api/review.go index 18f72738..8b8d2d20 100644 --- a/api/review.go +++ b/api/review.go @@ -3,6 +3,7 @@ package api import ( "context" "errors" + "fmt" "net/http" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" @@ -14,16 +15,15 @@ type createReviewPool struct { TaskID int32 `json:"task_id"` Updates *platform.PoolUpdate `json:"updates"` } -type createdReviewPool struct{} -func postReviewPool(ctx context.Context, r *http.Request, user platform.User, req createReviewPool) (*createdReviewPool, *nhttp.ErrorWithStatus) { - _, err := platform.ReviewPoolCreate(ctx, user, req.TaskID, req.Status, req.Updates) +func postReviewPool(ctx context.Context, r *http.Request, user platform.User, req createReviewPool) (string, *nhttp.ErrorWithStatus) { + id, err := platform.ReviewPoolCreate(ctx, user, req.TaskID, req.Status, req.Updates) if err != nil { if errors.As(err, &platform.ErrorNotFound{}) { - return nil, nhttp.NewErrorStatus(http.StatusNotFound, "review task %d not found", req.TaskID) + return "", nhttp.NewErrorStatus(http.StatusNotFound, "review task %d not found", req.TaskID) } - return nil, nhttp.NewError("failed to set review: %w", err) + return "", nhttp.NewError("failed to set review: %w", err) } - return &createdReviewPool{}, nil + return fmt.Sprintf("/review/%d", id), nil } diff --git a/api/routes.go b/api/routes.go index 84d8950e..c1a79812 100644 --- a/api/routes.go +++ b/api/routes.go @@ -8,12 +8,20 @@ import ( ) func AddRoutes(r chi.Router) { - // Authenticated endpoints r.Use(render.SetContentType(render.ContentTypeJSON)) + // Unauthenticated endpoints + r.Post("/signin", handlerJSONPost(postSignin)) + r.Post("/signup", handlerJSONPost(postSignup)) + // Authenticated endpoints r.Method("POST", "/audio/{uuid}", auth.NewEnsureAuth(apiAudioPost)) r.Method("POST", "/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost)) r.Method("GET", "/client/ios", auth.NewEnsureAuth(handleClientIos)) r.Method("GET", "/communication", authenticatedHandlerJSON(listCommunication)) + r.Method("POST", "/configuration/integration/arcgis", authenticatedHandlerJSONPost(postConfigurationIntegrationArcgis)) + r.Method("POST", "/configuration/upload/pool/flyover", authenticatedHandlerPostMultipart(postUploadPoolFlyoverCreate)) + r.Method("POST", "/configuration/upload/pool/custom", authenticatedHandlerPostMultipart(postUploadPoolCustomCreate)) + r.Method("POST", "/configuration/upload/{id}/commit", authenticatedHandlerJSONPost(postUploadCommit)) + r.Method("POST", "/configuration/upload/{id}/discard", authenticatedHandlerJSONPost(postUploadDiscard)) r.Method("GET", "/events", auth.NewEnsureAuth(streamEvents)) r.Method("POST", "/image/{uuid}", auth.NewEnsureAuth(apiImagePost)) r.Method("GET", "/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentGet)) @@ -28,6 +36,9 @@ func AddRoutes(r chi.Router) { r.Method("GET", "/review-task/pool", authenticatedHandlerJSON(listReviewTaskPool)) r.Method("GET", "/service-request", auth.NewEnsureAuth(apiServiceRequest)) r.Method("GET", "/signal", authenticatedHandlerJSON(listSignal)) + r.Method("POST", "/sudo/email", authenticatedHandlerJSONPost(postSudoEmail)) + r.Method("POST", "/sudo/sms", authenticatedHandlerJSONPost(postSudoSMS)) + r.Method("POST", "/sudo/sse", authenticatedHandlerJSONPost(postSudoSSE)) r.Method("GET", "/trap-data", auth.NewEnsureAuth(apiTrapData)) r.Method("GET", "/tile/{z}/{y}/{x}", auth.NewEnsureAuth(getTile)) r.Method("GET", "/upload/{id}", authenticatedHandlerJSON(getUploadByID)) @@ -39,7 +50,6 @@ func AddRoutes(r chi.Router) { r.Get("/district", apiGetDistrict) r.Get("/district/{slug}/logo", apiGetDistrictLogo) r.Get("/compliance-request/image/pool/{public_id}", getComplianceRequestImagePool) - r.Post("/signin", postSignin) r.Post("/twilio/call", twilioCallPost) r.Post("/twilio/call/status", twilioCallStatusPost) r.Post("/twilio/message", twilioMessagePost) diff --git a/api/signin.go b/api/signin.go index bd319adc..6af9bb89 100644 --- a/api/signin.go +++ b/api/signin.go @@ -1,46 +1,37 @@ package api import ( + "context" "errors" - "fmt" "net/http" "github.com/Gleipnir-Technology/nidus-sync/auth" - "github.com/go-chi/render" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/rs/zerolog/log" ) -func postSignin(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - render.Render(w, r, errRender(fmt.Errorf("Failed to parse POST form: %w", err))) - return +type reqSignin struct { + Password string `json:"password"` + Username string `json:"username"` +} +func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) { + if req.Password == "" { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty password") } - - username := r.FormValue("username") - password := r.FormValue("password") - - if password == "" || username == "" { - w.Header().Set("WWW-Authenticate-Error", "no-credentials") - http.Error(w, "invalid-credentials", http.StatusUnauthorized) - return + if req.Username == "" { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty username") } - log.Info().Str("username", username).Msg("API Signin") - _, err := auth.SigninUser(r, username, password) + log.Info().Str("username", req.Username).Msg("API Signin") + _, err := auth.SigninUser(r, req.Username, req.Password) if err != nil { if errors.Is(err, auth.InvalidCredentials{}) { - w.Header().Set("WWW-Authenticate-Error", "invalid-credentials") - http.Error(w, "invalid-credentials", http.StatusUnauthorized) - return + return "", nhttp.NewErrorStatus(http.StatusUnauthorized, "invalid credentials") } if errors.Is(err, auth.InvalidUsername{}) { - w.Header().Set("WWW-Authenticate-Error", "invalid-credentials") - http.Error(w, "invalid-credentials", http.StatusUnauthorized) - return + return "", nhttp.NewErrorStatus(http.StatusUnauthorized, "invalid credentials") } - log.Error().Err(err).Str("username", username).Msg("Login server error") - http.Error(w, "signin-server-error", http.StatusInternalServerError) - return + log.Error().Err(err).Str("username", req.Username).Msg("Login server error") + return "", nhttp.NewError("login server error") } - - http.Error(w, "", http.StatusAccepted) + return "/", nil } diff --git a/api/signup.go b/api/signup.go new file mode 100644 index 00000000..da518637 --- /dev/null +++ b/api/signup.go @@ -0,0 +1,36 @@ +package api + +import ( + "context" + "net/http" + "strings" + + "github.com/Gleipnir-Technology/nidus-sync/auth" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/rs/zerolog/log" +) +type reqSignup struct { + Username string `json:"username"` + Name string `json:"name"` + Password string `json:"password"` + Terms bool `json:"terms"` +} +func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string, *nhttp.ErrorWithStatus) { + + log.Info().Str("username", signup.Username).Str("name", signup.Name).Str("password", strings.Repeat("*", len(signup.Password))).Msg("Signup") + + if !signup.Terms { + log.Warn().Msg("Terms not agreed") + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must agree to the terms to register") + } + + user, err := auth.SignupUser(r.Context(), signup.Username, signup.Name, signup.Password) + if err != nil { + return "", nhttp.NewError("Failed to signup user", err) + } + + auth.AddUserSession(r, user) + + return "/", nil +} + diff --git a/api/sudo.go b/api/sudo.go new file mode 100644 index 00000000..69ab49d1 --- /dev/null +++ b/api/sudo.go @@ -0,0 +1,104 @@ +package api + +import ( + "context" + "fmt" + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/comms/email" + "github.com/Gleipnir-Technology/nidus-sync/comms/text" + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/html" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/rs/zerolog/log" +) + +type contentSudo struct { + ForwardEmailRMOAddress string + ForwardEmailNidusAddress string +} + +func getSudo(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSudo], *nhttp.ErrorWithStatus) { + if !user.HasRoot() { + return nil, &nhttp.ErrorWithStatus{ + Message: "You have to be a root user to access this", + Status: http.StatusForbidden, + } + } + content := contentSudo{ + ForwardEmailRMOAddress: config.ForwardEmailRMOAddress, + ForwardEmailNidusAddress: config.ForwardEmailNidusAddress, + } + return html.NewResponse("sync/sudo.html", content), nil +} + +type FormEmail struct { + Body string `schema:"emailBody"` + From string `schema:"emailFrom"` + Subject string `schema:"emailSubject"` + To string `schema:"emailTo"` +} + +func postSudoEmail(ctx context.Context, r *http.Request, u platform.User, e FormEmail) (string, *nhttp.ErrorWithStatus) { + if !u.HasRoot() { + return "", &nhttp.ErrorWithStatus{ + Message: "You must have sudo powers to do this", + Status: http.StatusForbidden, + } + } + request := email.Request{ + From: e.From, + HTML: fmt.Sprintf("
%s
", e.Body), + Sender: e.From, + Subject: e.Subject, + To: e.To, + Text: e.Body, + } + resp, err := email.Send(ctx, request) + if err != nil { + log.Warn().Err(err).Msg("Failed to send email") + } else { + log.Info().Str("id", resp.ID).Str("to", e.To).Msg("Sent Email") + } + return "/sudo", nil +} + +type FormSMS struct { + Message string `schema:"smsMessage"` + Phone string `schema:"smsPhone"` +} + +func postSudoSMS(ctx context.Context, r *http.Request, u platform.User, sms FormSMS) (string, *nhttp.ErrorWithStatus) { + if !u.HasRoot() { + return "", &nhttp.ErrorWithStatus{ + Message: "You must have sudo powers to do this", + Status: http.StatusForbidden, + } + } + id, err := text.SendText(ctx, config.VoipMSNumber, sms.Phone, sms.Message) + if err != nil { + log.Warn().Err(err).Msg("Failed to send SMS") + } else { + log.Info().Str("id", id).Msg("Sent SMS") + } + return "/sudo", nil +} + +type FormSSE struct { + OrganizationID int32 `schema:"organizationID"` + Resource string `schema:"resource"` + Type string `schema:"type"` + URIPath string `schema:"uriPath"` +} + +func postSudoSSE(ctx context.Context, r *http.Request, u platform.User, sse FormSSE) (string, *nhttp.ErrorWithStatus) { + if !u.HasRoot() { + return "", &nhttp.ErrorWithStatus{ + Message: "You must have sudo powers to do this", + Status: http.StatusForbidden, + } + } + platform.SudoEvent(sse.OrganizationID, sse.Resource, sse.Type, sse.URIPath) + return "/sudo", nil +} diff --git a/api/upload.go b/api/upload.go index 0add200a..e6f88dce 100644 --- a/api/upload.go +++ b/api/upload.go @@ -1,13 +1,17 @@ package api import ( "context" + "fmt" "net/http" "strconv" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/file" "github.com/go-chi/chi/v5" - //"github.com/rs/zerolog/log" + "github.com/rs/zerolog/log" ) func getUploadByID(ctx context.Context, r *http.Request, u platform.User, query queryParams) (*platform.UploadPoolDetail, *nhttp.ErrorWithStatus) { file_id_str := chi.URLParam(r, "id") @@ -22,3 +26,118 @@ func getUploadByID(ctx context.Context, r *http.Request, u platform.User, query } return detail, nil } + +type contentUploadList struct { + RecentUploads []platform.UploadSummary +} +type contentUploadPlaceholder struct{} + +func getUploadList(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentUploadList], *nhttp.ErrorWithStatus) { + rows, err := platform.UploadSummaryList(ctx, user.Organization) + return html.NewResponse("sync/upload-list.html", contentUploadList{ + RecentUploads: rows, + }), nhttp.NewErrorMaybe("get upload list: %w", err) +} + +type contentUploadDetail struct { + CSVFileID int32 + Organization platform.Organization + Upload platform.UploadPoolDetail +} +type contentUploadPoolList struct { + Uploads []platform.Upload +} +type contentUploadPool struct{} + +func getUploadPool(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPool], *nhttp.ErrorWithStatus) { + data := contentUploadPool{} + return html.NewResponse("sync/upload-csv-pool.html", data), nil +} + +type contentUploadPoolFlyoverCreate struct{} + +func getUploadPoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPoolFlyoverCreate], *nhttp.ErrorWithStatus) { + data := contentUploadPoolFlyoverCreate{} + return html.NewResponse("sync/upload-csv-pool-flyover.html", data), nil +} + +type contentUploadPoolCustomCreate struct{} + +func getUploadPoolCustomCreate(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPoolCustomCreate], *nhttp.ErrorWithStatus) { + data := contentUploadPoolCustomCreate{} + return html.NewResponse("sync/upload-csv-pool-custom.html", data), nil +} + +type FormUploadCommit struct{} + +func postUploadCommit(ctx context.Context, r *http.Request, u platform.User, f FormUploadCommit) (string, *nhttp.ErrorWithStatus) { + file_id_str := chi.URLParam(r, "id") + file_id_, err := strconv.ParseInt(file_id_str, 10, 32) + if err != nil { + return "", nhttp.NewError("Failed to parse file_id: %w", err) + } + err = platform.UploadCommit(ctx, u.Organization, int32(file_id_), u) + if err != nil { + return "", nhttp.NewError("Failed to mark committed: %w", err) + } + log.Debug().Int64("file_id", file_id_).Int("user_id", u.ID).Msg("Committed file") + return "/configuration/upload", nil +} + +type FormUploadDiscard struct{} + +func postUploadDiscard(ctx context.Context, r *http.Request, u platform.User, f FormUploadDiscard) (string, *nhttp.ErrorWithStatus) { + file_id_str := chi.URLParam(r, "id") + file_id_, err := strconv.ParseInt(file_id_str, 10, 32) + if err != nil { + return "", nhttp.NewError("Failed to parse file_id: %w", err) + } + err = platform.UploadDiscard(ctx, u.Organization, int32(file_id_)) + if err != nil { + return "", nhttp.NewError("Failed to mark discarded: %w", err) + } + return "/configuration/upload", nil +} + +type FormUploadPool struct{} + +func postUploadPoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User, f FormUploadPool) (string, *nhttp.ErrorWithStatus) { + // If the organization we're uploading to doesn't have a service area, we can't process the upload correctly + if !(u.Organization.HasServiceArea() || u.Organization.IsCatchall()) { + return "", nhttp.NewErrorStatus(http.StatusConflict, "Your organization does not yet have a service area") + } + uploads, err := file.SaveFileUpload(r, "csvfile", file.CollectionCSV) + if err != nil { + return "", nhttp.NewError("Failed to extract image uploads: %s", err) + } + if len(uploads) == 0 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") + } + if len(uploads) != 1 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") + } + upload := uploads[0] + saved_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypeFlyover) + if err != nil { + return "", nhttp.NewError("Failed to create new pool: %w", err) + } + return fmt.Sprintf("/configuration/upload/%d", saved_upload.ID), nil +} +func postUploadPoolCustomCreate(ctx context.Context, r *http.Request, u platform.User, f FormUploadPool) (string, *nhttp.ErrorWithStatus) { + uploads, err := file.SaveFileUpload(r, "csvfile", file.CollectionCSV) + if err != nil { + return "", nhttp.NewError("Failed to extract image uploads: %s", err) + } + if len(uploads) == 0 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") + } + if len(uploads) != 1 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") + } + upload := uploads[0] + pool_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypePoollist) + if err != nil { + return "", nhttp.NewError("Failed to create new pool: %w", err) + } + return fmt.Sprintf("/configuration/upload/%d", pool_upload.ID), nil +} diff --git a/platform/dashboard.go b/platform/dashboard.go new file mode 100644 index 00000000..fed1e484 --- /dev/null +++ b/platform/dashboard.go @@ -0,0 +1,66 @@ +package platform + +import ( + "context" + "time" + + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" +) +type ServiceRequestSummary struct { + Date time.Time + Location string + Status string +} +type contentDashboard struct { + CountTraps int + CountMosquitoSources int + CountServiceRequests int + IsSyncOngoing bool + LastSync *time.Time + RecentRequests []ServiceRequestSummary +} + +func getDashboardData(ctx context.Context, user User) (*contentDashboard, *nhttp.ErrorWithStatus) { + var lastSync *time.Time + sync, err := user.Organization.FieldseekerSyncLatest(ctx) + if err != nil { + return nil, nhttp.NewError("Failed to get syncs: %w", err) + } else if sync != nil { + lastSync = &sync.Created + } + is_syncing := user.Organization.IsSyncOngoing() + count_trap, err := user.Organization.CountTrap(ctx) + if err != nil { + return nil, nhttp.NewError("Failed to get trap count: %w", err) + } + count_source, err := user.Organization.CountTrap(ctx) + if err != nil { + return nil, nhttp.NewError("Failed to get source count: %w", err) + } + count_service, err := user.Organization.CountServiceRequest(ctx) + if err != nil { + return nil, nhttp.NewError("Failed to get service count: %w", err) + } + service_request_recent, err := user.Organization.ServiceRequestRecent(ctx) + if err != nil { + return nil, nhttp.NewError("Failed to get recent service: %w", err) + } + + requests := make([]ServiceRequestSummary, 0) + for _, r := range service_request_recent { + requests = append(requests, ServiceRequestSummary{ + Date: r.Creationdate.MustGet(), + Location: r.Reqaddr1.MustGet(), + Status: "Completed", + }) + } + content := contentDashboard{ + CountTraps: int(count_trap), + CountMosquitoSources: int(count_source), + CountServiceRequests: int(count_service), + IsSyncOngoing: is_syncing, + LastSync: lastSync, + RecentRequests: requests, + } + return &content, nil +} diff --git a/sync/cell.go b/sync/cell.go index d957bdb2..247a7e9e 100644 --- a/sync/cell.go +++ b/sync/cell.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/Gleipnir-Technology/nidus-sync/h3utils" + //"github.com/Gleipnir-Technology/nidus-sync/h3utils" "github.com/Gleipnir-Technology/nidus-sync/html" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" @@ -16,7 +16,6 @@ type contentCell struct { BreedingSources []platform.BreedingSourceSummary CellBoundary h3.CellBoundary Inspections []platform.Inspection - MapData ComponentMap Traps []platform.TrapSummary Treatments []platform.Treatment } @@ -30,10 +29,6 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (* if err != nil { return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Cannot convert provided cell to uint64") } - center, err := h3.Cell(c).LatLng() - if err != nil { - return nil, nhttp.NewError("Failed to get center: %w", err) - } boundary, err := h3.Cell(c).Boundary() if err != nil { return nil, nhttp.NewError("Failed to get boundary: %w", err) @@ -42,11 +37,17 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (* if err != nil { return nil, nhttp.NewError("Failed to get inspections by cell: %w", err) } + /* + center, err := h3.Cell(c).LatLng() + if err != nil { + return nil, nhttp.NewError("Failed to get center: %w", err) + } geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)}) if err != nil { return nil, nhttp.NewError("Failed to get boundaries: %w", err) } resolution := h3.Cell(c).Resolution() + */ sources, err := platform.BreedingSourcesByCell(ctx, user.Organization, h3.Cell(c)) if err != nil { return nil, nhttp.NewError("Failed to get sources: %w", err) @@ -64,14 +65,6 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (* BreedingSources: sources, CellBoundary: boundary, Inspections: inspections, - MapData: ComponentMap{ - Center: h3.LatLng{ - Lat: center.Lat, - Lng: center.Lng, - }, - GeoJSON: geojson, - Zoom: resolution + 5, - }, Traps: traps, Treatments: treatments, }), nil diff --git a/sync/dash.go b/sync/dash.go index 829a6d7a..bfa060ae 100644 --- a/sync/dash.go +++ b/sync/dash.go @@ -2,9 +2,7 @@ package sync import ( "context" - "html/template" "net/http" - "time" "github.com/Gleipnir-Technology/nidus-sync/html" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" @@ -15,7 +13,6 @@ import ( type contentSource struct { Inspections []platform.Inspection - MapData ComponentMap Source *platform.BreedingSourceDetail Traps []platform.TrapNearby Treatments []platform.Treatment @@ -24,21 +21,9 @@ type contentSource struct { User platform.User } type contentTrap struct { - MapData ComponentMap Trap platform.Trap User platform.User } -type contentDashboard struct { - CountTraps int - CountMosquitoSources int - CountServiceRequests int - Geo template.JS - IsSyncOngoing bool - LastSync *time.Time - MapData ComponentMap - RecentRequests []ServiceRequestSummary -} - type contentLayoutTest struct { User platform.User } @@ -54,50 +39,8 @@ func getLayoutTest(ctx context.Context, r *http.Request, user platform.User) (*h return html.NewResponse("sync/layout-test.html", contentLayoutTest{}), nil } -func getRoot(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentDashboard], *nhttp.ErrorWithStatus) { - var lastSync *time.Time - sync, err := user.Organization.FieldseekerSyncLatest(ctx) - if err != nil { - return nil, nhttp.NewError("Failed to get syncs: %w", err) - } else if sync != nil { - lastSync = &sync.Created - } - is_syncing := user.Organization.IsSyncOngoing() - count_trap, err := user.Organization.CountTrap(ctx) - if err != nil { - return nil, nhttp.NewError("Failed to get trap count: %w", err) - } - count_source, err := user.Organization.CountTrap(ctx) - if err != nil { - return nil, nhttp.NewError("Failed to get source count: %w", err) - } - count_service, err := user.Organization.CountServiceRequest(ctx) - if err != nil { - return nil, nhttp.NewError("Failed to get service count: %w", err) - } - service_request_recent, err := user.Organization.ServiceRequestRecent(ctx) - if err != nil { - return nil, nhttp.NewError("Failed to get recent service: %w", err) - } - - requests := make([]ServiceRequestSummary, 0) - for _, r := range service_request_recent { - requests = append(requests, ServiceRequestSummary{ - Date: r.Creationdate.MustGet(), - Location: r.Reqaddr1.MustGet(), - Status: "Completed", - }) - } - content := contentDashboard{ - CountTraps: int(count_trap), - CountMosquitoSources: int(count_source), - CountServiceRequests: int(count_service), - IsSyncOngoing: is_syncing, - LastSync: lastSync, - MapData: ComponentMap{}, - RecentRequests: requests, - } - return html.NewResponse("sync/authenticated.html", content), nil +func getRoot(w http.ResponseWriter, r *http.Request) { + html.RenderOrError(w, "static/gen/main.html", struct{}{}) } func getSource(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSource], *nhttp.ErrorWithStatus) { @@ -127,22 +70,8 @@ func getSource(ctx context.Context, r *http.Request, user platform.User) (*html. return nil, nhttp.NewError("Failed to get treatments: %w", err) } treatment_models := platform.ModelTreatment(treatments) - latlng, err := s.H3Cell.LatLng() - if err != nil { - return nil, nhttp.NewError("Failed to get latlng: %w", err) - } data := contentSource{ Inspections: inspections, - MapData: ComponentMap{ - Center: latlng, - //GeoJSON: - Markers: []MapMarker{ - MapMarker{ - LatLng: latlng, - }, - }, - Zoom: 13, - }, Source: s, Traps: traps, Treatments: treatments, @@ -153,12 +82,6 @@ func getSource(ctx context.Context, r *http.Request, user platform.User) (*html. return html.NewResponse("sync/source.html", data), nil } -func getStadia(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentDashboard], *nhttp.ErrorWithStatus) { - data := contentDashboard{ - MapData: ComponentMap{}, - } - return html.NewResponse("sync/stadia.html", data), nil -} func getTemplateTest(w http.ResponseWriter, r *http.Request) { html.RenderOrError(w, "sync/template-test.html", nil) } @@ -175,21 +98,13 @@ func getTrap(ctx context.Context, r *http.Request, user platform.User) (*html.Re if err != nil { return nil, nhttp.NewError("Failed to get trap: %w", err) } + /* latlng, err := t.H3Cell.LatLng() if err != nil { return nil, nhttp.NewError("Failed to get latlng: %w", err) } + */ data := contentTrap{ - MapData: ComponentMap{ - Center: latlng, - //GeoJSON: - Markers: []MapMarker{ - MapMarker{ - LatLng: latlng, - }, - }, - Zoom: 13, - }, Trap: *t, User: user, } diff --git a/sync/handler.go b/sync/handler.go deleted file mode 100644 index b0d010b7..00000000 --- a/sync/handler.go +++ /dev/null @@ -1,97 +0,0 @@ -package sync - -import ( - "context" - "net/http" - - "github.com/Gleipnir-Technology/nidus-sync/auth" - "github.com/Gleipnir-Technology/nidus-sync/html" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/gorilla/schema" - "github.com/rs/zerolog/log" -) - -var decoder = schema.NewDecoder() - -type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User) (*html.Response[T], *nhttp.ErrorWithStatus) -type wrappedHandler func(http.ResponseWriter, *http.Request) -type contentAuthenticated[T any] struct { - C T - Config html.ContentConfig - Organization platform.Organization - URL html.ContentURL - User platform.User -} - -// w http.ResponseWriter, r *http.Request, u platform.User) { -func authenticatedHandler[T any](f handlerFunctionGet[T]) http.Handler { - return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { - ctx := r.Context() - resp, e := f(ctx, r, 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, resp.Template, contentAuthenticated[T]{ - C: resp.Content, - Config: html.NewContentConfig(), - Organization: u.Organization, - URL: html.NewContentURL(), - User: u, - }) - }) -} - -type handlerFunctionPost[T any] func(context.Context, *http.Request, platform.User, T) (string, *nhttp.ErrorWithStatus) - -func authenticatedHandlerPost[T any](f handlerFunctionPost[T]) http.Handler { - return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { - err := r.ParseForm() - 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 - } - 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 platform.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 - } - http.Redirect(w, r, path, http.StatusFound) - }) -} diff --git a/sync/routes.go b/sync/routes.go index c04ae8a9..360d3bf7 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -2,7 +2,6 @@ package sync import ( "github.com/Gleipnir-Technology/nidus-sync/api" - "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/static" "github.com/go-chi/chi/v5" ) @@ -31,66 +30,13 @@ func Router() chi.Router { r.Get("/privacy", getPrivacy) r.Get("/qr-code/report/{code}", getQRCodeReport) r.Get("/qr-code/mailer/{code}", getQRCodeMailer) - r.Get("/signin", getSignin) - r.Post("/signin", postSignin) - r.Get("/signup", getSignup) - r.Post("/signup", postSignup) r.Get("/template-test", getTemplateTest) // Authenticated endpoints r.Route("/api", api.AddRoutes) - r.Method("GET", "/", authenticatedHandler(getRoot)) - r.Method("GET", "/communication", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/integration", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/integration/arcgis", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/organization", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/pesticide", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/pesticide/add", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/upload", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/upload/pool", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/upload/pool/custom", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/upload/pool/flyover", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/upload/{id}", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/user", authenticatedHandler(getRoot)) - r.Method("GET", "/configuration/user/add", authenticatedHandler(getRoot)) - r.Method("GET", "/intelligence", authenticatedHandler(getRoot)) - r.Method("GET", "/operations", authenticatedHandler(getRoot)) - r.Method("GET", "/oauth/refresh/arcgis", authenticatedHandler(getRoot)) - r.Method("GET", "/planning", authenticatedHandler(getRoot)) - r.Method("GET", "/review", authenticatedHandler(getRoot)) - r.Method("GET", "/review/pool", authenticatedHandler(getRoot)) - r.Method("GET", "/review/site", authenticatedHandler(getRoot)) - - r.Method("GET", "/admin", authenticatedHandler(getAdminDash)) - r.Method("GET", "/cell/{cell}", authenticatedHandler(getCellDetails)) - r.Method("POST", "/configuration/integration/arcgis", authenticatedHandlerPost(postConfigurationIntegrationArcgis)) - r.Method("POST", "/configuration/upload/pool/flyover", authenticatedHandlerPostMultipart(postUploadPoolFlyoverCreate)) - r.Method("POST", "/configuration/upload/pool/custom", authenticatedHandlerPostMultipart(postUploadPoolCustomCreate)) - r.Method("POST", "/configuration/upload/{id}/commit", authenticatedHandlerPost(postUploadCommit)) - r.Method("POST", "/configuration/upload/{id}/discard", authenticatedHandlerPost(postUploadDiscard)) - r.Method("GET", "/download", authenticatedHandler(getDownloadList)) - r.Method("GET", "/layout-test", authenticatedHandler(getLayoutTest)) - r.Method("GET", "/message", authenticatedHandler(getMessageList)) - r.Method("GET", "/notification", authenticatedHandler(getNotificationList)) - r.Method("GET", "/parcel", authenticatedHandler(getParcel)) - 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", "/signout", auth.NewEnsureAuth(getSignout)) - 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("POST", "/sudo/sse", authenticatedHandlerPost(postSudoSSE)) - r.Method("GET", "/trap/{globalid}", authenticatedHandler(getTrap)) - r.Method("GET", "/text/{destination}", authenticatedHandler(getTextMessages)) - r.Method("GET", "/tile/gps", auth.NewEnsureAuth(getTileGPS)) + r.Get("/", getRoot) + r.Get("/_/*", getRoot) static.AddStaticRoute(r, "/static") return r diff --git a/sync/signin.go b/sync/signin.go index 6981a50f..358def6a 100644 --- a/sync/signin.go +++ b/sync/signin.go @@ -3,7 +3,6 @@ package sync import ( "errors" "net/http" - "strings" "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/config" @@ -16,7 +15,6 @@ type contentSignin struct { InvalidCredentials bool Next string } -type contentSignup struct{} func getSignin(w http.ResponseWriter, r *http.Request) { errorCode := r.URL.Query().Get("error") @@ -29,15 +27,6 @@ func getSignout(w http.ResponseWriter, r *http.Request, user platform.User) { http.Redirect(w, r, "/signin", http.StatusFound) } -func getSignup(w http.ResponseWriter, r *http.Request) { - data := contentSignup{} - html.RenderOrError(w, "sync/signup.html", contentUnauthenticated[contentSignup]{ - C: data, - Config: html.NewContentConfig(), - URL: html.NewContentURL(), - }) -} - func postSignin(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { respondError(w, "Could not parse form", err, http.StatusBadRequest) @@ -70,36 +59,6 @@ func postSignin(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, location, 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) -} - type contentUnauthenticated[T any] struct { C T Config html.ContentConfig diff --git a/sync/sudo.go b/sync/sudo.go index 1b9f2190..1ca2a85e 100644 --- a/sync/sudo.go +++ b/sync/sudo.go @@ -1,104 +1 @@ package sync - -import ( - "context" - "fmt" - "net/http" - - "github.com/Gleipnir-Technology/nidus-sync/comms/email" - "github.com/Gleipnir-Technology/nidus-sync/comms/text" - "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/html" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/rs/zerolog/log" -) - -type contentSudo struct { - ForwardEmailRMOAddress string - ForwardEmailNidusAddress string -} - -func getSudo(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSudo], *nhttp.ErrorWithStatus) { - if !user.HasRoot() { - return nil, &nhttp.ErrorWithStatus{ - Message: "You have to be a root user to access this", - Status: http.StatusForbidden, - } - } - content := contentSudo{ - ForwardEmailRMOAddress: config.ForwardEmailRMOAddress, - ForwardEmailNidusAddress: config.ForwardEmailNidusAddress, - } - return html.NewResponse("sync/sudo.html", content), nil -} - -type FormEmail struct { - Body string `schema:"emailBody"` - From string `schema:"emailFrom"` - Subject string `schema:"emailSubject"` - To string `schema:"emailTo"` -} - -func postSudoEmail(ctx context.Context, r *http.Request, u platform.User, e FormEmail) (string, *nhttp.ErrorWithStatus) { - if !u.HasRoot() { - return "", &nhttp.ErrorWithStatus{ - Message: "You must have sudo powers to do this", - Status: http.StatusForbidden, - } - } - request := email.Request{ - From: e.From, - HTML: fmt.Sprintf("%s
", e.Body), - Sender: e.From, - Subject: e.Subject, - To: e.To, - Text: e.Body, - } - resp, err := email.Send(ctx, request) - if err != nil { - log.Warn().Err(err).Msg("Failed to send email") - } else { - log.Info().Str("id", resp.ID).Str("to", e.To).Msg("Sent Email") - } - return "/sudo", nil -} - -type FormSMS struct { - Message string `schema:"smsMessage"` - Phone string `schema:"smsPhone"` -} - -func postSudoSMS(ctx context.Context, r *http.Request, u platform.User, sms FormSMS) (string, *nhttp.ErrorWithStatus) { - if !u.HasRoot() { - return "", &nhttp.ErrorWithStatus{ - Message: "You must have sudo powers to do this", - Status: http.StatusForbidden, - } - } - id, err := text.SendText(ctx, config.VoipMSNumber, sms.Phone, sms.Message) - if err != nil { - log.Warn().Err(err).Msg("Failed to send SMS") - } else { - log.Info().Str("id", id).Msg("Sent SMS") - } - return "/sudo", nil -} - -type FormSSE struct { - OrganizationID int32 `schema:"organizationID"` - Resource string `schema:"resource"` - Type string `schema:"type"` - URIPath string `schema:"uriPath"` -} - -func postSudoSSE(ctx context.Context, r *http.Request, u platform.User, sse FormSSE) (string, *nhttp.ErrorWithStatus) { - if !u.HasRoot() { - return "", &nhttp.ErrorWithStatus{ - Message: "You must have sudo powers to do this", - Status: http.StatusForbidden, - } - } - platform.SudoEvent(sse.OrganizationID, sse.Resource, sse.Type, sse.URIPath) - return "/sudo", nil -} diff --git a/sync/types.go b/sync/types.go index e00c4c6d..b0429096 100644 --- a/sync/types.go +++ b/sync/types.go @@ -1,20 +1,12 @@ package sync import ( - "time" - "github.com/uber/h3-go/v4" ) type MapMarker struct { LatLng h3.LatLng } -type ComponentMap struct { - Center h3.LatLng - GeoJSON interface{} - Markers []MapMarker - Zoom int -} type ContentMockURLs struct { Dispatch string DispatchResults string @@ -43,8 +35,3 @@ type Link struct { Href string Title string } -type ServiceRequestSummary struct { - Date time.Time - Location string - Status string -} diff --git a/sync/upload.go b/sync/upload.go index e921c4b4..45f98b32 100644 --- a/sync/upload.go +++ b/sync/upload.go @@ -1,131 +1,4 @@ package sync import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/Gleipnir-Technology/nidus-sync/html" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/Gleipnir-Technology/nidus-sync/platform/file" - "github.com/go-chi/chi/v5" - "github.com/rs/zerolog/log" ) - -type contentUploadList struct { - RecentUploads []platform.UploadSummary -} -type contentUploadPlaceholder struct{} - -func getUploadList(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentUploadList], *nhttp.ErrorWithStatus) { - rows, err := platform.UploadSummaryList(ctx, user.Organization) - return html.NewResponse("sync/upload-list.html", contentUploadList{ - RecentUploads: rows, - }), nhttp.NewErrorMaybe("get upload list: %w", err) -} - -type contentUploadDetail struct { - CSVFileID int32 - Organization platform.Organization - Upload platform.UploadPoolDetail -} -type contentUploadPoolList struct { - Uploads []platform.Upload -} -type contentUploadPool struct{} - -func getUploadPool(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPool], *nhttp.ErrorWithStatus) { - data := contentUploadPool{} - return html.NewResponse("sync/upload-csv-pool.html", data), nil -} - -type contentUploadPoolFlyoverCreate struct{} - -func getUploadPoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPoolFlyoverCreate], *nhttp.ErrorWithStatus) { - data := contentUploadPoolFlyoverCreate{} - return html.NewResponse("sync/upload-csv-pool-flyover.html", data), nil -} - -type contentUploadPoolCustomCreate struct{} - -func getUploadPoolCustomCreate(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPoolCustomCreate], *nhttp.ErrorWithStatus) { - data := contentUploadPoolCustomCreate{} - return html.NewResponse("sync/upload-csv-pool-custom.html", data), nil -} - -type FormUploadCommit struct{} - -func postUploadCommit(ctx context.Context, r *http.Request, u platform.User, f FormUploadCommit) (string, *nhttp.ErrorWithStatus) { - file_id_str := chi.URLParam(r, "id") - file_id_, err := strconv.ParseInt(file_id_str, 10, 32) - if err != nil { - return "", nhttp.NewError("Failed to parse file_id: %w", err) - } - err = platform.UploadCommit(ctx, u.Organization, int32(file_id_), u) - if err != nil { - return "", nhttp.NewError("Failed to mark committed: %w", err) - } - log.Debug().Int64("file_id", file_id_).Int("user_id", u.ID).Msg("Committed file") - return "/configuration/upload", nil -} - -type FormUploadDiscard struct{} - -func postUploadDiscard(ctx context.Context, r *http.Request, u platform.User, f FormUploadDiscard) (string, *nhttp.ErrorWithStatus) { - file_id_str := chi.URLParam(r, "id") - file_id_, err := strconv.ParseInt(file_id_str, 10, 32) - if err != nil { - return "", nhttp.NewError("Failed to parse file_id: %w", err) - } - err = platform.UploadDiscard(ctx, u.Organization, int32(file_id_)) - if err != nil { - return "", nhttp.NewError("Failed to mark discarded: %w", err) - } - return "/configuration/upload", nil -} - -type FormUploadPool struct{} - -func postUploadPoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User, f FormUploadPool) (string, *nhttp.ErrorWithStatus) { - // If the organization we're uploading to doesn't have a service area, we can't process the upload correctly - if !(u.Organization.HasServiceArea() || u.Organization.IsCatchall()) { - return "", nhttp.NewErrorStatus(http.StatusConflict, "Your organization does not yet have a service area") - } - uploads, err := file.SaveFileUpload(r, "csvfile", file.CollectionCSV) - if err != nil { - return "", nhttp.NewError("Failed to extract image uploads: %s", err) - } - if len(uploads) == 0 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") - } - if len(uploads) != 1 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") - } - upload := uploads[0] - saved_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypeFlyover) - if err != nil { - return "", nhttp.NewError("Failed to create new pool: %w", err) - } - return fmt.Sprintf("/configuration/upload/%d", saved_upload.ID), nil -} -func postUploadPoolCustomCreate(ctx context.Context, r *http.Request, u platform.User, f FormUploadPool) (string, *nhttp.ErrorWithStatus) { - uploads, err := file.SaveFileUpload(r, "csvfile", file.CollectionCSV) - if err != nil { - return "", nhttp.NewError("Failed to extract image uploads: %s", err) - } - if len(uploads) == 0 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") - } - if len(uploads) != 1 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") - } - upload := uploads[0] - pool_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypePoollist) - if err != nil { - return "", nhttp.NewError("Failed to create new pool: %w", err) - } - return fmt.Sprintf("/configuration/upload/%d", pool_upload.ID), nil -}