Continued work around getting messages and notifications cleaned up since moving to MySQL and changing to Gin, SCS, NoSurf.
This commit is contained in:
26
internal/handlers/account/messages/archive.go
Normal file
26
internal/handlers/account/messages/archive.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: archive.go
|
||||||
|
|
||||||
|
package accountMessageHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,98 +1,14 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: list.go
|
||||||
|
// ToDo: helpers for reading getting messages shouldn't really be here. ---
|
||||||
|
|
||||||
package accountMessageHandler
|
package accountMessageHandler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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 {
|
func mustUserID(c *gin.Context) int64 {
|
||||||
// Pull from your auth middleware/session. Panic-unsafe alternative:
|
// Pull from your auth middleware/session. Panic-unsafe alternative:
|
||||||
if v, ok := c.Get("userID"); ok {
|
if v, ok := c.Get("userID"); ok {
|
||||||
|
|||||||
46
internal/handlers/account/messages/read.go
Normal file
46
internal/handlers/account/messages/read.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: read.go
|
||||||
|
|
||||||
|
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)
|
||||||
|
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/: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,
|
||||||
|
})
|
||||||
|
}
|
||||||
44
internal/handlers/account/messages/send.go
Normal file
44
internal/handlers/account/messages/send.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: send.go
|
||||||
|
|
||||||
|
package accountMessageHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: types.go
|
||||||
|
|
||||||
package accountMessageHandler
|
package accountMessageHandler
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
accountHandlers "synlotto-website/internal/handlers/account"
|
accountHandler "synlotto-website/internal/handlers/account"
|
||||||
accountMessageHandlers "synlotto-website/internal/handlers/account/messages"
|
accoutMessageHandler "synlotto-website/internal/handlers/account/messages"
|
||||||
accountNotificationHandlers "synlotto-website/internal/handlers/account/notifications"
|
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
||||||
accountTicketHandlers "synlotto-website/internal/handlers/account/tickets"
|
accountTicketHandler "synlotto-website/internal/handlers/account/tickets"
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
"synlotto-website/internal/platform/bootstrap"
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
@@ -37,45 +37,45 @@ func RegisterAccountRoutes(app *bootstrap.App) {
|
|||||||
acc := r.Group("/account")
|
acc := r.Group("/account")
|
||||||
acc.Use(middleware.PublicOnly())
|
acc.Use(middleware.PublicOnly())
|
||||||
{
|
{
|
||||||
acc.GET("/login", accountHandlers.LoginGet)
|
acc.GET("/login", accountHandler.LoginGet)
|
||||||
acc.POST("/login", accountHandlers.LoginPost)
|
acc.POST("/login", accountHandler.LoginPost)
|
||||||
acc.GET("/signup", accountHandlers.SignupGet)
|
acc.GET("/signup", accountHandler.SignupGet)
|
||||||
acc.POST("/signup", accountHandlers.SignupPost)
|
acc.POST("/signup", accountHandler.SignupPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth-required account actions
|
// Auth-required account actions
|
||||||
accAuth := r.Group("/account")
|
accAuth := r.Group("/account")
|
||||||
accAuth.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
accAuth.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
accAuth.POST("/logout", accountHandlers.Logout)
|
accAuth.POST("/logout", accountHandler.Logout)
|
||||||
accAuth.GET("/logout", accountHandlers.Logout) // optional
|
accAuth.GET("/logout", accountHandler.Logout) // optional
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages (auth-required)
|
// Messages (auth-required)
|
||||||
messages := r.Group("/account/messages")
|
messages := r.Group("/account/messages")
|
||||||
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
messages.GET("/", accountMessageHandlers.List)
|
messages.GET("/", accoutMessageHandler.List)
|
||||||
messages.GET("/add", accountMessageHandlers.AddGet)
|
messages.GET("/add", accoutMessageHandler.AddGet)
|
||||||
messages.POST("/add", accountMessageHandlers.AddPost)
|
messages.POST("/add", accoutMessageHandler.AddPost)
|
||||||
messages.GET("/archived", accountMessageHandlers.ArchivedList) // renders archived.html
|
messages.GET("/archived", accoutMessageHandler.ArchivedList) // renders archived.html
|
||||||
messages.GET("/:id", accountMessageHandlers.ReadGet) // renders read.html
|
messages.GET("/:id", accoutMessageHandler.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("/", accountNotificationHandlers.List)
|
notifications.GET("/", accountNotificationHandler.List)
|
||||||
notifications.GET("/:id", accountNotificationHandlers.ReadGet) // renders read.html
|
notifications.GET("/:id", accountNotificationHandler.ReadGet) // renders read.html
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tickets (auth-required)
|
// Tickets (auth-required)
|
||||||
tickets := r.Group("/account/tickets")
|
tickets := r.Group("/account/tickets")
|
||||||
tickets.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
tickets.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
tickets.GET("/", accountTicketHandlers.List) // GET /account/tickets
|
tickets.GET("/", accountTicketHandler.List) // GET /account/tickets
|
||||||
tickets.GET("/add", accountTicketHandlers.AddGet) // GET /account/tickets/add
|
tickets.GET("/add", accountTicketHandler.AddGet) // GET /account/tickets/add
|
||||||
tickets.POST("/add", accountTicketHandlers.AddPost) // POST /account/tickets/add
|
tickets.POST("/add", accountTicketHandler.AddPost) // POST /account/tickets/add
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
177
internal/platform/services/messages/service.go
Normal file
177
internal/platform/services/messages/service.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// ToDo: not currently used and need to carve out sql
|
||||||
|
package messagesvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
accountMessageHandler "synlotto-website/internal/handlers/account/messages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service implements accountMessageHandler.MessageService.
|
||||||
|
type Service struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Dialect string // "postgres", "mysql", "sqlite" (affects INSERT id retrieval)
|
||||||
|
Now func() time.Time
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||||
|
s := &Service{
|
||||||
|
DB: db,
|
||||||
|
Dialect: "mysql", // sane default for LastInsertId (works for mysql/sqlite)
|
||||||
|
Now: time.Now,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDialect sets SQL dialect hints: "postgres" uses RETURNING id.
|
||||||
|
func WithDialect(d string) func(*Service) { return func(s *Service) { s.Dialect = d } }
|
||||||
|
|
||||||
|
// WithTimeout overrides per-call context timeout.
|
||||||
|
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)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE user_id = ? AND is_archived = FALSE
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []accountMessageHandler.Message
|
||||||
|
for rows.Next() {
|
||||||
|
var m accountMessageHandler.Message
|
||||||
|
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
|
||||||
|
}
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE user_id = ? AND is_archived = TRUE
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []accountMessageHandler.Message
|
||||||
|
for rows.Next() {
|
||||||
|
var m accountMessageHandler.Message
|
||||||
|
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
|
||||||
|
}
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE user_id = ? AND id = ?`
|
||||||
|
var m accountMessageHandler.Message
|
||||||
|
err := s.DB.QueryRowContext(s.rebind(ctx), s.bind(q), userID, id).
|
||||||
|
Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInput) (int64, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
switch s.Dialect {
|
||||||
|
case "postgres":
|
||||||
|
const q = `
|
||||||
|
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, FALSE, FALSE, NOW())
|
||||||
|
RETURNING id`
|
||||||
|
var id int64
|
||||||
|
if err := s.DB.QueryRowContext(ctx, q, userID, "", in.To, in.Subject, in.Body).Scan(&id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
default: // mysql/sqlite
|
||||||
|
const q = `
|
||||||
|
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)`
|
||||||
|
res, err := s.DB.ExecContext(s.rebind(ctx), q, userID, "", in.To, in.Subject, in.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return res.LastInsertId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- small helpers ---
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if s.Dialect != "postgres" {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
// cheap replacer for positional params:
|
||||||
|
n := 0
|
||||||
|
out := make([]byte, 0, len(q)+10)
|
||||||
|
for i := 0; i < len(q); i++ {
|
||||||
|
if q[i] == '?' {
|
||||||
|
n++
|
||||||
|
out = append(out, '$')
|
||||||
|
out = append(out, []byte(intToStr(n))...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, q[i])
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) rebind(ctx context.Context) context.Context { return ctx }
|
||||||
|
|
||||||
|
// intToStr avoids fmt for tiny helper
|
||||||
|
func intToStr(n int) string {
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var b [12]byte
|
||||||
|
i := len(b)
|
||||||
|
for n > 0 {
|
||||||
|
i--
|
||||||
|
b[i] = byte('0' + n%10)
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return string(b[i:])
|
||||||
|
}
|
||||||
80
internal/platform/services/notifications/service.go
Normal file
80
internal/platform/services/notifications/service.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// ToDo: carve out sql
|
||||||
|
package notifysvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Now func() time.Time
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||||
|
s := &Service{
|
||||||
|
DB: db,
|
||||||
|
Now: time.Now,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
||||||
|
|
||||||
|
// List returns newest-first notifications for a user.
|
||||||
|
func (s *Service) List(userID int64) ([]accountNotificationHandler.Notification, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, title, body, is_read, created_at
|
||||||
|
FROM notifications
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
|
||||||
|
rows, err := s.DB.QueryContext(ctx, q, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []accountNotificationHandler.Notification
|
||||||
|
for rows.Next() {
|
||||||
|
var n accountNotificationHandler.Notification
|
||||||
|
if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(userID, id int64) (*accountNotificationHandler.Notification, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, title, body, is_read, created_at
|
||||||
|
FROM notifications
|
||||||
|
WHERE user_id = ? AND id = ?`
|
||||||
|
|
||||||
|
var n accountNotificationHandler.Notification
|
||||||
|
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
||||||
|
Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user