Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Localizer module to localize various strings #359

Merged
merged 9 commits into from
Dec 30, 2023
4 changes: 2 additions & 2 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
pidUser, err := a.Authboss.Storage.Server.Load(r.Context(), pid)
if err == authboss.ErrUserNotFound {
logger.Infof("failed to load user requested by pid: %s", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
data := authboss.HTMLData{authboss.DataErr: a.Localize(r.Context(), authboss.TxtInvalidCredentials)}
return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
} else if err != nil {
return err
Expand All @@ -85,7 +85,7 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
}

logger.Infof("user %s failed to log in", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
data := authboss.HTMLData{authboss.DataErr: a.Localize(r.Context(), authboss.TxtInvalidCredentials)}
return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
}

Expand Down
17 changes: 16 additions & 1 deletion authboss.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ func (a *Authboss) VerifyPassword(user AuthableUser, password string) error {
return a.Core.Hasher.CompareHashAndPassword(user.GetPassword(), password)
}

// Localize is a helper to translate a key using the translator
// If the localizer is nil or returns an empty string,
// then the original text will be returned using [fmt.Sprintf] to interpolate the args.
func (a *Authboss) Localize(ctx context.Context, text string, args ...any) string {
stephenafamo marked this conversation as resolved.
Show resolved Hide resolved
if a.Config.Core.Localizer == nil {
return fmt.Sprintf(text, args...)
}

if translated := a.Config.Core.Localizer.Localize(ctx, text, args...); translated != "" {
return translated
}

return fmt.Sprintf(text, args...)
}

// VerifyPassword uses authboss mechanisms to check that a password is correct.
// Returns nil on success otherwise there will be an error. Simply a helper
// to do the bcrypt comparison.
Expand Down Expand Up @@ -216,7 +231,7 @@ func MountedMiddleware2(ab *Authboss, mountPathed bool, reqs MWRequirements, fai

ro := RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "please re-login",
Failure: ab.Localize(r.Context(), TxtAuthFailed),
RedirectPath: path.Join(ab.Config.Paths.Mount, fmt.Sprintf("/login?%s", vals.Encode())),
}

Expand Down
3 changes: 3 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ type Config struct {
// also implement the ContextLogger to be able to upgrade to a
// request specific logger.
Logger Logger

// Localizer is used to translate strings into different languages.
Localizer Localizer
}
}

Expand Down
12 changes: 6 additions & 6 deletions confirm/confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (c *Confirm) PreventAuth(w http.ResponseWriter, r *http.Request, handled bo
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
Failure: "Your account has not been confirmed, please check your e-mail.",
Failure: c.Localize(r.Context(), authboss.TxtAccountNotConfirmed),
}
return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
Expand All @@ -114,7 +114,7 @@ func (c *Confirm) StartConfirmationWeb(w http.ResponseWriter, r *http.Request, h
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
Success: "Please verify your account, an e-mail has been sent to you.",
Success: c.Localize(r.Context(), authboss.TxtConfirmYourAccount),
}
return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
Expand Down Expand Up @@ -157,7 +157,7 @@ func (c *Confirm) SendConfirmEmail(ctx context.Context, to, token string) {
To: []string{to},
From: c.Config.Mail.From,
FromName: c.Config.Mail.FromName,
Subject: c.Config.Mail.SubjectPrefix + "Confirm New Account",
Subject: c.Config.Mail.SubjectPrefix + c.Localize(ctx, authboss.TxtConfirmEmailSubject),
}

