feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Package notifier provides a lightweight HTTP client for the notification
|
||||
// service. All methods are fire-and-forget: errors are logged but never
|
||||
// propagate to the caller, so a notification failure never breaks a render
|
||||
// flow.
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Client sends notifications to the internal notification service endpoint.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
serviceToken string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// New returns a Client pointing at baseURL (e.g. "http://notification-svc:8080")
|
||||
// and authenticating with serviceToken.
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
serviceToken: serviceToken,
|
||||
http: &http.Client{Timeout: 5 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// createNotificationReq mirrors the notification service's CreateNotificationRequest.
|
||||
type createNotificationReq struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
NotificationType string `json:"notification_type"`
|
||||
Priority string `json:"priority"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
RenderJobID *uuid.UUID `json:"render_job_id,omitempty"`
|
||||
ExportID *uuid.UUID `json:"export_id,omitempty"`
|
||||
ActionURL *string `json:"action_url,omitempty"`
|
||||
ActionText *string `json:"action_text,omitempty"`
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
// send posts a notification to the service. Returns an error for caller
|
||||
// awareness, but callers are expected to ignore it.
|
||||
func (c *Client) send(ctx context.Context, req createNotificationReq) error {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notifier marshal: %w", err)
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
c.baseURL+"/v1/internal/notifications", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("notifier new request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.serviceToken)
|
||||
|
||||
resp, err := c.http.Do(httpReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notifier send: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("notifier: status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyRenderDone fires a RenderCompleted notification. Never blocks the
|
||||
// caller — errors are only logged.
|
||||
func (c *Client) NotifyRenderDone(
|
||||
ctx context.Context,
|
||||
userID, tenantID, jobID uuid.UUID,
|
||||
exportID *uuid.UUID,
|
||||
jobName string,
|
||||
) {
|
||||
actionURL := "/dashboard/renders/" + jobID.String()
|
||||
actionText := "مشاهده فایل"
|
||||
req := createNotificationReq{
|
||||
UserID: userID,
|
||||
TenantID: tenantID,
|
||||
NotificationType: "RenderCompleted",
|
||||
Priority: "High",
|
||||
Title: "رندر تکمیل شد 🎉",
|
||||
Message: fmt.Sprintf("پروژه «%s» با موفقیت رندر شد و آماده دانلود است.", jobName),
|
||||
RenderJobID: &jobID,
|
||||
ExportID: exportID,
|
||||
ActionURL: &actionURL,
|
||||
ActionText: &actionText,
|
||||
Channels: []string{"InApp"},
|
||||
}
|
||||
if err := c.send(ctx, req); err != nil {
|
||||
log.Printf("[notifier] RenderDone job=%s err=%v", jobID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyRenderFailed fires a RenderFailed notification. Never blocks the
|
||||
// caller — errors are only logged.
|
||||
func (c *Client) NotifyRenderFailed(
|
||||
ctx context.Context,
|
||||
userID, tenantID, jobID uuid.UUID,
|
||||
jobName, reason string,
|
||||
) {
|
||||
actionURL := "/dashboard/renders/" + jobID.String()
|
||||
actionText := "جزئیات"
|
||||
var msg string
|
||||
if reason != "" {
|
||||
msg = fmt.Sprintf("رندر پروژه «%s» با خطا مواجه شد: %s", jobName, reason)
|
||||
} else {
|
||||
msg = fmt.Sprintf("رندر پروژه «%s» با خطا مواجه شد. لطفاً دوباره تلاش کنید.", jobName)
|
||||
}
|
||||
req := createNotificationReq{
|
||||
UserID: userID,
|
||||
TenantID: tenantID,
|
||||
NotificationType: "RenderFailed",
|
||||
Priority: "High",
|
||||
Title: "خطا در رندر",
|
||||
Message: msg,
|
||||
RenderJobID: &jobID,
|
||||
ActionURL: &actionURL,
|
||||
ActionText: &actionText,
|
||||
Channels: []string{"InApp"},
|
||||
}
|
||||
if err := c.send(ctx, req); err != nil {
|
||||
log.Printf("[notifier] RenderFailed job=%s err=%v", jobID, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user