- Backend: Email producer (LavinMQ), EmailService interface - Backend: CRUD API for email_templates and email_settings - Backend: avatar_url field in users table + UpdateMyProfile support - Backend: StorageService for pre-signed URLs - NestJS: Email consumer with Nodemailer and Handlebars - Frontend: Email Templates admin pages (list/edit) - Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow - Frontend: New /post-job public page (company registration + job creation wizard) - Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
85 lines
1.9 KiB
Go
85 lines
1.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
firebase "firebase.google.com/go/v4"
|
|
"firebase.google.com/go/v4/messaging"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
type FCMService struct {
|
|
credentials *CredentialsService
|
|
client *messaging.Client
|
|
currentKey string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewFCMService(c *CredentialsService) *FCMService {
|
|
return &FCMService{credentials: c}
|
|
}
|
|
|
|
func (s *FCMService) getClient(ctx context.Context) (*messaging.Client, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
// Get Service Account JSON from DB
|
|
jsonKey, err := s.credentials.GetDecryptedKey(ctx, "fcm_service_account")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fcm credentials missing: %w", err)
|
|
}
|
|
|
|
// If initialized and key hasn't changed, return cached client
|
|
if s.client != nil && jsonKey == s.currentKey {
|
|
return s.client, nil
|
|
}
|
|
|
|
// Initialize new client
|
|
opt := option.WithCredentialsJSON([]byte(jsonKey))
|
|
app, err := firebase.NewApp(ctx, nil, opt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error initializing firebase app: %w", err)
|
|
}
|
|
|
|
client, err := app.Messaging(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting messaging client: %w", err)
|
|
}
|
|
|
|
s.client = client
|
|
s.currentKey = jsonKey
|
|
return client, nil
|
|
}
|
|
|
|
// SendPush sends a push notification to a specific token
|
|
func (s *FCMService) SendPush(ctx context.Context, token, title, body string, data map[string]string) error {
|
|
client, err := s.getClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
msg := &messaging.Message{
|
|
Token: token,
|
|
Notification: &messaging.Notification{
|
|
Title: title,
|
|
Body: body,
|
|
},
|
|
Data: data,
|
|
}
|
|
|
|
_, err = client.Send(ctx, msg)
|
|
return err
|
|
}
|
|
|
|
// SubscribeToTopic subscribes a token to a topic
|
|
func (s *FCMService) SubscribeToTopic(ctx context.Context, tokens []string, topic string) error {
|
|
client, err := s.getClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = client.SubscribeToTopic(ctx, tokens, topic)
|
|
return err
|
|
}
|