diff --git a/comms/email.go b/comms/email.go new file mode 100644 index 00000000..a31c145a --- /dev/null +++ b/comms/email.go @@ -0,0 +1,58 @@ +package comms + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/rs/zerolog/log" +) + +type AttachmentRequest struct { + Filename string `json:"filename"` + Content string `json:"content"` +} + +type EmailRequest struct { + From string `json:"from"` + To string `json:"to"` + CC []string `json:"cc,omitempty"` + BCC []string `json:"bcc,omitempty"` + Subject string `json:"subject"` + Text string `json:"text"` + HTML string `json:"html,omitempty"` + Attachments []AttachmentRequest `json:"attachments,omitempty"` + Sender string `json:"sender"` + ReplyTo string `json:"replyTo,omitempty"` + InReplyTo string `json:"inReplyTo,omitempty"` + References []string `json:"references,omitempty"` +} + +type EmailResponse struct { + Message string `json:"message"` +} + +func SendEmail(email EmailRequest) error { + url := "https://api.forwardemail.net/v1/emails" + + payload, err := json.Marshal(email) + if err != nil { + return fmt.Errorf("Failed to marshal email request: %w", err) + } + //payload := strings.NewReader("{\n \"from\": \"\",\n \"to\": \"\",\n \"cc\": \"\",\n \"bcc\": \"\",\n \"subject\": \"\",\n \"text\": \"\",\n \"html\": \"\",\n \"attachments\": [\n {}\n ],\n \"sender\": \"\",\n \"replyTo\": \"\",\n \"inReplyTo\": \"\",\n \"references\": \"\",\n \"attachDataUrls\": true,\n \"watchHtml\": \"\",\n \"amp\": \"\",\n \"icalEvent\": {},\n \"alternatives\": [\n {}\n ],\n \"encoding\": \"\",\n \"raw\": \"\",\n \"textEncoding\": \"quoted-printable\",\n \"priority\": \"high\",\n \"headers\": {\"ANY_ADDITIONAL_PROPERTY\": \"anything\"},\n \"messageId\": \"\",\n \"date\": \"\",\n \"list\": {},\n \"requireTLS\": true\n}") + + req, _ := http.NewRequest("POST", url, bytes.NewReader(payload)) + req.SetBasicAuth(config.ForwardEmailAPIToken, "") + req.Header.Add("Content-Type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + log.Info().Str("status", res.Status).Str("request_body", string(payload)).Str("response_body", string(body)).Msg("Attempted to send email") + return nil +} diff --git a/comms/sms.go b/comms/sms.go new file mode 100644 index 00000000..1a5ad94a --- /dev/null +++ b/comms/sms.go @@ -0,0 +1,58 @@ +package comms + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/rs/zerolog/log" +) + +var VOIP_MS_API = "https://voip.ms/api/v1/rest.php" + +type SendSMSResponse struct { + Status string `json:"status"` + SMS int `json:"sms"` +} +func SendSMS(to string, content string) error { + if len(content) > 160 { + return errors.New("Message content is more than 160 characters") + } + params := url.Values{} + params.Add("api_password", config.VoipMSPassword) + params.Add("api_username", config.VoipMSUsername) + params.Add("method", "sendSMS") + params.Add("did", config.VoipMSNumber) + params.Add("dst", to) + params.Add("message", content) + // Construct the URL with query parameters + full_url := VOIP_MS_API + "?" + params.Encode() + + // Make the HTTP request + resp, err := http.Get(full_url) + if err != nil { + log.Warn().Err(err).Str("url", full_url).Msg("Failed to make request to Voip.MS") + return fmt.Errorf("Error making request: %w", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Warn().Err(err).Str("url", full_url).Msg("Failed to read Voip.MS response body") + return fmt.Errorf("Failed to read response: %w", err) + } + log.Info().Str("response", string(body)).Msg("Response from Voip.MS") + + // Parse the JSON response + var response SendSMSResponse + err = json.Unmarshal(body, &response) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON response: %w", err) + } + return nil +} diff --git a/config/config.go b/config/config.go index 1b6ac507..e4a7879c 100644 --- a/config/config.go +++ b/config/config.go @@ -7,7 +7,26 @@ import ( "strconv" ) -var Bind, ClientID, ClientSecret, Environment, FilesDirectoryPublic, FilesDirectoryUser, FieldseekerSchemaDirectory, MapboxToken, PGDSN, URLReport, URLSync, URLTegola string +var ( + Bind string + ClientID string + ClientSecret string + Environment string + FilesDirectoryPublic string + FilesDirectoryUser string + FieldseekerSchemaDirectory string + ForwardEmailAPIToken string + ForwardEmailReportPassword string + ForwardEmailReportUsername string + MapboxToken string + PGDSN string + URLReport string + URLSync string + URLTegola string + VoipMSPassword string + VoipMSNumber string + VoipMSUsername string +) // Build the ArcGIS authorization URL with PKCE func BuildArcGISAuthURL(clientID string) string { @@ -43,6 +62,10 @@ func MakeURLSync(path string) string { } func Parse() error { + Bind = os.Getenv("BIND") + if Bind == "" { + Bind = ":9001" + } ClientID = os.Getenv("ARCGIS_CLIENT_ID") if ClientID == "" { return fmt.Errorf("You must specify a non-empty ARCGIS_CLIENT_ID") @@ -51,22 +74,6 @@ func Parse() error { if ClientSecret == "" { return fmt.Errorf("You must specify a non-empty ARCGIS_CLIENT_SECRET") } - URLReport = os.Getenv("URL_REPORT") - if URLReport == "" { - return fmt.Errorf("You must specify a non-empty URL_REPORT") - } - URLSync = os.Getenv("URL_SYNC") - if URLSync == "" { - return fmt.Errorf("You must specify a non-empty URL_SYNC") - } - URLTegola = os.Getenv("URL_TEGOLA") - if URLTegola == "" { - return fmt.Errorf("You must specify a non-empty URL_TEGOLA") - } - Bind = os.Getenv("BIND") - if Bind == "" { - Bind = ":9001" - } Environment = os.Getenv("ENVIRONMENT") if Environment == "" { return fmt.Errorf("You must specify a non-empty ENVIRONMENT") @@ -74,14 +81,6 @@ func Parse() error { if !(Environment == "PRODUCTION" || Environment == "DEVELOPMENT") { return fmt.Errorf("ENVIRONMENT should be either DEVELOPMENT or PRODUCTION") } - MapboxToken = os.Getenv("MAPBOX_TOKEN") - if MapboxToken == "" { - return fmt.Errorf("You must specify a non-empty MAPBOX_TOKEN") - } - PGDSN = os.Getenv("POSTGRES_DSN") - if PGDSN == "" { - return fmt.Errorf("You must specify a non-empty POSTGRES_DSN") - } FieldseekerSchemaDirectory = os.Getenv("FIELDSEEKER_SCHEMA_DIRECTORY") if FieldseekerSchemaDirectory == "" { return fmt.Errorf("You must specify a non-empty FIELDSEEKER_SCHEMA_DIRECTORY") @@ -94,6 +93,50 @@ func Parse() error { if FilesDirectoryUser == "" { return fmt.Errorf("You must specify a non-empty FILES_DIRECTORY_USER") } + ForwardEmailAPIToken = os.Getenv("FORWARDEMAIL_API_TOKEN") + if ForwardEmailAPIToken == "" { + return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_API_TOKEN") + } + ForwardEmailReportUsername = os.Getenv("FORWARDEMAIL_REPORT_USERNAME") + if ForwardEmailReportUsername == "" { + return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_REPORT_USERNAME") + } + ForwardEmailReportPassword = os.Getenv("FORWARDEMAIL_REPORT_PASSWORD") + if ForwardEmailReportPassword == "" { + return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_REPORT_PASSWORD") + } + MapboxToken = os.Getenv("MAPBOX_TOKEN") + if MapboxToken == "" { + return fmt.Errorf("You must specify a non-empty MAPBOX_TOKEN") + } + PGDSN = os.Getenv("POSTGRES_DSN") + if PGDSN == "" { + return fmt.Errorf("You must specify a non-empty POSTGRES_DSN") + } + URLReport = os.Getenv("URL_REPORT") + if URLReport == "" { + return fmt.Errorf("You must specify a non-empty URL_REPORT") + } + URLSync = os.Getenv("URL_SYNC") + if URLSync == "" { + return fmt.Errorf("You must specify a non-empty URL_SYNC") + } + URLTegola = os.Getenv("URL_TEGOLA") + if URLTegola == "" { + return fmt.Errorf("You must specify a non-empty URL_TEGOLA") + } + VoipMSNumber = os.Getenv("VOIPMS_NUMBER") + if VoipMSNumber == "" { + return fmt.Errorf("You must specify a non-empty VOIPMS_NUMBER") + } + VoipMSPassword = os.Getenv("VOIPMS_PASSWORD") + if VoipMSPassword == "" { + return fmt.Errorf("You must specify a non-empty VOIPMS_PASSWORD") + } + VoipMSUsername = os.Getenv("VOIPMS_USERNAME") + if VoipMSUsername == "" { + return fmt.Errorf("You must specify a non-empty VOIPMS_USERNAME") + } return nil } diff --git a/public-report/endpoint.go b/public-report/endpoint.go index 35bff222..58cfab0f 100644 --- a/public-report/endpoint.go +++ b/public-report/endpoint.go @@ -1,14 +1,10 @@ package publicreport import ( - "fmt" "net/http" - "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/htmlpage" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/um" ) type ContextRegisterNotificationsComplete struct { @@ -29,52 +25,6 @@ func getRoot(w http.ResponseWriter, r *http.Request) { ) } -func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) { - report := r.URL.Query().Get("report") - htmlpage.RenderOrError( - w, - RegisterNotificationsComplete, - ContextRegisterNotificationsComplete{ - ReportID: report, - }, - ) -} -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) - } -} - // 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") diff --git a/public-report/quick.go b/public-report/quick.go index e076d425..ef686783 100644 --- a/public-report/quick.go +++ b/public-report/quick.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/nidus-sync/comms" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" @@ -46,6 +47,16 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) { }, ) } +func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) { + report := r.URL.Query().Get("report") + htmlpage.RenderOrError( + w, + RegisterNotificationsComplete, + ContextRegisterNotificationsComplete{ + ReportID: report, + }, + ) +} func postQuick(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(32 << 10) // 32 MB buffer if err != nil { @@ -134,3 +145,57 @@ func postQuick(w http.ResponseWriter, r *http.Request) { tx.Commit(ctx) 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 + } + if email == "" && phone == "" { + http.Redirect(w, r, fmt.Sprintf("/quick-submit-complete?report=%s", report_id), http.StatusFound) + 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 email != "" { + comms.SendEmail(comms.EmailRequest{ + From: "website@mosquitoes.online", + To: email, + Subject: "test email", + Text: "This is just testing that I can send email", + }) + } + if phone != "" { + err := comms.SendSMS(phone, "testing 1 2 3") + if err != nil { + log.Error().Err(err).Msg("Failed to send SMS") + } + } + 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) + } +} +