refactor(pr): Fixed some PR issues
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -27,7 +29,7 @@ type Admin struct {
|
|||||||
// cfg provides PostgreSQL connection parameters and the default admin credentials used to create the
|
// cfg provides PostgreSQL connection parameters and the default admin credentials used to create the
|
||||||
// default admin user when missing.
|
// default admin user when missing.
|
||||||
func Init(cfg *config.Config) {
|
func Init(cfg *config.Config) {
|
||||||
password := url.QueryEscape(cfg.PGPassword)
|
password := url.PathEscape(cfg.PGPassword)
|
||||||
|
|
||||||
psqlInfo := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=require",
|
psqlInfo := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=require",
|
||||||
cfg.PGUser, password, cfg.PGHost, cfg.PGPort, cfg.PGDatabase,
|
cfg.PGUser, password, cfg.PGHost, cfg.PGPort, cfg.PGDatabase,
|
||||||
@@ -45,7 +47,9 @@ func Init(cfg *config.Config) {
|
|||||||
db.SetMaxOpenConns(25)
|
db.SetMaxOpenConns(25)
|
||||||
db.SetMaxIdleConns(5)
|
db.SetMaxIdleConns(5)
|
||||||
|
|
||||||
if err = db.Ping(); err != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel
|
||||||
|
if err = db.PingContext(ctx); err != nil {
|
||||||
log.Fatalf("Failed to ping database: %v", err)
|
log.Fatalf("Failed to ping database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +62,9 @@ func Init(cfg *config.Config) {
|
|||||||
// It is safe to call multiple times; if no connection exists, the call is a no-op.
|
// It is safe to call multiple times; if no connection exists, the call is a no-op.
|
||||||
func Close() {
|
func Close() {
|
||||||
if db != nil {
|
if db != nil {
|
||||||
db.Close()
|
if err := db.Close(); err != nil {
|
||||||
|
log.Printf("Error closing database connection: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package email
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rideaware/admin-panel/internal/config"
|
"github.com/rideaware/admin-panel/internal/config"
|
||||||
@@ -23,22 +24,24 @@ func SendUpdate(subject, body string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "Failed to retrieve subscribers", err
|
return "Failed to retrieve subscribers", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(subscribers) == 0 {
|
if len(subscribers) == 0 {
|
||||||
return "No subscribers found.", nil
|
return "No subscribers found.", nil
|
||||||
}
|
}
|
||||||
|
var succeeded, failed int
|
||||||
for _, email := range subscribers {
|
for _, email := range subscribers {
|
||||||
if !send(subject, body, email) {
|
if send(subject, body, email) {
|
||||||
return fmt.Sprintf("Failed to send to %s", email), nil
|
succeeded++
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
return "Email has been sent to all subscribers.", 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 using the current SMTP configuration.
|
||||||
@@ -72,7 +75,7 @@ func send(subject, body, recipient string) bool {
|
|||||||
m.Subject(subject)
|
m.Subject(subject)
|
||||||
|
|
||||||
unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
|
unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
|
||||||
cfg.BaseURL, recipient)
|
cfg.BaseURL, url.QueryEscape(recipient))
|
||||||
|
|
||||||
htmlBody := fmt.Sprintf(
|
htmlBody := fmt.Sprintf(
|
||||||
"%s<br><br>If you ever wish to unsubscribe, "+
|
"%s<br><br>If you ever wish to unsubscribe, "+
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ func SendUpdatePost(c *gin.Context) {
|
|||||||
subject := c.PostForm("subject")
|
subject := c.PostForm("subject")
|
||||||
body := c.PostForm("body")
|
body := c.PostForm("body")
|
||||||
|
|
||||||
|
// validate inputs
|
||||||
|
if strings.TrimSpace(subject) == "" || strings.TrimSpace(body) == {
|
||||||
|
c.HTML(http,StatusBadRequest, "send_update.html",
|
||||||
|
gin.H{"error": "Subject and message cannot be empty"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
message, err := email.SendUpdate(subject, body)
|
message, err := email.SendUpdate(subject, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusOK, "send_update.html",
|
c.HTML(http.StatusInternalServerError, "send_update.html",
|
||||||
gin.H{"error": message})
|
gin.H{"error": message})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import (
|
|||||||
func IndexGet(c *gin.Context) {
|
func IndexGet(c *gin.Context) {
|
||||||
emails, err := database.GetAllEmails()
|
emails, err := database.GetAllEmails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.HTML(http.StatusInternalServerError, "admin_index.html",
|
||||||
|
gin.H{"error": "Failed to retrieve subscribers"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "admin_index.html",
|
c.HTML(http.StatusOK, "admin_index.html",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ var store *sessions.CookieStore
|
|||||||
// It panics if config.Current.SecretKey is empty.
|
// It panics if config.Current.SecretKey is empty.
|
||||||
// The created store is configured with Path "/", MaxAge one week, HttpOnly true, Secure false, and SameSite 0.
|
// The created store is configured with Path "/", MaxAge one week, HttpOnly true, Secure false, and SameSite 0.
|
||||||
func Init() {
|
func Init() {
|
||||||
|
if config.Current == nil {
|
||||||
|
panic("config was not loaded; call config.Load() before middleware.Init()")
|
||||||
|
}
|
||||||
|
|
||||||
if config.Current.SecretKey == "" {
|
if config.Current.SecretKey == "" {
|
||||||
panic("SECRET_KEY not set")
|
panic("SECRET_KEY not set")
|
||||||
}
|
}
|
||||||
@@ -23,8 +27,8 @@ func Init() {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: 86400 * 7,
|
MaxAge: 86400 * 7,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: false,
|
Secure: true,
|
||||||
SameSite: 0,
|
SameSite: http.SameSiteStrictMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +44,11 @@ func GetStore() *sessions.CookieStore {
|
|||||||
// Otherwise the middleware calls the next handler in the chain.
|
// Otherwise the middleware calls the next handler in the chain.
|
||||||
func Auth() gin.HandlerFunc {
|
func Auth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
if store == nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Session store not initialized.")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
session, err := store.Get(c.Request, "session")
|
session, err := store.Get(c.Request, "session")
|
||||||
if err != nil || session.Values["username"] == nil {
|
if err != nil || session.Values["username"] == nil {
|
||||||
c.Redirect(http.StatusFound, "/login")
|
c.Redirect(http.StatusFound, "/login")
|
||||||
|
|||||||
Reference in New Issue
Block a user