From b6b5207d4377e79d78a8de3f4ba8c2121f75d57a Mon Sep 17 00:00:00 2001 From: H3ALY Date: Wed, 29 Oct 2025 10:43:48 +0000 Subject: [PATCH] Fleshing out some routes from notifications and messages --- internal/handlers/account/messages/list.go | 128 ++++++++++++++++++ internal/handlers/account/messages/types.go | 32 +++++ .../handlers/account/notifications/list.go | 78 +++++++++++ .../handlers/account/notifications/types.go | 21 +++ internal/http/routes/accountroutes.go | 21 +++ 5 files changed, 280 insertions(+) create mode 100644 internal/handlers/account/messages/list.go create mode 100644 internal/handlers/account/messages/types.go create mode 100644 internal/handlers/account/notifications/list.go create mode 100644 internal/handlers/account/notifications/types.go diff --git a/internal/handlers/account/messages/list.go b/internal/handlers/account/messages/list.go new file mode 100644 index 0000000..e2d0c11 --- /dev/null +++ b/internal/handlers/account/messages/list.go @@ -0,0 +1,128 @@ +package accountMessageHandler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// GET /account/messages +// Renders: web/templates/account/messages/index.html +func (h *AccountMessageHandlers) List(c *gin.Context) { + userID := mustUserID(c) // replace with your auth/user extraction + msgs, err := h.Svc.ListInbox(userID) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load messages"}) + return + } + c.HTML(http.StatusOK, "account/messages/index.html", gin.H{ + "title": "Messages", + "messages": msgs, + }) +} + +// GET /account/messages/add +// Renders: web/templates/account/messages/send.html +func (h *AccountMessageHandlers) AddGet(c *gin.Context) { + c.HTML(http.StatusOK, "account/messages/send.html", gin.H{ + "title": "Send Message", + }) +} + +// POST /account/messages/add +func (h *AccountMessageHandlers) AddPost(c *gin.Context) { + userID := mustUserID(c) + var in CreateMessageInput + if err := c.ShouldBind(&in); err != nil { + // Re-render form with validation errors + c.HTML(http.StatusBadRequest, "account/messages/send.html", gin.H{ + "title": "Send Message", + "error": "Please correct the errors below.", + "form": in, + }) + return + } + if _, err := h.Svc.Create(userID, in); err != nil { + c.HTML(http.StatusInternalServerError, "account/messages/send.html", gin.H{ + "title": "Send Message", + "error": "Could not send message.", + "form": in, + }) + return + } + // Redirect back to inbox + c.Redirect(http.StatusSeeOther, "/account/messages") +} + +// --- Optional extras since you have read.html and archived.html --- + +// GET /account/messages/:id +// Renders: web/templates/account/messages/read.html +func (h *AccountMessageHandlers) ReadGet(c *gin.Context) { + userID := mustUserID(c) + id, err := parseIDParam(c, "id") + if err != nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + msg, err := h.Svc.GetByID(userID, id) + if err != nil || msg == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + c.HTML(http.StatusOK, "account/messages/read.html", gin.H{ + "title": msg.Subject, + "message": msg, + }) +} + +// GET /account/messages/archived +// Renders: web/templates/account/messages/archived.html +func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) { + userID := mustUserID(c) + msgs, err := h.Svc.ListArchived(userID) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load archived messages"}) + return + } + c.HTML(http.StatusOK, "account/messages/archived.html", gin.H{ + "title": "Archived Messages", + "messages": msgs, + }) +} + +// --- helpers --- + +func mustUserID(c *gin.Context) int64 { + // Pull from your auth middleware/session. Panic-unsafe alternative: + if v, ok := c.Get("userID"); ok { + if id, ok2 := v.(int64); ok2 { + return id + } + } + // Fallback for stubs: + return 1 +} + +func parseIDParam(c *gin.Context, name string) (int64, error) { + // typical atoi wrapper + // (implement: strconv.ParseInt(c.Param(name), 10, 64)) + return atoi64(c.Param(name)) +} + +func atoi64(s string) (int64, error) { + // small helper to keep imports focused + // replace with strconv.ParseInt in real code + var n int64 + for _, ch := range []byte(s) { + if ch < '0' || ch > '9' { + return 0, &strconvNumErr{} + } + n = n*10 + int64(ch-'0') + } + return n, nil +} + +type strconvNumErr struct{} + +func (e *strconvNumErr) Error() string { return "invalid number" } diff --git a/internal/handlers/account/messages/types.go b/internal/handlers/account/messages/types.go new file mode 100644 index 0000000..eb20669 --- /dev/null +++ b/internal/handlers/account/messages/types.go @@ -0,0 +1,32 @@ +package accountMessageHandlers + +import "time" + +type AccountMessageHandlers struct { + Svc MessageService +} + +type CreateMessageInput struct { + To string `form:"to" binding:"required,email"` + Subject string `form:"subject" binding:"required,max=200"` + Body string `form:"body" binding:"required"` +} + +type Message struct { + ID int64 + From string + To string + Subject string + Body string + IsRead bool + IsArchived bool + CreatedAt time.Time +} + +// ToDo: Should interfaces be else where? +type MessageService interface { + ListInbox(userID int64) ([]Message, error) + ListArchived(userID int64) ([]Message, error) + GetByID(userID, id int64) (*Message, error) + Create(userID int64, in CreateMessageInput) (int64, error) +} diff --git a/internal/handlers/account/notifications/list.go b/internal/handlers/account/notifications/list.go new file mode 100644 index 0000000..d5a5b78 --- /dev/null +++ b/internal/handlers/account/notifications/list.go @@ -0,0 +1,78 @@ +package accountNotificationHandler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func NewAccountNotificationHandlers(svc NotificationService) *AccountNotificationHandlers { + return &AccountNotificationHandlers{Svc: svc} +} + +// GET /account/notifications +// Renders: web/templates/account/notifications/index.html +func (h *AccountNotificationHandlers) List(c *gin.Context) { + userID := mustUserID(c) + items, err := h.Svc.List(userID) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load notifications"}) + return + } + c.HTML(http.StatusOK, "account/notifications/index.html", gin.H{ + "title": "Notifications", + "notifications": items, + }) +} + +// --- Optional since you have read.html --- + +// GET /account/notifications/:id +// Renders: web/templates/account/notifications/read.html +func (h *AccountNotificationHandlers) ReadGet(c *gin.Context) { + userID := mustUserID(c) + id, err := parseIDParam(c, "id") + if err != nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + nt, err := h.Svc.GetByID(userID, id) + if err != nil || nt == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + c.HTML(http.StatusOK, "account/notifications/read.html", gin.H{ + "title": nt.Title, + "notification": nt, + }) +} + +// --- helpers (same as in messages; consider sharing in a pkg) --- + +func mustUserID(c *gin.Context) int64 { + if v, ok := c.Get("userID"); ok { + if id, ok2 := v.(int64); ok2 { + return id + } + } + return 1 +} + +func parseIDParam(c *gin.Context, name string) (int64, error) { + return atoi64(c.Param(name)) +} + +func atoi64(s string) (int64, error) { + var n int64 + for _, ch := range []byte(s) { + if ch < '0' || ch > '9' { + return 0, &strconvNumErr{} + } + n = n*10 + int64(ch-'0') + } + return n, nil +} + +type strconvNumErr struct{} + +func (e *strconvNumErr) Error() string { return "invalid number" } diff --git a/internal/handlers/account/notifications/types.go b/internal/handlers/account/notifications/types.go new file mode 100644 index 0000000..dc3bd13 --- /dev/null +++ b/internal/handlers/account/notifications/types.go @@ -0,0 +1,21 @@ +package accountNotificationHandler + +import "time" + +type Notification struct { + ID int64 + Title string + Body string + IsRead bool + CreatedAt time.Time +} + +type AccountNotificationHandlers struct { + Svc NotificationService +} + +// ToDo: Should interfaces be else where? +type NotificationService interface { + List(userID int64) ([]Notification, error) + GetByID(userID, id int64) (*Notification, error) +} diff --git a/internal/http/routes/accountroutes.go b/internal/http/routes/accountroutes.go index 56882c3..67431e4 100644 --- a/internal/http/routes/accountroutes.go +++ b/internal/http/routes/accountroutes.go @@ -22,6 +22,8 @@ package routes import ( accountHandlers "synlotto-website/internal/handlers/account" + accountMessageHandlers "synlotto-website/internal/handlers/account/messages" + accountNotificationHandlers "synlotto-website/internal/handlers/account/notifications" accountTicketHandlers "synlotto-website/internal/handlers/account/tickets" "synlotto-website/internal/http/middleware" @@ -49,6 +51,25 @@ func RegisterAccountRoutes(app *bootstrap.App) { accAuth.GET("/logout", accountHandlers.Logout) // optional } + // Messages (auth-required) + messages := r.Group("/account/messages") + messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth()) + { + messages.GET("/", accountMessageHandlers.List) + messages.GET("/add", accountMessageHandlers.AddGet) + messages.POST("/add", accountMessageHandlers.AddPost) + messages.GET("/archived", accountMessageHandlers.ArchivedList) // renders archived.html + messages.GET("/:id", accountMessageHandlers.ReadGet) // renders read.html + } + + // Notifications (auth-required) + notifications := r.Group("/account/notifications") + notifications.Use(middleware.AuthMiddleware(), middleware.RequireAuth()) + { + notifications.GET("/", accountNotificationHandlers.List) + notifications.GET("/:id", accountNotificationHandlers.ReadGet) // renders read.html + } + // Tickets (auth-required) tickets := r.Group("/account/tickets") tickets.Use(middleware.AuthMiddleware(), middleware.RequireAuth())