Fleshing out some routes from notifications and messages

This commit is contained in:
2025-10-29 10:43:48 +00:00
parent 34918d770f
commit b6b5207d43
5 changed files with 280 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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