Skip to content

Commit 7ea8c5e

Browse files
feat(auth): add multilingual support for password notification emails
1 parent c2b5d31 commit 7ea8c5e

9 files changed

Lines changed: 92 additions & 47 deletions

File tree

manifests/bucketeer/charts/web/values.yaml

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,26 @@ oauth:
127127
passwordResetParam:
128128
passwordSetupParam:
129129
templates:
130-
passwordChanged:
131-
subject: "✅ Password Changed Successfully"
132-
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.alert{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>✅ Password Changed Successfully</h1><p>Hello,</p><p>This email confirms that your Bucketeer password has been successfully changed.</p><div class=\"alert\"><strong>Security Notice:</strong> If you did not make this change, please contact your system administrator immediately.</div><p>Thank you for keeping your account secure.</p></div></body></html>"
133-
passwordSetup:
134-
subject: "🔐 Set Up Your Bucketeer Password"
135-
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#007bff;color:white;text-decoration:none;border-radius:5px}.warning{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>Set Up Your Bucketeer Password</h1><p>Hello,</p><p>Your Bucketeer account is ready! To get started, please set up your password by clicking the button below:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{setupURL}}\" class=\"button\">Set Up Password</a></p><p>Or copy and paste this link: {{setupURL}}</p><div class=\"warning\"><strong>Security Note:</strong> This link will expire in {{expirationTime}}. Never share this link with anyone. Choose a strong, unique password.</div></div></body></html>"
136-
passwordReset:
137-
subject: "🔒 Reset Your Bucketeer Password"
138-
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#dc3545;color:white;text-decoration:none;border-radius:5px}.warning{background:#f8d7da;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>🔒 Reset Your Bucketeer Password</h1><p>Hello,</p><p>We received a request to reset your Bucketeer password. Click the button below to create a new password:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{resetURL}}\" class=\"button\">Reset Password</a></p><p>Or copy and paste this link: {{resetURL}}</p><div class=\"warning\"><strong>Security Information:</strong> This link will expire in {{expirationTime}}. If you did not request this password reset, please ignore this email. Your password will remain unchanged until you create a new one.</div></div></body></html>"
130+
en:
131+
passwordChanged:
132+
subject: "✅ Password Changed Successfully"
133+
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.alert{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>✅ Password Changed Successfully</h1><p>Hello,</p><p>This email confirms that your Bucketeer password has been successfully changed.</p><div class=\"alert\"><strong>Security Notice:</strong> If you did not make this change, please contact your system administrator immediately.</div><p>Thank you for keeping your account secure.</p></div></body></html>"
134+
passwordSetup:
135+
subject: "🔐 Set Up Your Bucketeer Password"
136+
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#007bff;color:white;text-decoration:none;border-radius:5px}.warning{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>Set Up Your Bucketeer Password</h1><p>Hello,</p><p>Your Bucketeer account is ready! To get started, please set up your password by clicking the button below:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{setupURL}}\" class=\"button\">Set Up Password</a></p><p>Or copy and paste this link: {{setupURL}}</p><div class=\"warning\"><strong>Security Note:</strong> This link will expire in {{expirationTime}}. Never share this link with anyone. Choose a strong, unique password.</div></div></body></html>"
137+
passwordReset:
138+
subject: "🔒 Reset Your Bucketeer Password"
139+
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#dc3545;color:white;text-decoration:none;border-radius:5px}.warning{background:#f8d7da;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>🔒 Reset Your Bucketeer Password</h1><p>Hello,</p><p>We received a request to reset your Bucketeer password. Click the button below to create a new password:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{resetURL}}\" class=\"button\">Reset Password</a></p><p>Or copy and paste this link: {{resetURL}}</p><div class=\"warning\"><strong>Security Information:</strong> This link will expire in {{expirationTime}}. If you did not request this password reset, please ignore this email. Your password will remain unchanged until you create a new one.</div></div></body></html>"
140+
ja:
141+
passwordChanged:
142+
subject: "✅ パスワードが正常に変更されました"
143+
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.alert{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>✅ パスワードが正常に変更されました</h1><p>こんにちは、</p><p>このメールは、Bucketeerのパスワードが正常に変更されたことを確認するものです。</p><div class=\"alert\"><strong>セキュリティ通知:</strong> この変更を行っていない場合は、すぐにシステム管理者にお問い合わせください。</div><p>アカウントのセキュリティを保つためのご協力をありがとうございます。</p></div></body></html>"
144+
passwordSetup:
145+
subject: "🔐 Bucketeerパスワードのセットアップ"
146+
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#007bff;color:white;text-decoration:none;border-radius:5px}.warning{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>Bucketeerパスワードのセットアップ</h1><p>こんにちは、</p><p>Bucketeerアカウントの準備ができました!開始するには、下のボタンをクリックしてパスワードを設定してください:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{setupURL}}\" class=\"button\">パスワードを設定</a></p><p>または、このリンクをコピーして貼り付けてください:{{setupURL}}</p><div class=\"warning\"><strong>セキュリティ注記:</strong> このリンクは{{expirationTime}}で期限切れになります。このリンクを他の人と共有しないでください。強力でユニークなパスワードを選択してください。</div></div></body></html>"
147+
passwordReset:
148+
subject: "🔒 Bucketeerパスワードのリセット"
149+
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#dc3545;color:white;text-decoration:none;border-radius:5px}.warning{background:#f8d7da;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>🔒 Bucketeerパスワードのリセット</h1><p>こんにちは、</p><p>Bucketeerパスワードのリセット要求を受け取りました。新しいパスワードを作成するには下のボタンをクリックしてください:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{resetURL}}\" class=\"button\">パスワードをリセット</a></p><p>または、このリンクをコピーして貼り付けてください:{{resetURL}}</p><div class=\"warning\"><strong>セキュリティ情報:</strong> このリンクは{{expirationTime}}で期限切れになります。このパスワードリセットを要求していない場合は、このメールを無視してください。新しいパスワードを作成するまで、パスワードは変更されません。</div></div></body></html>"
139150
demoSignIn:
140151
enabled:
141152
email:

pkg/auth/api/api.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func (s *authService) ExchangeToken(
322322
}
323323

324324
// Check and offer password setup for existing users without credentials
325-
s.checkAndOfferPasswordSetup(ctx, userInfo.Email, organizations)
325+
s.checkAndOfferPasswordSetup(ctx, userInfo.Email, organizations, localizer)
326326

327327
return &authproto.ExchangeTokenResponse{Token: token}, nil
328328
}
@@ -1025,6 +1025,7 @@ func (s *authService) checkAndOfferPasswordSetup(
10251025
ctx context.Context,
10261026
email string,
10271027
organizations []*envproto.Organization,
1028+
localizer locale.Localizer,
10281029
) {
10291030
// Only proceed if password auth and email service are enabled
10301031
if !s.config.PasswordAuth.Enabled || !s.config.PasswordAuth.EmailServiceEnabled {
@@ -1051,7 +1052,7 @@ func (s *authService) checkAndOfferPasswordSetup(
10511052
}
10521053

10531054
// User doesn't have password credentials, initiate setup
1054-
err = s.initiatePasswordSetupInternal(ctx, email)
1055+
err = s.initiatePasswordSetupInternal(ctx, email, localizer)
10551056
if err != nil {
10561057
s.logger.Warn("Failed to initiate password setup for OAuth user",
10571058
zap.Error(err),
@@ -1061,7 +1062,11 @@ func (s *authService) checkAndOfferPasswordSetup(
10611062
}
10621063

10631064
// initiatePasswordSetupInternal handles the internal password setup process
1064-
func (s *authService) initiatePasswordSetupInternal(ctx context.Context, email string) error {
1065+
func (s *authService) initiatePasswordSetupInternal(
1066+
ctx context.Context,
1067+
email string,
1068+
localizer locale.Localizer,
1069+
) error {
10651070
// Generate secure setup token
10661071
setupToken, err := auth.GenerateSecureToken()
10671072
if err != nil {
@@ -1094,7 +1099,13 @@ func (s *authService) initiatePasswordSetupInternal(ctx context.Context, email s
10941099
setupURL := fmt.Sprintf("%s%s?%s=%s",
10951100
s.config.PasswordAuth.EmailServiceConfig.BaseURL, setupPath, setupParam, setupToken)
10961101

1097-
err = s.emailService.SendPasswordSetupEmail(ctx, email, setupURL, s.config.PasswordAuth.PasswordSetupTokenTTL)
1102+
err = s.emailService.SendPasswordSetupEmail(
1103+
ctx,
1104+
email,
1105+
setupURL,
1106+
s.config.PasswordAuth.PasswordSetupTokenTTL,
1107+
localizer.GetLocale(),
1108+
)
10981109
if err != nil {
10991110
return fmt.Errorf("failed to send password setup email: %w", err)
11001111
}

pkg/auth/api/password.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func (s *authService) UpdatePassword(
122122

123123
// Send notification email if email service is enabled
124124
if s.config.PasswordAuth.EmailServiceEnabled && s.emailService != nil {
125-
err = s.emailService.SendPasswordChangedNotification(ctx, email)
125+
err = s.emailService.SendPasswordChangedNotification(ctx, email, localizer.GetLocale())
126126
if err != nil {
127127
s.logger.Warn("Failed to send password changed notification",
128128
zap.Error(err),
@@ -220,7 +220,7 @@ func (s *authService) InitiatePasswordSetup(
220220
}
221221
setupURL := fmt.Sprintf("%s%s?%s=%s",
222222
s.config.PasswordAuth.EmailServiceConfig.BaseURL, setupPath, setupParam, setupToken)
223-
err = s.emailService.SendPasswordSetupEmail(ctx, email, setupURL, s.config.PasswordAuth.PasswordSetupTokenTTL)
223+
err = s.emailService.SendPasswordSetupEmail(ctx, email, setupURL, s.config.PasswordAuth.PasswordSetupTokenTTL, localizer.GetLocale())
224224
if err != nil {
225225
s.logger.Error("Failed to send password setup email",
226226
zap.Error(err),
@@ -356,7 +356,7 @@ func (s *authService) SetupPassword(
356356

357357
// Send welcome email if email service is enabled
358358
if s.config.PasswordAuth.EmailServiceEnabled && s.emailService != nil {
359-
err = s.emailService.SendPasswordChangedNotification(ctx, setupToken.Email)
359+
err = s.emailService.SendPasswordChangedNotification(ctx, setupToken.Email, localizer.GetLocale())
360360
if err != nil {
361361
s.logger.Warn("Failed to send password setup completion notification",
362362
zap.Error(err),
@@ -495,7 +495,7 @@ func (s *authService) InitiatePasswordReset(
495495
}
496496
resetURL := fmt.Sprintf("%s%s?%s=%s",
497497
s.config.PasswordAuth.EmailServiceConfig.BaseURL, resetPath, resetParam, resetToken)
498-
err = s.emailService.SendPasswordResetEmail(ctx, email, resetURL, s.config.PasswordAuth.PasswordResetTokenTTL)
498+
err = s.emailService.SendPasswordResetEmail(ctx, email, resetURL, s.config.PasswordAuth.PasswordResetTokenTTL, localizer.GetLocale())
499499
if err != nil {
500500
s.logger.Error("Failed to send password reset email",
501501
zap.Error(err),
@@ -633,7 +633,7 @@ func (s *authService) ResetPassword(
633633

634634
// Send password changed notification email if email service is enabled
635635
if s.config.PasswordAuth.EmailServiceEnabled && s.emailService != nil {
636-
err = s.emailService.SendPasswordChangedNotification(ctx, resetToken.Email)
636+
err = s.emailService.SendPasswordChangedNotification(ctx, resetToken.Email, localizer.GetLocale())
637637
if err != nil {
638638
s.logger.Warn("Failed to send password changed notification",
639639
zap.Error(err),

pkg/auth/email/interface.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import (
2626

2727
// EmailService defines the interface for sending emails
2828
type EmailService interface {
29-
SendPasswordChangedNotification(ctx context.Context, to string) error
30-
SendPasswordSetupEmail(ctx context.Context, to, setupURL string, ttl time.Duration) error
31-
SendPasswordResetEmail(ctx context.Context, to, resetURL string, ttl time.Duration) error
29+
SendPasswordChangedNotification(ctx context.Context, to string, language string) error
30+
SendPasswordSetupEmail(ctx context.Context, to, setupURL string, ttl time.Duration, language string) error
31+
SendPasswordResetEmail(ctx context.Context, to, resetURL string, ttl time.Duration, language string) error
3232
}
3333

3434
// NewEmailService EmailServiceFactory creates an email service based on configuration
@@ -55,25 +55,28 @@ func NewNoOpEmailService(logger *zap.Logger) EmailService {
5555
return &NoOpEmailService{logger: logger}
5656
}
5757

58-
func (s *NoOpEmailService) SendPasswordChangedNotification(ctx context.Context, to string) error {
58+
func (s *NoOpEmailService) SendPasswordChangedNotification(ctx context.Context, to string, language string) error {
5959
s.logger.Info("No-op email service: password changed notification not sent",
6060
zap.String("to", to),
61+
zap.String("language", language),
6162
)
6263
return nil
6364
}
6465

65-
func (s *NoOpEmailService) SendPasswordSetupEmail(ctx context.Context, to, setupURL string, ttl time.Duration) error {
66+
func (s *NoOpEmailService) SendPasswordSetupEmail(ctx context.Context, to, setupURL string, ttl time.Duration, language string) error {
6667
s.logger.Info("No-op email service: password setup email not sent",
6768
zap.String("to", to),
6869
zap.String("setupURL", setupURL),
70+
zap.String("language", language),
6971
)
7072
return nil
7173
}
7274

73-
func (s *NoOpEmailService) SendPasswordResetEmail(ctx context.Context, to, resetURL string, ttl time.Duration) error {
75+
func (s *NoOpEmailService) SendPasswordResetEmail(ctx context.Context, to, resetURL string, ttl time.Duration, language string) error {
7476
s.logger.Info("No-op email service: password reset email not sent",
7577
zap.String("to", to),
7678
zap.String("resetURL", resetURL),
79+
zap.String("language", language),
7780
)
7881
return nil
7982
}

pkg/auth/email/sendgrid.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ func NewSendGridEmailService(config auth.EmailServiceConfig, logger *zap.Logger)
4242
}
4343
}
4444

45-
func (s *SendGridEmailService) SendPasswordChangedNotification(ctx context.Context, to string) error {
46-
subject, body := s.renderer.RenderPasswordChangedEmail()
45+
func (s *SendGridEmailService) SendPasswordChangedNotification(ctx context.Context, to string, language string) error {
46+
subject, body := s.renderer.RenderPasswordChangedEmail(language)
4747

4848
err := s.sendEmail(ctx, to, subject, body)
4949
if err != nil {
@@ -64,8 +64,9 @@ func (s *SendGridEmailService) SendPasswordSetupEmail(
6464
ctx context.Context,
6565
to, setupURL string,
6666
ttl time.Duration,
67+
language string,
6768
) error {
68-
subject, body := s.renderer.RenderPasswordSetupEmail(setupURL, ttl)
69+
subject, body := s.renderer.RenderPasswordSetupEmail(language, setupURL, ttl)
6970

7071
err := s.sendEmail(ctx, to, subject, body)
7172
if err != nil {
@@ -86,8 +87,9 @@ func (s *SendGridEmailService) SendPasswordResetEmail(
8687
ctx context.Context,
8788
to, resetURL string,
8889
ttl time.Duration,
90+
language string,
8991
) error {
90-
subject, body := s.renderer.RenderPasswordResetEmail(resetURL, ttl)
92+
subject, body := s.renderer.RenderPasswordResetEmail(language, resetURL, ttl)
9193

9294
err := s.sendEmail(ctx, to, subject, body)
9395
if err != nil {

pkg/auth/email/ses.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ func NewSESEmailService(emailConfig auth.EmailServiceConfig, logger *zap.Logger)
6262
}, nil
6363
}
6464

65-
func (s *SESEmailService) SendPasswordChangedNotification(ctx context.Context, to string) error {
66-
subject, body := s.renderer.RenderPasswordChangedEmail()
65+
func (s *SESEmailService) SendPasswordChangedNotification(ctx context.Context, to string, language string) error {
66+
subject, body := s.renderer.RenderPasswordChangedEmail(language)
6767

6868
err := s.sendEmail(ctx, to, subject, body)
6969
if err != nil {
@@ -80,8 +80,8 @@ func (s *SESEmailService) SendPasswordChangedNotification(ctx context.Context, t
8080
return nil
8181
}
8282

83-
func (s *SESEmailService) SendPasswordSetupEmail(ctx context.Context, to, setupURL string, ttl time.Duration) error {
84-
subject, body := s.renderer.RenderPasswordSetupEmail(setupURL, ttl)
83+
func (s *SESEmailService) SendPasswordSetupEmail(ctx context.Context, to, setupURL string, ttl time.Duration, language string) error {
84+
subject, body := s.renderer.RenderPasswordSetupEmail(language, setupURL, ttl)
8585

8686
err := s.sendEmail(ctx, to, subject, body)
8787
if err != nil {
@@ -98,8 +98,8 @@ func (s *SESEmailService) SendPasswordSetupEmail(ctx context.Context, to, setupU
9898
return nil
9999
}
100100

101-
func (s *SESEmailService) SendPasswordResetEmail(ctx context.Context, to, resetURL string, ttl time.Duration) error {
102-
subject, body := s.renderer.RenderPasswordResetEmail(resetURL, ttl)
101+
func (s *SESEmailService) SendPasswordResetEmail(ctx context.Context, to, resetURL string, ttl time.Duration, language string) error {
102+
subject, body := s.renderer.RenderPasswordResetEmail(language, resetURL, ttl)
103103

104104
err := s.sendEmail(ctx, to, subject, body)
105105
if err != nil {

0 commit comments

Comments
 (0)