Continued work on messages and notifications.
This commit is contained in:
20
internal/domain/messages/domain.go
Normal file
20
internal/domain/messages/domain.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package domainMessages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message = models.Message
|
||||||
|
|
||||||
|
type CreateMessageInput struct {
|
||||||
|
RecipientID int64 `form:"to" binding:"required,username"`
|
||||||
|
Subject string `form:"subject" binding:"required,max=200"`
|
||||||
|
Body string `form:"body" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
14
internal/domain/notifications/domain.go
Normal file
14
internal/domain/notifications/domain.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package domainMessages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToDo: Should be taken from model.
|
||||||
|
type Notification = models.Notification
|
||||||
|
|
||||||
|
// ToDo: Should interfaces be else where?
|
||||||
|
type NotificationService interface {
|
||||||
|
List(userID int64) ([]Notification, error)
|
||||||
|
GetByID(userID, id int64) (*Notification, error)
|
||||||
|
}
|
||||||
@@ -7,20 +7,46 @@ package accountMessageHandler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /account/messages/archived
|
// GET /account/messages/archived
|
||||||
// Renders: web/templates/account/messages/archived.html
|
// Renders: web/templates/account/messages/archived.html
|
||||||
func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) {
|
func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
userID := mustUserID(c)
|
userID := mustUserID(c)
|
||||||
msgs, err := h.Svc.ListArchived(userID)
|
msgs, err := h.Svc.ListArchived(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load archived messages"})
|
logging.Info("❌ list archived error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Failed to load archived messages")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "account/messages/archived.html", gin.H{
|
|
||||||
"title": "Archived Messages",
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
"messages": msgs,
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
})
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = "Archived Messages"
|
||||||
|
ctx["Messages"] = msgs
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"layout.html",
|
||||||
|
"web/templates/account/messages/archived.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering archived messages")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,40 +7,88 @@ package accountMessageHandler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /account/messages
|
// GET /account/messages
|
||||||
// Renders: web/templates/account/messages/index.html
|
// Renders: web/templates/account/messages/index.html
|
||||||
func (h *AccountMessageHandlers) List(c *gin.Context) {
|
func (h *AccountMessageHandlers) List(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
userID := mustUserID(c)
|
userID := mustUserID(c)
|
||||||
|
|
||||||
|
// Pull messages (via service)
|
||||||
msgs, err := h.Svc.ListInbox(userID)
|
msgs, err := h.Svc.ListInbox(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load messages"})
|
logging.Info("❌ list inbox error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Failed to load messages")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "account/messages/index.html", gin.H{
|
|
||||||
"title": "Messages",
|
// Build template context just like LoginGet
|
||||||
"messages": msgs,
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
})
|
|
||||||
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = "Messages"
|
||||||
|
ctx["Messages"] = msgs
|
||||||
|
|
||||||
|
// Use the same loader + layout pattern
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/index.html")
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering messages page")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /account/messages/:id
|
// GET /account/messages/:id
|
||||||
// Renders: web/templates/account/messages/read.html
|
// Renders: web/templates/account/messages/read.html
|
||||||
func (h *AccountMessageHandlers) ReadGet(c *gin.Context) {
|
func (h *AccountMessageHandlers) ReadGet(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
userID := mustUserID(c)
|
userID := mustUserID(c)
|
||||||
id, err := parseIDParam(c, "id")
|
id, err := parseIDParam(c, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatus(http.StatusNotFound)
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := h.Svc.GetByID(userID, id)
|
msg, err := h.Svc.GetByID(userID, id)
|
||||||
if err != nil || msg == nil {
|
if err != nil || msg == nil {
|
||||||
c.AbortWithStatus(http.StatusNotFound)
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "account/messages/read.html", gin.H{
|
|
||||||
"title": msg.Subject,
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
"message": msg,
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
})
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = msg.Subject
|
||||||
|
ctx["Message"] = msg
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"layout.html",
|
||||||
|
"web/templates/account/messages/read.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering message")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,38 +7,101 @@ package accountMessageHandler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
domain "synlotto-website/internal/domain/messages"
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /account/messages/add
|
// GET /account/messages/add
|
||||||
// Renders: web/templates/account/messages/send.html
|
// Renders: web/templates/account/messages/send.html
|
||||||
func (h *AccountMessageHandlers) AddGet(c *gin.Context) {
|
func (h *AccountMessageHandlers) AddGet(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "account/messages/send.html", gin.H{
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
"title": "Send Message",
|
sm := app.SessionManager
|
||||||
})
|
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = "Send Message"
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"layout.html",
|
||||||
|
"web/templates/account/messages/send.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering send message page")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /account/messages/add
|
// POST /account/messages/add
|
||||||
func (h *AccountMessageHandlers) AddPost(c *gin.Context) {
|
func (h *AccountMessageHandlers) AddPost(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
userID := mustUserID(c)
|
userID := mustUserID(c)
|
||||||
var in CreateMessageInput
|
|
||||||
|
var in domain.CreateMessageInput
|
||||||
if err := c.ShouldBind(&in); err != nil {
|
if err := c.ShouldBind(&in); err != nil {
|
||||||
// Re-render form with validation errors
|
// Re-render form with validation errors
|
||||||
c.HTML(http.StatusBadRequest, "account/messages/send.html", gin.H{
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
"title": "Send Message",
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
"error": "Please correct the errors below.",
|
ctx["Flash"] = f
|
||||||
"form": in,
|
}
|
||||||
})
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = "Send Message"
|
||||||
|
ctx["Error"] = "Please correct the errors below."
|
||||||
|
ctx["Form"] = in
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"layout.html",
|
||||||
|
"web/templates/account/messages/send.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusBadRequest)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering send message page")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.Svc.Create(userID, in); err != nil {
|
if _, err := h.Svc.Create(userID, in); err != nil {
|
||||||
c.HTML(http.StatusInternalServerError, "account/messages/send.html", gin.H{
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
"title": "Send Message",
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
"error": "Could not send message.",
|
ctx["Flash"] = f
|
||||||
"form": in,
|
}
|
||||||
})
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = "Send Message"
|
||||||
|
ctx["Error"] = "Could not send message."
|
||||||
|
ctx["Form"] = in
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"layout.html",
|
||||||
|
"web/templates/account/messages/send.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering send message page")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: set a flash message for success (since you already PopString elsewhere)
|
||||||
|
// If you're using scs/v2, Put is available:
|
||||||
|
sm.Put(c.Request.Context(), "flash", "Message sent!")
|
||||||
|
|
||||||
// Redirect back to inbox
|
// Redirect back to inbox
|
||||||
c.Redirect(http.StatusSeeOther, "/account/messages")
|
c.Redirect(http.StatusSeeOther, "/account/messages")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,33 +4,8 @@
|
|||||||
|
|
||||||
package accountMessageHandler
|
package accountMessageHandler
|
||||||
|
|
||||||
import "time"
|
import domain "synlotto-website/internal/domain/messages"
|
||||||
|
|
||||||
type AccountMessageHandlers struct {
|
type AccountMessageHandlers struct {
|
||||||
Svc MessageService
|
Svc domain.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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,66 +3,32 @@ package accountNotificationHandler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAccountNotificationHandlers(svc NotificationService) *AccountNotificationHandlers {
|
// ToDo: functional also in messages needs to come out
|
||||||
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 {
|
func mustUserID(c *gin.Context) int64 {
|
||||||
|
// Pull from your auth middleware/session. Panic-unsafe alternative:
|
||||||
if v, ok := c.Get("userID"); ok {
|
if v, ok := c.Get("userID"); ok {
|
||||||
if id, ok2 := v.(int64); ok2 {
|
if id, ok2 := v.(int64); ok2 {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Fallback for stubs:
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseIDParam(c *gin.Context, name string) (int64, error) {
|
// ToDo: functional also in messages needs to come out
|
||||||
return atoi64(c.Param(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func atoi64(s string) (int64, error) {
|
func atoi64(s string) (int64, error) {
|
||||||
|
// small helper to keep imports focused
|
||||||
|
// replace with strconv.ParseInt in real code
|
||||||
var n int64
|
var n int64
|
||||||
for _, ch := range []byte(s) {
|
for _, ch := range []byte(s) {
|
||||||
if ch < '0' || ch > '9' {
|
if ch < '0' || ch > '9' {
|
||||||
@@ -76,3 +42,34 @@ func atoi64(s string) (int64, error) {
|
|||||||
type strconvNumErr struct{}
|
type strconvNumErr struct{}
|
||||||
|
|
||||||
func (e *strconvNumErr) Error() string { return "invalid number" }
|
func (e *strconvNumErr) Error() string { return "invalid number" }
|
||||||
|
|
||||||
|
// GET /account/notifications/:id
|
||||||
|
// Renders: web/templates/account/notifications/read.html
|
||||||
|
func (h *AccountNotificationHandlers) List(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
|
userID := mustUserID(c)
|
||||||
|
notes, err := h.Svc.List(userID) // or ListAll/ListUnread – use your method name
|
||||||
|
if err != nil {
|
||||||
|
logging.Info("❌ list notifications error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Failed to load notifications")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = "Notifications"
|
||||||
|
ctx["Notifications"] = notes
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/notifications/index.html")
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering notifications page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
58
internal/handlers/account/notifications/read.go
Normal file
58
internal/handlers/account/notifications/read.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// internal/handlers/account/notifications/read.go
|
||||||
|
package accountNotificationHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToDo: functional also in messages needs to come out
|
||||||
|
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 (h *AccountNotificationHandlers) ReadGet(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
|
userID := mustUserID(c)
|
||||||
|
id, err := parseIDParam(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := h.Svc.GetByID(userID, id)
|
||||||
|
if err != nil || n == nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
ctx["Title"] = n.Title // or Subject/Heading depending on your struct
|
||||||
|
ctx["Notification"] = n
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"layout.html",
|
||||||
|
"web/templates/account/notifications/read.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,7 @@
|
|||||||
package accountNotificationHandler
|
package accountNotificationHandler
|
||||||
|
|
||||||
import "time"
|
import domain "synlotto-website/internal/domain/notifications"
|
||||||
|
|
||||||
type Notification struct {
|
|
||||||
ID int64
|
|
||||||
Title string
|
|
||||||
Body string
|
|
||||||
IsRead bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountNotificationHandlers struct {
|
type AccountNotificationHandlers struct {
|
||||||
Svc NotificationService
|
Svc domain.NotificationService
|
||||||
}
|
|
||||||
|
|
||||||
// ToDo: Should interfaces be else where?
|
|
||||||
type NotificationService interface {
|
|
||||||
List(userID int64) ([]Notification, error)
|
|
||||||
GetByID(userID, id int64) (*Notification, error)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
accountHandler "synlotto-website/internal/handlers/account"
|
accountHandler "synlotto-website/internal/handlers/account"
|
||||||
accoutMessageHandler "synlotto-website/internal/handlers/account/messages"
|
accountMsgHandlers "synlotto-website/internal/handlers/account/messages"
|
||||||
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
||||||
accountTicketHandler "synlotto-website/internal/handlers/account/tickets"
|
accountTicketHandler "synlotto-website/internal/handlers/account/tickets"
|
||||||
|
|
||||||
@@ -33,6 +33,16 @@ import (
|
|||||||
func RegisterAccountRoutes(app *bootstrap.App) {
|
func RegisterAccountRoutes(app *bootstrap.App) {
|
||||||
r := app.Router
|
r := app.Router
|
||||||
|
|
||||||
|
// Instantiate handlers that have method receivers
|
||||||
|
messageSvc := app.Services.Messages
|
||||||
|
msgH := &accountMsgHandlers.AccountMessageHandlers{Svc: messageSvc}
|
||||||
|
|
||||||
|
notificationSvc := app.Services.Notifications
|
||||||
|
notifH := &accountNotificationHandler.AccountNotificationHandlers{Svc: notificationSvc}
|
||||||
|
|
||||||
|
// ticketSvc := app.Services.TicketService
|
||||||
|
// ticketH := &accountTickets.AccountTicketHandlers{Svc: ticketSvc}
|
||||||
|
|
||||||
// Public account pages
|
// Public account pages
|
||||||
acc := r.Group("/account")
|
acc := r.Group("/account")
|
||||||
acc.Use(middleware.PublicOnly())
|
acc.Use(middleware.PublicOnly())
|
||||||
@@ -55,19 +65,19 @@ func RegisterAccountRoutes(app *bootstrap.App) {
|
|||||||
messages := r.Group("/account/messages")
|
messages := r.Group("/account/messages")
|
||||||
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
messages.GET("/", accoutMessageHandler.List)
|
messages.GET("/", msgH.List)
|
||||||
messages.GET("/add", accoutMessageHandler.AddGet)
|
messages.GET("/add", msgH.AddGet)
|
||||||
messages.POST("/add", accoutMessageHandler.AddPost)
|
messages.POST("/add", msgH.AddPost)
|
||||||
messages.GET("/archived", accoutMessageHandler.ArchivedList) // renders archived.html
|
messages.GET("/archived", msgH.ArchivedList) // renders archived.html
|
||||||
messages.GET("/:id", accoutMessageHandler.ReadGet) // renders read.html
|
messages.GET("/:id", msgH.ReadGet) // renders read.html
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications (auth-required)
|
// Notifications (auth-required)
|
||||||
notifications := r.Group("/account/notifications")
|
notifications := r.Group("/account/notifications")
|
||||||
notifications.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
notifications.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
notifications.GET("/", accountNotificationHandler.List)
|
notifications.GET("/", notifH.List)
|
||||||
notifications.GET("/:id", accountNotificationHandler.ReadGet) // renders read.html
|
notifications.GET("/:id", notifH.ReadGet) // renders read.html
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tickets (auth-required)
|
// Tickets (auth-required)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type User struct {
|
|||||||
type Notification struct {
|
type Notification struct {
|
||||||
ID int
|
ID int
|
||||||
UserId int
|
UserId int
|
||||||
Subject string
|
Title string
|
||||||
Body string
|
Body string
|
||||||
IsRead bool
|
IsRead bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@@ -30,8 +30,9 @@ type Message struct {
|
|||||||
SenderId int
|
SenderId int
|
||||||
RecipientId int
|
RecipientId int
|
||||||
Subject string
|
Subject string
|
||||||
Message string
|
Body string
|
||||||
IsRead bool
|
IsRead bool
|
||||||
|
IsArchived bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
ArchivedAt *time.Time
|
ArchivedAt *time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
domainMsgs "synlotto-website/internal/domain/messages"
|
||||||
|
domainNotifs "synlotto-website/internal/domain/notifications"
|
||||||
weberr "synlotto-website/internal/http/error"
|
weberr "synlotto-website/internal/http/error"
|
||||||
databasePlatform "synlotto-website/internal/platform/database"
|
databasePlatform "synlotto-website/internal/platform/database"
|
||||||
|
messagesvc "synlotto-website/internal/platform/services/messages"
|
||||||
|
notifysvc "synlotto-website/internal/platform/services/notifications"
|
||||||
|
|
||||||
"synlotto-website/internal/platform/config"
|
"synlotto-website/internal/platform/config"
|
||||||
"synlotto-website/internal/platform/csrf"
|
"synlotto-website/internal/platform/csrf"
|
||||||
@@ -78,6 +82,11 @@ type App struct {
|
|||||||
Router *gin.Engine
|
Router *gin.Engine
|
||||||
Handler http.Handler
|
Handler http.Handler
|
||||||
Server *http.Server
|
Server *http.Server
|
||||||
|
|
||||||
|
Services struct {
|
||||||
|
Messages domainMsgs.MessageService
|
||||||
|
Notifications domainNotifs.NotificationService
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(configPath string) (*App, error) {
|
func Load(configPath string) (*App, error) {
|
||||||
@@ -119,6 +128,9 @@ func Load(configPath string) (*App, error) {
|
|||||||
Router: router,
|
Router: router,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Services.Messages = messagesvc.New(db)
|
||||||
|
app.Services.Notifications = notifysvc.New(db)
|
||||||
|
|
||||||
// Inject *App into Gin context for handler access
|
// Inject *App into Gin context for handler access
|
||||||
router.Use(func(c *gin.Context) {
|
router.Use(func(c *gin.Context) {
|
||||||
c.Set("app", app)
|
c.Set("app", app)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
// ToDo: not currently used and need to carve out sql
|
// Package messagesvc
|
||||||
|
// Path: /internal/platform/services/messages
|
||||||
|
// File: service.go
|
||||||
|
|
||||||
package messagesvc
|
package messagesvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,13 +10,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
accountMessageHandler "synlotto-website/internal/handlers/account/messages"
|
domain "synlotto-website/internal/domain/messages"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service implements accountMessageHandler.MessageService.
|
// Service implements domain.Service.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
Dialect string // "postgres", "mysql", "sqlite" (affects INSERT id retrieval)
|
Dialect string // "postgres", "mysql", "sqlite"
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
@@ -21,7 +24,7 @@ type Service struct {
|
|||||||
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
DB: db,
|
DB: db,
|
||||||
Dialect: "mysql", // sane default for LastInsertId (works for mysql/sqlite)
|
Dialect: "mysql", // default; works with LastInsertId
|
||||||
Now: time.Now,
|
Now: time.Now,
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -31,56 +34,58 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDialect sets SQL dialect hints: "postgres" uses RETURNING id.
|
// Ensure *Service satisfies the domain interface.
|
||||||
func WithDialect(d string) func(*Service) { return func(s *Service) { s.Dialect = d } }
|
var _ domain.MessageService = (*Service)(nil)
|
||||||
|
|
||||||
// WithTimeout overrides per-call context timeout.
|
func (s *Service) ListInbox(userID int64) ([]domain.Message, error) {
|
||||||
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
|
||||||
|
|
||||||
func (s *Service) ListInbox(userID int64) ([]accountMessageHandler.Message, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
const q = `
|
q := `
|
||||||
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
FROM messages
|
FROM users_messages
|
||||||
WHERE user_id = ? AND is_archived = FALSE
|
WHERE user_id = ? AND is_archived = FALSE
|
||||||
ORDER BY created_at DESC`
|
ORDER BY created_at DESC`
|
||||||
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
q = s.bind(q)
|
||||||
|
|
||||||
|
rows, err := s.DB.QueryContext(ctx, q, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var out []accountMessageHandler.Message
|
var out []domain.Message
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var m accountMessageHandler.Message
|
var m domain.Message
|
||||||
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out = append(out, m)
|
out = append(out, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, error) {
|
func (s *Service) ListArchived(userID int64) ([]domain.Message, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
const q = `
|
q := `
|
||||||
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
FROM messages
|
FROM users_messages
|
||||||
WHERE user_id = ? AND is_archived = TRUE
|
WHERE user_id = ? AND is_archived = TRUE
|
||||||
ORDER BY created_at DESC`
|
ORDER BY created_at DESC`
|
||||||
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
q = s.bind(q)
|
||||||
|
|
||||||
|
rows, err := s.DB.QueryContext(ctx, q, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var out []accountMessageHandler.Message
|
var out []domain.Message
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var m accountMessageHandler.Message
|
var m domain.Message
|
||||||
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -89,16 +94,18 @@ func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, e
|
|||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, error) {
|
func (s *Service) GetByID(userID, id int64) (*domain.Message, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
const q = `
|
q := `
|
||||||
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
FROM messages
|
FROM users_messages
|
||||||
WHERE user_id = ? AND id = ?`
|
WHERE user_id = ? AND id = ?`
|
||||||
var m accountMessageHandler.Message
|
q = s.bind(q)
|
||||||
err := s.DB.QueryRowContext(s.rebind(ctx), s.bind(q), userID, id).
|
|
||||||
|
var m domain.Message
|
||||||
|
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
||||||
Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt)
|
Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -109,7 +116,7 @@ func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, err
|
|||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInput) (int64, error) {
|
func (s *Service) Create(userID int64, in domain.CreateMessageInput) (int64, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -128,7 +135,7 @@ func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInp
|
|||||||
const q = `
|
const q = `
|
||||||
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
|
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)`
|
VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)`
|
||||||
res, err := s.DB.ExecContext(s.rebind(ctx), q, userID, "", in.To, in.Subject, in.Body)
|
res, err := s.DB.ExecContext(ctx, q, userID, "", in.To, in.Subject, in.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -136,17 +143,13 @@ func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- small helpers ---
|
// bind replaces '?' with '$1..' only for Postgres. For MySQL/SQLite it returns q unchanged.
|
||||||
|
|
||||||
// bind replaces ? with $1.. for Postgres if needed.
|
|
||||||
// We keep queries written with ? for brevity and adapt here.
|
|
||||||
func (s *Service) bind(q string) string {
|
func (s *Service) bind(q string) string {
|
||||||
if s.Dialect != "postgres" {
|
if s.Dialect != "postgres" {
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
// cheap replacer for positional params:
|
|
||||||
n := 0
|
n := 0
|
||||||
out := make([]byte, 0, len(q)+10)
|
out := make([]byte, 0, len(q)+8)
|
||||||
for i := 0; i < len(q); i++ {
|
for i := 0; i < len(q); i++ {
|
||||||
if q[i] == '?' {
|
if q[i] == '?' {
|
||||||
n++
|
n++
|
||||||
@@ -159,9 +162,7 @@ func (s *Service) bind(q string) string {
|
|||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) rebind(ctx context.Context) context.Context { return ctx }
|
// ToDo: helper dont think it should be here.
|
||||||
|
|
||||||
// intToStr avoids fmt for tiny helper
|
|
||||||
func intToStr(n int) string {
|
func intToStr(n int) string {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return "0"
|
return "0"
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
// Package notifysvc
|
||||||
|
// Path: /internal/platform/services/notifications
|
||||||
|
// File: service.go
|
||||||
// ToDo: carve out sql
|
// ToDo: carve out sql
|
||||||
|
|
||||||
package notifysvc
|
package notifysvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,7 +11,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
domain "synlotto-website/internal/domain/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -31,7 +35,7 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
|
|||||||
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
||||||
|
|
||||||
// List returns newest-first notifications for a user.
|
// List returns newest-first notifications for a user.
|
||||||
func (s *Service) List(userID int64) ([]accountNotificationHandler.Notification, error) {
|
func (s *Service) List(userID int64) ([]domain.Notification, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -47,9 +51,9 @@ ORDER BY created_at DESC`
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var out []accountNotificationHandler.Notification
|
var out []domain.Notification
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var n accountNotificationHandler.Notification
|
var n domain.Notification
|
||||||
if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err != nil {
|
if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -58,7 +62,7 @@ ORDER BY created_at DESC`
|
|||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetByID(userID, id int64) (*accountNotificationHandler.Notification, error) {
|
func (s *Service) GetByID(userID, id int64) (*domain.Notification, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -67,7 +71,7 @@ SELECT id, title, body, is_read, created_at
|
|||||||
FROM notifications
|
FROM notifications
|
||||||
WHERE user_id = ? AND id = ?`
|
WHERE user_id = ? AND id = ?`
|
||||||
|
|
||||||
var n accountNotificationHandler.Notification
|
var n domain.Notification
|
||||||
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
||||||
Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt)
|
Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
|||||||
Reference in New Issue
Block a user