feat: add admin apis

This commit is contained in:
Krispire Admin
2026-06-16 14:47:45 +05:30
parent aa61f4f0b1
commit a49bf7591e
24 changed files with 1164 additions and 22 deletions
+77
View File
@@ -612,6 +612,66 @@ func (s *NotificationService) publishPush(ctx context.Context, recipientID, kind
})
}
// NotifyAdminMessage delivers a free-text message from an admin to one user:
// a persisted in-app notification (kind 'admin_message') plus best-effort
// realtime, Web Push, and email. Admin messages bypass the per-kind opt-out
// matrix (there's no category for them) — they're deliberate, low-volume, and
// operationally important. Returns an error only if the row couldn't be
// persisted; the three delivery channels are best-effort.
func (s *NotificationService) NotifyAdminMessage(ctx context.Context, recipientID, title, body string) error {
if err := s.notifs.CreateAdminMessage(ctx, recipientID, body); err != nil {
return err
}
url := s.frontendURL + "/notifications"
if s.rt != nil {
if err := s.rt.Trigger(ctx, "notifications."+recipientID, "notification", map[string]any{
"kind": "admin_message",
"body": body,
}); err != nil {
s.logger.Warn("realtime admin message publish failed",
zap.String("recipient", recipientID), zap.Error(err))
}
}
if s.push != nil {
pushTitle := strings.TrimSpace(title)
if pushTitle == "" {
pushTitle = "Prabhupāda Says"
}
s.push.SendToUser(ctx, recipientID, PushPayload{
Title: pushTitle,
Body: body,
URL: url,
Icon: s.iconURL(),
Tag: "admin_message",
})
}
// Email — best-effort, only when the recipient has a verified-or-not address.
recipient, _ := s.users.FindByID(ctx, recipientID)
if recipient != nil && recipient.Email != nil && strings.TrimSpace(*recipient.Email) != "" {
var buf bytes.Buffer
if err := adminMessageEmailTpl.Execute(&buf, map[string]any{
"RecipientName": recipient.Name,
"Title": strings.TrimSpace(title),
"Body": body,
"URL": url,
}); err != nil {
s.logger.Warn("render admin message email failed", zap.Error(err))
} else {
subject := strings.TrimSpace(title)
if subject == "" {
subject = "A message from Prabhupāda Says"
}
if err := s.mail.Send(strings.TrimSpace(*recipient.Email), subject, buf.String()); err != nil {
s.logger.Warn("admin message email send failed", zap.Error(err))
}
}
}
return nil
}
// iconURL is the absolute URL of the notification icon (the app's square
// icon), so it renders regardless of the client's base path.
func (s *NotificationService) iconURL() string {
@@ -715,3 +775,20 @@ var likeEmailTpl = template.Must(template.New("postLike").Parse(`<div style="fon
<p style="margin-top:1em">Your servants,<br>
<strong>The Prabhupāda Says team</strong></p>
</div>`))
// adminMessageEmailTpl renders an admin → user direct message. Body is
// auto-escaped by html/template, so admin-entered text can't inject markup.
var adminMessageEmailTpl = template.Must(template.New("adminMessage").Parse(`<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#2a2118;max-width:560px;margin:0 auto;font-size:17px;line-height:1.7">
<p style="font-size:18px;color:#9a6b2f;letter-spacing:0.04em;margin-bottom:1em">Hare Kṛṣṇa{{if .RecipientName}}, {{.RecipientName}}{{end}} 🙏</p>
{{if .Title}}<p style="font-weight:600;font-size:19px;margin-bottom:0.5em">{{.Title}}</p>{{end}}
<p style="white-space:pre-line">{{.Body}}</p>
<p style="margin:1.25em 0">
<a href="{{.URL}}" style="display:inline-block;background:#9a6b2f;color:#fff;text-decoration:none;padding:0.6em 1.3em;border-radius:8px;font-weight:600">Open Prabhupāda Says</a>
</p>
<p style="margin-top:1em">Your servants,<br>
<strong>The Prabhupāda Says team</strong></p>
</div>`))