logger.Infof("sending confirm e-mail to: %s", to)
Expand Down Expand Up @@ -236,7 +236,7 @@ func (c *Confirm) Get(w http.ResponseWriter, r *http.Request) error {

ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Success: "You have successfully confirmed your account.",
Success: c.Localize(r.Context(), authboss.TxtConfrimationSuccess),
RedirectPath: c.Authboss.Config.Paths.ConfirmOK,
}
return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
Expand All @@ -256,7 +256,7 @@ func (c *Confirm) mailURL(token string) string {
func (c *Confirm) invalidToken(w http.ResponseWriter, r *http.Request) error {
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "confirm token is invalid",
Failure: c.Localize(r.Context(), authboss.TxtInvalidConfirmToken),
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
}
return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
Expand Down Expand Up @@ -284,7 +284,7 @@ func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
logger.Infof("user %s prevented from accessing %s: not confirmed", user.GetPID(), r.URL.Path)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "Your account has not been confirmed, please check your e-mail.",
Failure: ab.Localize(r.Context(), authboss.TxtAccountNotConfirmed),
RedirectPath: ab.Config.Paths.ConfirmNotOK,
}
if err := ab.Config.Core.Redirector.Redirect(w, r, ro); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions confirm/confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func TestGetValidationFailure(t *testing.T) {
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmNotOK {
t.Error("redir path was wrong:", p)
}
if reason := harness.redirector.Options.Failure; reason != "confirm token is invalid" {
if reason := harness.redirector.Options.Failure; reason != harness.ab.Localize(context.Background(), authboss.TxtInvalidConfirmToken) {
t.Error("reason for failure was wrong:", reason)
}
}
Expand All @@ -262,7 +262,7 @@ func TestGetBase64DecodeFailure(t *testing.T) {
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmNotOK {
t.Error("redir path was wrong:", p)
}
if reason := harness.redirector.Options.Failure; reason != "confirm token is invalid" {
if reason := harness.redirector.Options.Failure; reason != harness.ab.Localize(context.Background(), authboss.TxtInvalidConfirmToken) {
t.Error("reason for failure was wrong:", reason)
}
}
Expand Down Expand Up @@ -294,7 +294,7 @@ func TestGetUserNotFoundFailure(t *testing.T) {
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmNotOK {
t.Error("redir path was wrong:", p)
}
if reason := harness.redirector.Options.Failure; reason != "confirm token is invalid" {
if reason := harness.redirector.Options.Failure; reason != harness.ab.Localize(context.Background(), authboss.TxtInvalidConfirmToken) {
t.Error("reason for failure was wrong:", reason)
}
}
Expand Down
59 changes: 59 additions & 0 deletions localizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package authboss

import "context"

type Localizer interface {
// Get the translation for the given text in the given context.
// If no translation is found, an empty string should be returned.
Localize(ctx context.Context, txt string, args ...any) string
}

// Translation constants
const (
TxtSuccess = "success"

// Used in the auth module
TxtInvalidCredentials = "Invalid Credentials"
stephenafamo marked this conversation as resolved.
Show resolved Hide resolved
TxtAuthFailed = "Please login"

// Used in the register module
TxtUserAlreadyExists = "User already exists"
TxtRegisteredAndLoggedIn = "Account successfully created, you are now logged in"

// Used in the confirm module
TxtConfirmYourAccount = "Please verify your account, an e-mail has been sent to you."
TxtAccountNotConfirmed = "Your account has not been confirmed, please check your e-mail."
TxtInvalidConfirmToken = "Your confirmation token is invalid."
TxtConfrimationSuccess = "You have successfully confirmed your account."
TxtConfirmEmailSubject = "Confirm New Account"

// Used in the lock module
TxtLocked = "Your account has been locked, please contact the administrator."

// Used in the logout module
TxtLoggedOut = "You have been logged out"

// Used in the oauth2 module
TxtOAuth2LoginOK = "Logged in successfully with %s."
TxtOAuth2LoginNotOK = "%s login cancelled or failed"

// Used in the recover module
TxtRecoverInitiateSuccessFlash = "An email has been sent to you with further instructions on how to reset your password."
TxtPasswordResetEmailSubject = "Password Reset"
TxtRecoverSuccessMsg = "Successfully updated password"
TxtRecoverAndLoginSuccessMsg = "Successfully updated password and logged in"

// Used in the otp module
TxtTooManyOTPs = "You cannot have more than %d one time passwords"

// Used in the 2fa module
TxtEmailVerifyTriggered = "An e-mail has been sent to confirm 2FA activation"
TxtEmailVerifySubject = "Add 2FA to Account"
TxtInvalid2FAVerificationToken = "Invalid 2FA email verification token"
Txt2FAAuthorizationRequired = "You must first authorize adding 2fa by e-mail"
TxtInvalid2FACode = "2FA code was invalid"
TxtRepeated2FACode = "2FA code was previously used"
TxtTOTP2FANotActive = "TOTP 2FA is not active"
TxtSMSNumberRequired = "You must provide a phone number"
TxtSMSWaitToResend = "Please wait a few moments before resending the SMS code"
)
4 changes: 2 additions & 2 deletions lock/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (l *Lock) updateLockedState(w http.ResponseWriter, r *http.Request, wasCorr

ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "Your account has been locked, please contact the administrator.",
Failure: l.Localize(r.Context(), authboss.TxtLocked),
RedirectPath: l.Authboss.Config.Paths.LockNotOK,
}
return true, l.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
Expand Down Expand Up @@ -158,7 +158,7 @@ func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
logger.Infof("user %s prevented from accessing %s: locked", user.GetPID(), r.URL.Path)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "Your account has been locked, please contact the administrator.",
Failure: ab.Localize(r.Context(), authboss.TxtLocked),
RedirectPath: ab.Config.Paths.LockNotOK,
}
if err := ab.Config.Core.Redirector.Redirect(w, r, ro); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion logout/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (l *Logout) Logout(w http.ResponseWriter, r *http.Request) error {
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: l.Authboss.Paths.LogoutOK,
Success: "You have been logged out",
Success: authboss.TxtLoggedOut,
}
return l.Authboss.Core.Redirector.Redirect(w, r, ro)
}
50 changes: 24 additions & 26 deletions oauth2/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@
// only the web server flow is supported.
//
// The general flow looks like this:
// 1. User goes to Start handler and has his session packed with goodies
// then redirects to the OAuth service.
// 2. OAuth service returns to OAuthCallback which extracts state and
// parameters and generally checks that everything is ok. It uses the
// token received to get an access token from the oauth2 library
// 3. Calls the OAuth2Provider.FindUserDetails which should return the user's
// details in a generic form.
// 4. Passes the user details into the OAuth2ServerStorer.NewFromOAuth2 in
// order to create a user object we can work with.
// 5. Saves the user in the database, logs them in, redirects.
// 1. User goes to Start handler and has his session packed with goodies
// then redirects to the OAuth service.
// 2. OAuth service returns to OAuthCallback which extracts state and
// parameters and generally checks that everything is ok. It uses the
// token received to get an access token from the oauth2 library
// 3. Calls the OAuth2Provider.FindUserDetails which should return the user's
// details in a generic form.
// 4. Passes the user details into the OAuth2ServerStorer.NewFromOAuth2 in
// order to create a user object we can work with.
// 5. Saves the user in the database, logs them in, redirects.
//
// In order to do this there are a number of parts:
// 1. The configuration of a provider
// (handled by authboss.Config.Modules.OAuth2Providers).
// 2. The flow of redirection of client, parameter passing etc
// (handled by this package)
// 3. The HTTP call to the service once a token has been retrieved to
// get user details (handled by OAuth2Provider.FindUserDetails)
// 4. The creation of a user from the user details returned from the
// FindUserDetails (authboss.OAuth2ServerStorer)
// 5. The special casing of the ServerStorer implementation's Load()
// function to deal properly with incoming OAuth2 pids. See
// authboss.ParseOAuth2PID as a way to do this.
// 1. The configuration of a provider
// (handled by authboss.Config.Modules.OAuth2Providers).
// 2. The flow of redirection of client, parameter passing etc
// (handled by this package)
// 3. The HTTP call to the service once a token has been retrieved to
// get user details (handled by OAuth2Provider.FindUserDetails)
// 4. The creation of a user from the user details returned from the
// FindUserDetails (authboss.OAuth2ServerStorer)
// 5. The special casing of the ServerStorer implementation's Load()
// function to deal properly with incoming OAuth2 pids. See
// authboss.ParseOAuth2PID as a way to do this.
//
// Of these parts, the responsibility of the authboss library consumer
// is on 1, 3, 4, and 5. Configuration of providers that should be used is
Expand Down Expand Up @@ -61,9 +61,7 @@ const (
FormValueOAuth2Redir = "redir"
)

var (
errOAuthStateValidation = errors.New("could not validate oauth2 state param")
)
var errOAuthStateValidation = errors.New("could not validate oauth2 state param")

// OAuth2 module
type OAuth2 struct {
Expand Down Expand Up @@ -214,7 +212,7 @@ func (o *OAuth2) End(w http.ResponseWriter, r *http.Request) error {
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: o.Authboss.Config.Paths.OAuth2LoginNotOK,
Failure: fmt.Sprintf("%s login cancelled or failed", strings.Title(provider)),
Failure: o.Localize(r.Context(), authboss.TxtOAuth2LoginNotOK, provider),
}
return o.Authboss.Core.Redirector.Redirect(w, r, ro)
}
Expand Down Expand Up @@ -292,7 +290,7 @@ func (o *OAuth2) End(w http.ResponseWriter, r *http.Request) error {
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: redirect,
Success: fmt.Sprintf("Logged in successfully with %s.", strings.Title(provider)),
Success: o.Localize(r.Context(), authboss.TxtOAuth2LoginOK, provider),
}
return o.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
Expand Down
6 changes: 3 additions & 3 deletions otp/otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (o *OTP) LoginPost(w http.ResponseWriter, r *http.Request) error {
pidUser, err := o.Authboss.Storage.Server.Load(r.Context(), pid)
if err == authboss.ErrUserNotFound {
logger.Infof("failed to load user requested by pid: %s", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
data := authboss.HTMLData{authboss.DataErr: o.Localize(r.Context(), authboss.TxtInvalidCredentials)}
return o.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
} else if err != nil {
return err
Expand Down Expand Up @@ -153,7 +153,7 @@ func (o *OTP) LoginPost(w http.ResponseWriter, r *http.Request) error {
}

logger.Infof("user %s failed to log in with otp", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
data := authboss.HTMLData{authboss.DataErr: o.Localize(r.Context(), authboss.TxtInvalidCredentials)}
return o.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
}

Expand Down Expand Up @@ -218,7 +218,7 @@ func (o *OTP) AddPost(w http.ResponseWriter, r *http.Request) error {
currentOTPs := splitOTPs(otpUser.GetOTPs())

if len(currentOTPs) >= maxOTPs {
data := authboss.HTMLData{authboss.DataValidation: fmt.Sprintf("you cannot have more than %d one time passwords", maxOTPs)}
data := authboss.HTMLData{authboss.DataValidation: o.Localize(r.Context(), authboss.TxtTooManyOTPs, maxOTPs)}
return o.Core.Responder.Respond(w, r, http.StatusOK, PageAdd, data)
}

Expand Down
8 changes: 5 additions & 3 deletions otp/twofactor/sms2fa/sms.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ func (s *SMS) PostSetup(w http.ResponseWriter, r *http.Request) error {
number := smsVals.GetPhoneNumber()
if len(number) == 0 {
data := authboss.HTMLData{
authboss.DataValidation: map[string][]string{FormValuePhoneNumber: {"must provide a phone number"}},
authboss.DataValidation: map[string][]string{FormValuePhoneNumber: {
s.Localize(r.Context(), authboss.TxtSMSNumberRequired),
}},
}
return s.Core.Responder.Respond(w, r, http.StatusOK, PageSMSSetup, data)
}
Expand Down Expand Up @@ -355,7 +357,7 @@ func (s *SMSValidator) sendCode(w http.ResponseWriter, r *http.Request, user Use
var data authboss.HTMLData
err := s.SendCodeToUser(w, r, user.GetPID(), phoneNumber)
if err == errSMSRateLimit {
data = authboss.HTMLData{authboss.DataErr: "please wait a few moments before resending SMS code"}
data = authboss.HTMLData{authboss.DataErr: s.Localize(r.Context(), authboss.TxtSMSWaitToResend)}
} else if err != nil {
return err
}
Expand Down Expand Up @@ -401,7 +403,7 @@ func (s *SMSValidator) validateCode(w http.ResponseWriter, r *http.Request, user

logger.Infof("user %s sms 2fa failure (wrong code)", user.GetPID())
data := authboss.HTMLData{
authboss.DataValidation: map[string][]string{FormValueCode: {"2fa code was invalid"}},
authboss.DataValidation: map[string][]string{FormValueCode: {s.Localize(r.Context(), authboss.TxtInvalid2FACode)}},
}
return s.Authboss.Core.Responder.Respond(w, r, http.StatusOK, s.Page, data)
}
Expand Down
6 changes: 3 additions & 3 deletions otp/twofactor/sms2fa/sms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func TestPostSetup(t *testing.T) {
t.Error("page wrong:", h.responder.Page)
}
validation := h.responder.Data[authboss.DataValidation].(map[string][]string)
if got := validation[FormValuePhoneNumber][0]; got != "must provide a phone number" {
if got := validation[FormValuePhoneNumber][0]; got != h.ab.Localize(context.Background(), authboss.TxtSMSNumberRequired) {
t.Error("data wrong:", got)
}
})
Expand Down Expand Up @@ -547,7 +547,7 @@ func TestValidatorPostOk(t *testing.T) {
w.WriteHeader(http.StatusOK)

validation := h.responder.Data[authboss.DataValidation].(map[string][]string)
if got := validation[FormValueCode][0]; got != "2fa code was invalid" {
if got := validation[FormValueCode][0]; got != h.ab.Localize(context.Background(), authboss.TxtInvalid2FACode) {
t.Error("data wrong:", got)
}
})
Expand All @@ -574,7 +574,7 @@ func TestValidatorPostOk(t *testing.T) {
t.Error("page wrong:", h.responder.Page)
}
validation := h.responder.Data[authboss.DataValidation].(map[string][]string)
if got := validation[FormValueCode][0]; got != "2fa code was invalid" {
if got := validation[FormValueCode][0]; got != h.ab.Localize(context.Background(), authboss.TxtInvalid2FACode) {
t.Error("data wrong:", got)
}
})
Expand Down
Loading
Loading