Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,21 @@
|
|||
package inviteemail
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/inviteemailratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/emailer/mailgun"
|
||||
)
|
||||
|
||||
// ProvideSendInviteEmailService provides the send invite email service for Wire DI
|
||||
func ProvideSendInviteEmailService(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
userRepo dom_user.Repository,
|
||||
rateLimitRepo inviteemailratelimit.Repository,
|
||||
emailer mailgun.Emailer,
|
||||
) SendInviteEmailService {
|
||||
return NewSendInviteEmailService(cfg, logger, userRepo, rateLimitRepo, emailer)
|
||||
}
|
||||
234
cloud/maplefile-backend/internal/service/inviteemail/send.go
Normal file
234
cloud/maplefile-backend/internal/service/inviteemail/send.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
// Package inviteemail provides services for sending invitation emails
|
||||
// to non-registered users when someone wants to share a collection with them.
|
||||
package inviteemail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_inviteemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/inviteemail"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/inviteemailratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/emailer/mailgun"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// SendInviteEmailRequestDTO represents the request to send an invitation email
|
||||
type SendInviteEmailRequestDTO struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// SendInviteEmailResponseDTO represents the response after sending an invitation email
|
||||
type SendInviteEmailResponseDTO struct {
|
||||
Success bool `json:"success"`
|
||||
RemainingToday int `json:"remaining_invites_today"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SendInviteEmailService defines the interface for sending invitation emails
|
||||
type SendInviteEmailService interface {
|
||||
Execute(ctx context.Context, inviterID gocql.UUID, req *SendInviteEmailRequestDTO) (*SendInviteEmailResponseDTO, error)
|
||||
}
|
||||
|
||||
type sendInviteEmailServiceImpl struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
userRepo dom_user.Repository
|
||||
rateLimitRepo inviteemailratelimit.Repository
|
||||
emailer mailgun.Emailer
|
||||
maxEmailsPerDay int
|
||||
}
|
||||
|
||||
// NewSendInviteEmailService creates a new instance of the send invite email service
|
||||
func NewSendInviteEmailService(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
userRepo dom_user.Repository,
|
||||
rateLimitRepo inviteemailratelimit.Repository,
|
||||
emailer mailgun.Emailer,
|
||||
) SendInviteEmailService {
|
||||
logger = logger.Named("SendInviteEmailService")
|
||||
|
||||
// Get max emails per day from config, fallback to default
|
||||
maxEmails := cfg.InviteEmail.MaxEmailsPerDay
|
||||
if maxEmails <= 0 {
|
||||
maxEmails = dom_inviteemail.DefaultMaxInviteEmailsPerDay
|
||||
}
|
||||
|
||||
return &sendInviteEmailServiceImpl{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
userRepo: userRepo,
|
||||
rateLimitRepo: rateLimitRepo,
|
||||
emailer: emailer,
|
||||
maxEmailsPerDay: maxEmails,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *sendInviteEmailServiceImpl) Execute(ctx context.Context, inviterID gocql.UUID, req *SendInviteEmailRequestDTO) (*SendInviteEmailResponseDTO, error) {
|
||||
//
|
||||
// STEP 1: Sanitize input
|
||||
//
|
||||
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
|
||||
svc.logger.Debug("Processing invite email request",
|
||||
zap.String("inviter_id", inviterID.String()),
|
||||
zap.String("invited_email", validation.MaskEmail(req.Email)))
|
||||
|
||||
//
|
||||
// STEP 2: Validate input
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req.Email == "" {
|
||||
e["email"] = "Email is required"
|
||||
} else if !validation.IsValidEmail(req.Email) {
|
||||
e["email"] = "Invalid email format"
|
||||
} else if len(req.Email) > 255 {
|
||||
e["email"] = "Email is too long"
|
||||
}
|
||||
|
||||
if len(e) != 0 {
|
||||
svc.logger.Warn("Validation failed", zap.Any("errors", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Get inviter info
|
||||
//
|
||||
|
||||
inviter, err := svc.userRepo.GetByID(ctx, inviterID)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to get inviter info",
|
||||
zap.String("inviter_id", inviterID.String()),
|
||||
zap.Error(err))
|
||||
return nil, httperror.NewForInternalServerError("Failed to process request")
|
||||
}
|
||||
if inviter == nil {
|
||||
svc.logger.Error("Inviter not found",
|
||||
zap.String("inviter_id", inviterID.String()))
|
||||
return nil, httperror.NewForUnauthorizedWithSingleField("user", "User not found")
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 4: Check rate limit
|
||||
//
|
||||
|
||||
today := time.Now().UTC().Truncate(24 * time.Hour)
|
||||
dailyCount, err := svc.rateLimitRepo.GetDailyEmailCount(ctx, inviterID, today)
|
||||
if err != nil {
|
||||
svc.logger.Warn("Failed to get rate limit count, proceeding with caution",
|
||||
zap.String("inviter_id", inviterID.String()),
|
||||
zap.Error(err))
|
||||
// Fail open but log - don't block users due to rate limit DB issues
|
||||
dailyCount = 0
|
||||
}
|
||||
|
||||
if dailyCount >= svc.maxEmailsPerDay {
|
||||
svc.logger.Warn("Rate limit exceeded",
|
||||
zap.String("inviter_id", inviterID.String()),
|
||||
zap.Int("daily_count", dailyCount),
|
||||
zap.Int("max_per_day", svc.maxEmailsPerDay))
|
||||
return &SendInviteEmailResponseDTO{
|
||||
Success: false,
|
||||
RemainingToday: 0,
|
||||
Message: "Daily invitation limit reached. You can send more invitations tomorrow.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 5: Check if recipient already has an account
|
||||
//
|
||||
|
||||
exists, err := svc.userRepo.CheckIfExistsByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to check if user exists",
|
||||
zap.String("email", validation.MaskEmail(req.Email)),
|
||||
zap.Error(err))
|
||||
return nil, httperror.NewForInternalServerError("Failed to process request")
|
||||
}
|
||||
if exists {
|
||||
svc.logger.Debug("User already has account",
|
||||
zap.String("email", validation.MaskEmail(req.Email)))
|
||||
return &SendInviteEmailResponseDTO{
|
||||
Success: false,
|
||||
RemainingToday: svc.maxEmailsPerDay - dailyCount,
|
||||
Message: "This user already has an account. You can share with them directly.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 6: Send invitation email
|
||||
//
|
||||
|
||||
if err := svc.sendInvitationEmail(ctx, inviter.Email, req.Email); err != nil {
|
||||
svc.logger.Error("Failed to send invitation email",
|
||||
zap.String("invited_email", validation.MaskEmail(req.Email)),
|
||||
zap.Error(err))
|
||||
return nil, httperror.NewForInternalServerError("Failed to send invitation email. Please try again.")
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 7: Increment rate limit counter
|
||||
//
|
||||
|
||||
if err := svc.rateLimitRepo.IncrementDailyEmailCount(ctx, inviterID, today); err != nil {
|
||||
svc.logger.Warn("Failed to increment rate limit counter",
|
||||
zap.String("inviter_id", inviterID.String()),
|
||||
zap.Error(err))
|
||||
// Don't fail the request, email was already sent
|
||||
}
|
||||
|
||||
remaining := svc.maxEmailsPerDay - dailyCount - 1
|
||||
|
||||
svc.logger.Info("Invitation email sent successfully",
|
||||
zap.String("inviter_id", inviterID.String()),
|
||||
zap.String("invited_email", validation.MaskEmail(req.Email)),
|
||||
zap.Int("remaining_today", remaining))
|
||||
|
||||
return &SendInviteEmailResponseDTO{
|
||||
Success: true,
|
||||
RemainingToday: remaining,
|
||||
Message: fmt.Sprintf("Invitation sent to %s", req.Email),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *sendInviteEmailServiceImpl) sendInvitationEmail(ctx context.Context, inviterEmail, recipientEmail string) error {
|
||||
frontendURL := svc.emailer.GetFrontendDomainName()
|
||||
registerLink := fmt.Sprintf("%s/register", frontendURL)
|
||||
|
||||
subject := fmt.Sprintf("%s wants to share files with you on MapleFile", inviterEmail)
|
||||
|
||||
htmlContent := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #1e40af;">You've been invited to MapleFile!</h2>
|
||||
<p><strong>%s</strong> wants to share encrypted files with you.</p>
|
||||
<p>MapleFile is a secure, end-to-end encrypted file storage service. To receive the shared files, you'll need to create a free account.</p>
|
||||
<p style="margin: 30px 0;">
|
||||
<a href="%s" style="background-color: #1e40af; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: 500;">
|
||||
Create Your Account
|
||||
</a>
|
||||
</p>
|
||||
<p>Once you've registered, let <strong>%s</strong> know and they can share their files with you.</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
||||
<p style="color: #666; font-size: 14px;">If you didn't expect this invitation, you can safely ignore this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
`, inviterEmail, registerLink, inviterEmail)
|
||||
|
||||
return svc.emailer.Send(ctx, svc.emailer.GetSenderEmail(), subject, recipientEmail, htmlContent)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue