Compare commits
10 Commits
0918affe9a
...
5e737ebdd3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e737ebdd3 | ||
|
|
59514f721a | ||
|
|
f6029f4a66 | ||
|
|
66300be495 | ||
|
|
a92e836cc1 | ||
|
|
97f25814a1 | ||
|
|
1b91b72ffb | ||
|
|
85e49c9e9f | ||
|
|
9444bab05f | ||
|
|
ae3a484139 |
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM docker.io/library/golang:1.23-alpine AS builder
|
FROM docker.io/library/golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
@@ -26,4 +26,4 @@ COPY web ./web
|
|||||||
|
|
||||||
EXPOSE 5001
|
EXPOSE 5001
|
||||||
|
|
||||||
CMD ["./admin-panel"]
|
CMD ["./admin-panel"]
|
||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/rideaware/admin-panel/internal/config"
|
"github.com/rideaware/admin-panel/internal/config"
|
||||||
"github.com/rideaware/admin-panel/internal/database"
|
"github.com/rideaware/admin-panel/internal/database"
|
||||||
@@ -17,11 +18,22 @@ import (
|
|||||||
// authenticated routes, and starts the HTTP server on the configured port.
|
// authenticated routes, and starts the HTTP server on the configured port.
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// Set Gin mode based on environment (default to release)
|
||||||
|
if os.Getenv("GIN_MODE") == "" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
middleware.Init()
|
middleware.Init()
|
||||||
database.Init(cfg)
|
database.Init(cfg)
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
router := gin.Default()
|
router := gin.New()
|
||||||
|
router.Use(gin.Logger())
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
|
// Trust only localhost proxy in production
|
||||||
|
router.SetTrustedProxies([]string{"127.0.0.1", "localhost", "::1"})
|
||||||
|
|
||||||
router.LoadHTMLGlob("web/templates/*.html")
|
router.LoadHTMLGlob("web/templates/*.html")
|
||||||
router.Static("/static", "web/static")
|
router.Static("/static", "web/static")
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/rideaware/admin-panel
|
module github.com/rideaware/admin-panel
|
||||||
|
|
||||||
go 1.23
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.24.10
|
toolchain go1.24.10
|
||||||
|
|
||||||
@@ -9,8 +9,7 @@ require (
|
|||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/wneessen/go-mail v0.4.0
|
golang.org/x/crypto v0.36.0
|
||||||
golang.org/x/crypto v0.14.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -33,9 +32,9 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/net v0.10.0 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
24
go.sum
24
go.sum
@@ -23,7 +23,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
|
|||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -70,26 +69,23 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/wneessen/go-mail v0.4.0 h1:Oo4HLIV8My7G9JuZkoOX6eipXQD+ACvIqURYeIzUc88=
|
|
||||||
github.com/wneessen/go-mail v0.4.0/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/smtp"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rideaware/admin-panel/internal/config"
|
"github.com/rideaware/admin-panel/internal/config"
|
||||||
"github.com/rideaware/admin-panel/internal/database"
|
"github.com/rideaware/admin-panel/internal/database"
|
||||||
|
|
||||||
"github.com/wneessen/go-mail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database.
|
// SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database.
|
||||||
// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
|
// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
|
||||||
// - If retrieving subscribers fails: returns "Failed to retrieve subscribers" and the error.
|
|
||||||
// - If no subscribers are found: returns "No subscribers found." and nil.
|
|
||||||
// - If sending to a specific subscriber fails: returns "Failed to send to <email>" and nil.
|
|
||||||
// - On success: returns "Email has been sent to all subscribers." and nil.
|
|
||||||
// Note: logging the newsletter entry in the database is attempted after sending and any logging failure is non-fatal.
|
|
||||||
func SendUpdate(subject, body string) (string, error) {
|
func SendUpdate(subject, body string) (string, error) {
|
||||||
subscribers, err := database.GetAllEmails()
|
subscribers, err := database.GetAllEmails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -28,6 +22,7 @@ func SendUpdate(subject, body string) (string, error) {
|
|||||||
if len(subscribers) == 0 {
|
if len(subscribers) == 0 {
|
||||||
return "No subscribers found.", nil
|
return "No subscribers found.", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var succeeded, failed int
|
var succeeded, failed int
|
||||||
for _, email := range subscribers {
|
for _, email := range subscribers {
|
||||||
if send(subject, body, email) {
|
if send(subject, body, email) {
|
||||||
@@ -36,79 +31,119 @@ func SendUpdate(subject, body string) (string, error) {
|
|||||||
failed++
|
failed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.LogNewsletter(subject, body); err != nil {
|
if err := database.LogNewsletter(subject, body); err != nil {
|
||||||
log.Printf("Error logging newsletter: %v", err)
|
log.Printf("Error logging newsletter: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if failed == 0 {
|
if failed == 0 {
|
||||||
return fmt.Sprintf("Email sent to all %d subscribers.", succeeded), nil
|
return fmt.Sprintf("Email sent to all %d subscribers.", succeeded), nil
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Sent to %d/%d subscribers; %d failed.", succeeded, succeeded+failed, failed), nil
|
return fmt.Sprintf("Sent to %d/%d subscribers; %d failed.", succeeded, succeeded+failed, failed), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// send constructs and sends an HTML newsletter update to the specified recipient using the current SMTP configuration.
|
// send constructs and sends an HTML newsletter update to the specified recipient
|
||||||
// It embeds an unsubscribe link for the recipient and returns true if the message was sent successfully, false if client creation, message setup, or sending fails.
|
|
||||||
func send(subject, body, recipient string) bool {
|
func send(subject, body, recipient string) bool {
|
||||||
cfg := config.Current
|
cfg := config.Current
|
||||||
|
|
||||||
client, err := mail.NewClient(
|
log.Printf("Attempting to send email to %s via %s:%d", recipient, cfg.SMTPServer, cfg.SMTPPort)
|
||||||
cfg.SMTPServer,
|
|
||||||
mail.WithPort(cfg.SMTPPort),
|
|
||||||
mail.WithSMTPAuth(mail.SMTPAuthPlain),
|
|
||||||
mail.WithUsername(cfg.SMTPUser),
|
|
||||||
mail.WithPassword(cfg.SMTPPassword),
|
|
||||||
mail.WithTimeout(10*time.Second),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to create mail client: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
m := mail.NewMsg()
|
addr := fmt.Sprintf("%s:%d", cfg.SMTPServer, cfg.SMTPPort)
|
||||||
if err := m.From(cfg.SenderEmail); err != nil {
|
|
||||||
log.Printf("Failed to set from: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if err := m.To(recipient); err != nil {
|
|
||||||
log.Printf("Failed to set to: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
m.Subject(subject)
|
|
||||||
|
|
||||||
unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
|
unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
|
||||||
cfg.BaseURL, url.QueryEscape(recipient))
|
cfg.BaseURL, url.QueryEscape(recipient))
|
||||||
|
|
||||||
// Build HTML body with unsubscribe link
|
|
||||||
htmlBody := buildHTMLBody(body, unsubLink)
|
htmlBody := buildHTMLBody(body, unsubLink)
|
||||||
m.SetBodyString(mail.TypeTextHTML, htmlBody)
|
message := buildMessage(cfg.SenderEmail, recipient, subject, htmlBody)
|
||||||
|
|
||||||
if err := client.Send(m); err != nil {
|
// Create TLS connection
|
||||||
log.Printf("Failed to send email to %s: %v", recipient, err)
|
tlsconfig := &tls.Config{
|
||||||
|
ServerName: cfg.SMTPServer,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.Dial("tcp", addr, tlsconfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to connect to SMTP %s: %v", addr, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Create SMTP client
|
||||||
|
client, err := smtp.NewClient(conn, cfg.SMTPServer)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to create SMTP client: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
auth := smtp.PlainAuth("", cfg.SMTPUser, cfg.SMTPPassword, cfg.SMTPServer)
|
||||||
|
if err := client.Auth(auth); err != nil {
|
||||||
|
log.Printf("SMTP auth failed for %s: %v", cfg.SMTPUser, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
if err := client.Mail(cfg.SenderEmail); err != nil {
|
||||||
|
log.Printf("MAIL command failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Rcpt(recipient); err != nil {
|
||||||
|
log.Printf("RCPT command failed for %s: %v", recipient, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("DATA command failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(message))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to write message: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to close DATA: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Quit()
|
||||||
|
|
||||||
log.Printf("Update email sent to: %s", recipient)
|
log.Printf("Update email sent to: %s", recipient)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildMessage(from, to, subject, body string) string {
|
||||||
|
msg := fmt.Sprintf("From: %s\r\n", from)
|
||||||
|
msg += fmt.Sprintf("To: %s\r\n", to)
|
||||||
|
msg += fmt.Sprintf("Subject: %s\r\n", subject)
|
||||||
|
msg += "MIME-Version: 1.0\r\n"
|
||||||
|
msg += "Content-Type: text/html; charset=\"utf-8\"\r\n"
|
||||||
|
msg += "\r\n"
|
||||||
|
msg += body
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
// buildHTMLBody constructs the final HTML email body by appending an unsubscribe footer to the user-provided content.
|
// buildHTMLBody constructs the final HTML email body by appending an unsubscribe footer to the user-provided content.
|
||||||
// It handles both complete HTML documents and HTML fragments.
|
|
||||||
func buildHTMLBody(body, unsubLink string) string {
|
func buildHTMLBody(body, unsubLink string) string {
|
||||||
footer := fmt.Sprintf(
|
footer := fmt.Sprintf(
|
||||||
"<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+
|
"<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+
|
||||||
"please click <a href='%s'>here</a>.</p>",
|
"please click <a href='%s'>here</a>.</p>",
|
||||||
unsubLink)
|
unsubLink)
|
||||||
|
|
||||||
// If body contains closing html tag, insert before it
|
|
||||||
if strings.Contains(strings.ToLower(body), "</html>") {
|
if strings.Contains(strings.ToLower(body), "</html>") {
|
||||||
return strings.Replace(body, "</html>", footer+"</html>", 1)
|
return strings.Replace(body, "</html>", footer+"</html>", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If body contains closing body tag, insert before it
|
|
||||||
if strings.Contains(strings.ToLower(body), "</body>") {
|
if strings.Contains(strings.ToLower(body), "</body>") {
|
||||||
return strings.Replace(body, "</body>", footer+"</body>", 1)
|
return strings.Replace(body, "</body>", footer+"</body>", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise just append
|
|
||||||
return body + footer
|
return body + footer
|
||||||
}
|
}
|
||||||
|
|||||||
174
scripts/build.sh
Executable file
174
scripts/build.sh
Executable file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
IMAGE_NAME="rideaware-admin"
|
||||||
|
IMAGE_TAG="latest"
|
||||||
|
NO_CACHE=false
|
||||||
|
RUN_CONTAINER=false
|
||||||
|
CONTAINER_NAME="rideaware-admin"
|
||||||
|
|
||||||
|
# Help function
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: $0 [OPTIONS]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-t, --tag TAG Image tag (default: latest)
|
||||||
|
-n, --name NAME Image name (default: rideaware)
|
||||||
|
-r, --run Run container after build
|
||||||
|
-c, --container NAME Container name when running (default: rideaware-admin)
|
||||||
|
--no-cache Build without cache
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
$0 # Build as rideaware:latest
|
||||||
|
$0 -t v1.0 # Build as rideaware:v1.0
|
||||||
|
$0 -t dev --run # Build and run
|
||||||
|
$0 --no-cache -t prod # Build without cache as rideaware:prod
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-t|--tag)
|
||||||
|
IMAGE_TAG="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-n|--name)
|
||||||
|
IMAGE_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-r|--run)
|
||||||
|
RUN_CONTAINER=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-c|--container)
|
||||||
|
CONTAINER_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-cache)
|
||||||
|
NO_CACHE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Unknown option: $1${NC}"
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
FULL_IMAGE="$IMAGE_NAME:$IMAGE_TAG"
|
||||||
|
BUILD_ARGS=""
|
||||||
|
|
||||||
|
if [ "$NO_CACHE" = true ]; then
|
||||||
|
BUILD_ARGS="--no-cache"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to stop and remove container
|
||||||
|
cleanup_container() {
|
||||||
|
local name=$1
|
||||||
|
|
||||||
|
if podman ps -a --format "{{.Names}}" | grep -q "^${name}\$"; then
|
||||||
|
echo -e "${YELLOW}Removing existing container: $name${NC}"
|
||||||
|
|
||||||
|
# Stop if running
|
||||||
|
if podman ps --format "{{.Names}}" | grep -q "^${name}\$"; then
|
||||||
|
echo " Stopping container..."
|
||||||
|
podman kill "$name" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
echo " Removing container..."
|
||||||
|
if podman rm "$name" 2>/dev/null; then
|
||||||
|
echo -e "${GREEN} ✓ Container removed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Failed to remove container${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ Building Podman Image ║${NC}"
|
||||||
|
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
|
||||||
|
echo -e "${YELLOW}Image: $FULL_IMAGE${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! podman build $BUILD_ARGS -f Dockerfile -t "$FULL_IMAGE" .; then
|
||||||
|
echo -e "${RED}✗ Build failed${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Image built successfully${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Show image info
|
||||||
|
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ Image Details ║${NC}"
|
||||||
|
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
|
||||||
|
podman images "$IMAGE_NAME:$IMAGE_TAG" \
|
||||||
|
--format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.Created}}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$RUN_CONTAINER" = true ]; then
|
||||||
|
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ Starting Container ║${NC}"
|
||||||
|
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
# Cleanup existing container
|
||||||
|
if ! cleanup_container "$CONTAINER_NAME"; then
|
||||||
|
echo -e "${RED}✗ Failed to clean up existing container${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Starting new container: $CONTAINER_NAME"
|
||||||
|
|
||||||
|
if podman run -d \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
|
-p 5001:5001 \
|
||||||
|
--env-file .env \
|
||||||
|
"$FULL_IMAGE"; then
|
||||||
|
echo -e "${GREEN}✓ Container running: $CONTAINER_NAME${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Wait for startup
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Container logs:${NC}"
|
||||||
|
podman logs "$CONTAINER_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}API available at: http://localhost:5001${NC}"
|
||||||
|
echo -e "${YELLOW}To view logs: podman logs -f $CONTAINER_NAME${NC}"
|
||||||
|
echo -e "${YELLOW}To stop: podman kill $CONTAINER_NAME${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Failed to start container${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}To run the container:${NC}"
|
||||||
|
echo " podman run -d --name $CONTAINER_NAME -p 5001:5001 --env-file .env $FULL_IMAGE"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Or use this script with --run:${NC}"
|
||||||
|
echo " $0 -t $IMAGE_TAG --run"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✓ Done!${NC}"
|
||||||
Reference in New Issue
Block a user