Move all POST endpoints to the API

This commit is contained in:
Eli Ribble 2026-03-27 06:08:55 -07:00
parent 3ff7ff05ab
commit d7c07fc65f
No known key found for this signature in database
19 changed files with 466 additions and 641 deletions

146
api/configuration.go Normal file
View file

@ -0,0 +1,146 @@
package api
import (
"context"
"net/http"
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"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 contentConfigurationRoot struct{}
func getConfigurationRoot(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentConfigurationRoot], *nhttp.ErrorWithStatus) {
return html.NewResponse("sync/configuration/root.html", contentConfigurationRoot{}), nil
}
type contentSettingOrganization struct {
Organization platform.Organization
}
type contentSettingIntegration struct {
ArcGISAccount *models.ArcgisAccount
ArcGISOAuth *models.ArcgisOauthToken
ServiceMaps []*models.ArcgisServiceMap
}
func getConfigurationOrganization(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentSettingOrganization], *nhttp.ErrorWithStatus) {
/*
var district contentDistrict
district, err = bob.One[contentDistrict](ctx, db.PGInstance.BobDB, psql.Select(
sm.From("import.district"),
sm.Columns(
"address",
"agency",
"area_4326_sqm",
"city1",
"city2",
"contact",
"fax1",
"general_mg",
"gid",
"phone1",
"phone2",
"postal_c_1",
"website",
psql.F("ST_AsGeoJSON", "centroid_4326"),
psql.F("ST_XMin", "extent_4326"),
psql.F("ST_YMin", "extent_4326"),
psql.F("ST_XMax", "extent_4326"),
psql.F("ST_YMax", "extent_4326"),
),
sm.Where(psql.Quote("gid").EQ(psql.Arg(gid))),
), scan.StructMapper[contentDistrict]())
if err != nil {
respondError(w, "Failed to get extents", err, http.StatusInternalServerError)
return
}
*/
data := contentSettingOrganization{
Organization: u.Organization,
}
return html.NewResponse("sync/configuration/organization.html", data), nil
}
func getConfigurationIntegration(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentSettingIntegration], *nhttp.ErrorWithStatus) {
oauth, err := platform.GetOAuthForUser(ctx, u)
if err != nil {
return nil, nhttp.NewError("Failed to get oauth: %w", err)
}
data := contentSettingIntegration{
ArcGISOAuth: oauth,
}
return html.NewResponse("sync/configuration/integration.html", data), nil
}
func getConfigurationIntegrationArcgis(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentSettingIntegration], *nhttp.ErrorWithStatus) {
oauth, err := platform.GetOAuthForUser(ctx, u)
if err != nil {
return nil, nhttp.NewError("Failed to get oauth: %w", err)
}
var account *models.ArcgisAccount
var service_maps []*models.ArcgisServiceMap
account_id := u.Organization.ArcgisAccountID()
if account_id != "" {
account, err = models.FindArcgisAccount(ctx, db.PGInstance.BobDB, account_id)
if err != nil {
return nil, nhttp.NewError("Failed to get arcgis: %w", err)
}
service_maps, err = models.ArcgisServiceMaps.Query(
models.SelectWhere.ArcgisServiceMaps.AccountID.EQ(account.ID),
).All(ctx, db.PGInstance.BobDB)
if err != nil {
return nil, nhttp.NewError("Failed to get map services: %w", err)
}
}
data := contentSettingIntegration{
ArcGISAccount: account,
ArcGISOAuth: oauth,
ServiceMaps: service_maps,
}
return html.NewResponse("sync/configuration/integration-arcgis.html", data), nil
}
type contentSettingPlaceholder struct{}
func getConfigurationPesticide(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/pesticide.html", content), nil
}
func getConfigurationPesticideAdd(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/pesticide-add.html", content), nil
}
func getConfigurationUserAdd(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/user-add.html", content), nil
}
func getConfigurationUserList(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/user-list.html", content), nil
}
type formArcgisConfiguration struct {
MapService *string `schema:"map-service"`
}
func postConfigurationIntegrationArcgis(ctx context.Context, r *http.Request, u platform.User, f formArcgisConfiguration) (string, *nhttp.ErrorWithStatus) {
if f.MapService != nil {
_, err := psql.Update(
um.Table("organization"),
um.SetCol("arcgis_map_service_id").ToArg(f.MapService),
um.Where(psql.Quote("id").EQ(psql.Arg(u.Organization.ID))),
).Exec(ctx, db.PGInstance.BobDB)
if err != nil {
return "", nhttp.NewError("Failed to update map service config: %w", err)
}
log.Info().Str("map-service", *f.MapService).Int32("org-id", u.Organization.ID).Msg("changed map service")
} else {
log.Info().Msg("no map service")
}
return "/configuration/integration/arcgis", nil
}

View file

@ -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{

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

36
api/signup.go Normal file
View file

@ -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
}

104
api/sudo.go Normal file
View file

@ -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("<html><p>%s</p></html>", 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
}

View file

@ -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
}