mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
This commit is contained in:
parent
0293736a08
commit
a2e8592dd5
9 changed files with 116 additions and 87 deletions
|
|
@ -33,8 +33,10 @@ func TestMain(m *testing.M) {
|
|||
c := config.TestConfig()
|
||||
get.SetConfig(c)
|
||||
|
||||
// Increase login rate limit for testing.
|
||||
// Increase the login and authentication rate limits for testing so the many
|
||||
// failed-auth cases across the suite don't exhaust the shared per-IP buckets.
|
||||
limiter.Login = limiter.NewLimit(1, 10000)
|
||||
limiter.Auth = limiter.NewLimit(1, 10000)
|
||||
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func OIDCLogin(router *gin.RouterGroup) {
|
|||
|
||||
// Abort if failure rate limit is exceeded.
|
||||
if r.Reject() || limiter.Auth.Reject(clientIp) {
|
||||
c.HTML(http.StatusTooManyRequests, "auth.gohtml", CreateSessionError(http.StatusTooManyRequests, i18n.Error(i18n.ErrTooManyRequests)))
|
||||
c.HTML(http.StatusTooManyRequests, "auth.gohtml", CreateSessionError(http.StatusTooManyRequests, i18n.ErrTooManyRequests))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ func OIDCLogin(router *gin.RouterGroup) {
|
|||
|
||||
if provider == nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrInvalidProviderConfiguration.Error()})
|
||||
c.HTML(http.StatusInternalServerError, "auth.gohtml", CreateSessionError(http.StatusInternalServerError, i18n.Error(i18n.ErrConnectionFailed)))
|
||||
c.HTML(http.StatusInternalServerError, "auth.gohtml", CreateSessionError(http.StatusInternalServerError, i18n.ErrConnectionFailed))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
|
||||
// Abort if failure rate limit is exceeded.
|
||||
if r.Reject() || limiter.Auth.Reject(clientIp) {
|
||||
c.HTML(http.StatusTooManyRequests, "auth.gohtml", CreateSessionError(http.StatusTooManyRequests, i18n.Error(i18n.ErrTooManyRequests)))
|
||||
c.HTML(http.StatusTooManyRequests, "auth.gohtml", CreateSessionError(http.StatusTooManyRequests, i18n.ErrTooManyRequests))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
// access_denied). Surface it in the instance's branded UI, not a silent bounce.
|
||||
if oauthErr := c.Query("error"); oauthErr != "" {
|
||||
event.AuditWarn([]string{clientIp, "create session", "oidc", "provider returned error", clean.Log(oauthErr), clean.Log(c.Query("error_description"))})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(oidcRedirectErrorMessage(oauthErr))))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, oidcRedirectErrorMessage(oauthErr)))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
|
||||
if provider == nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrInvalidProviderConfiguration.Error()})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
// raw, dead-end error with no way forward.
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", claimErr.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, claimErr.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrUnexpected)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrUnexpected))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -171,13 +171,13 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
message := "IdP omitted some or all groups; cannot validate required groups"
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", message})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, message)
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrForbidden)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrForbidden))
|
||||
return
|
||||
case !oidc.HasAnyGroup(groups, requiredGroups):
|
||||
message := "missing required group membership"
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", message})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, message)
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrForbidden)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrForbidden))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -205,13 +205,13 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
} else if _, emailDomain, _ := strings.Cut(userEmail, "@"); emailDomain == "" || !userInfo.EmailVerified {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrVerifiedEmailRequired.Error()})
|
||||
event.LoginError(clientIp, "oidc", userEmail, userAgent, authn.ErrVerifiedEmailRequired.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrForbidden)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrVerifiedEmailRequired))
|
||||
return
|
||||
} else if !strings.HasSuffix("."+emailDomain, "."+domain) {
|
||||
message := fmt.Sprintf("domain must match '%s'", domain)
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userEmail, message})
|
||||
event.LoginError(clientIp, "oidc", userEmail, userAgent, message)
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrForbidden)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrForbidden))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -219,19 +219,19 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
if oidcUser := entity.OidcUser(userInfo, provider.Issuer(), oidc.Username(userInfo, conf.OIDCUsername())); authn.ProviderOIDC.NotEqual(oidcUser.AuthProvider) {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrAuthProviderIsNotOIDC.Error()})
|
||||
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrAuthProviderIsNotOIDC.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
} else if oidcUser.UserName == "" {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrUsernameRequiredToRegister.Error()})
|
||||
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrUsernameRequiredToRegister.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
} else if user = entity.FindUser(oidcUser); user != nil {
|
||||
// Ensure user has a username.
|
||||
if user.Username() == "" {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", oidcUser.UserName, authn.ErrUsernameRequired.Error()})
|
||||
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrUsernameRequired.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
case !user.CanLogIn():
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountDisabled.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountDisabled.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
case authn.ProviderOIDC.NotEqual(user.AuthProvider):
|
||||
// Claimable collision: a non-OIDC account matched the login. Log the
|
||||
|
|
@ -253,12 +253,12 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
hint := oidcReconcileHint(userName, user.AuthProvider, oidcUser.AuthID)
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAuthProviderIsNotOIDC.Error(), hint})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAuthProviderIsNotOIDC.Error()+": "+hint)
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
case user.AuthID == "" || oidcUser.AuthID == "" || user.AuthID != oidcUser.AuthID:
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrInvalidAuthID.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrInvalidAuthID.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +340,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
if err = user.Save(); err != nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountUpdateFailed.Error(), err.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountUpdateFailed.Error()+" ("+err.Error()+")")
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +354,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
userName = oidcUser.Username()
|
||||
event.AuditWarn([]string{clientIp, "create session", "oidc", "create user", userName, authn.ErrUsersQuotaExceeded.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrUsersQuotaExceeded.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrQuotaExceeded)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrQuotaExceeded))
|
||||
return
|
||||
} else if conf.OIDCRegister() {
|
||||
// Create new user record.
|
||||
|
|
@ -430,12 +430,12 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
if err = user.Create(); err != nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountCreateFailed.Error(), err.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountCreateFailed.Error()+" ("+err.Error()+")")
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
} else if err = user.UpdateAuthID(userInfo.Subject, provider.Issuer()); err != nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountUpdateFailed.Error(), err.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountUpdateFailed.Error()+" ("+err.Error()+")")
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -448,7 +448,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
} else {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrRegistrationDisabled.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrRegistrationDisabled.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrRegistrationDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -456,7 +456,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
if !user.CanLogIn() {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountDisabled.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountDisabled.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -495,11 +495,11 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||
// Save session after successful authentication.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, status.Error(err)})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrInvalidCredentials))
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, status.Failed})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrUnexpected)))
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.ErrUnexpected))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ func TestOIDCRedirect_ProviderError(t *testing.T) {
|
|||
// auth.gohtml renders the failed-status branch and stores the branded message.
|
||||
assert.Contains(t, body, `setItem("session.error"`)
|
||||
assert.Contains(t, body, i18n.Error(i18n.ErrForbidden).Error())
|
||||
// The page also carries the message key (messageId) so the Web UI can render
|
||||
// it in the current UI locale; notify.vue applies Tp to the source string.
|
||||
assert.Contains(t, body, `setItem("session.messageId"`)
|
||||
assert.Contains(t, body, i18n.Source(i18n.ErrForbidden))
|
||||
}
|
||||
|
||||
// extractTemplateList returns the template source between the provided markers.
|
||||
|
|
|
|||
|
|
@ -110,13 +110,15 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
if err = sess.LogIn(frm, c); err != nil {
|
||||
switch {
|
||||
case sess.GetMethod().IsNot(authn.Method2FA):
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
Abort(c, sess.HttpStatus(), i18n.ErrInvalidCredentials)
|
||||
case errors.Is(err, authn.ErrPasscodeRequired):
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error(), "code": 32, "message": i18n.Msg(i18n.ErrPasscodeRequired)})
|
||||
// Code 32 asks the client to enter a 2FA passcode (a continuation request,
|
||||
// not a failure), so the text goes in "message" (code < 400) with messageId.
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(32, i18n.ErrPasscodeRequired))
|
||||
// Return the reserved request rate limit tokens if password is correct, even if the verification code is missing.
|
||||
r.Success()
|
||||
default:
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error(), "code": http.StatusUnauthorized, "message": i18n.Msg(i18n.ErrInvalidPasscode)})
|
||||
Abort(c, http.StatusUnauthorized, i18n.ErrInvalidPasscode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -131,10 +133,10 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
switch saved, saveErr := get.Session().Save(sess); {
|
||||
case saveErr != nil:
|
||||
event.AuditErr([]string{clientIp, status.Error(saveErr)})
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
Abort(c, sess.HttpStatus(), i18n.ErrInvalidCredentials)
|
||||
return
|
||||
case saved == nil:
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
|
||||
Abort(c, sess.HttpStatus(), i18n.ErrUnexpected)
|
||||
return
|
||||
case isNew:
|
||||
event.AuditInfo([]string{clientIp, "session %s", "created"}, saved.RefID)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
)
|
||||
|
||||
// CreateSessionResponse returns the authentication response data for POST requests
|
||||
|
|
@ -14,13 +15,18 @@ func CreateSessionResponse(authToken string, sess *entity.Session, conf *config.
|
|||
return GetSessionResponse(authToken, sess, conf)
|
||||
}
|
||||
|
||||
// CreateSessionError returns an authentication error response.
|
||||
func CreateSessionError(code int, err error) gin.H {
|
||||
// CreateSessionError returns an authentication error response. "error" is the
|
||||
// instance-locale fallback; "messageId" (the English source) and "messageParams"
|
||||
// let the Web UI render the message in the current UI locale.
|
||||
func CreateSessionError(code int, id i18n.Message, params ...any) gin.H {
|
||||
resp := i18n.NewResponse(code, id, params...)
|
||||
return gin.H{
|
||||
"status": StatusFailed,
|
||||
"code": code,
|
||||
"error": err.Error(),
|
||||
"config": get.Config().ClientPublic(),
|
||||
"status": StatusFailed,
|
||||
"code": resp.Code,
|
||||
"error": resp.Error,
|
||||
"messageId": resp.MessageID,
|
||||
"messageParams": resp.MessageParams,
|
||||
"config": get.Config().ClientPublic(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ func TestCreateSession(t *testing.T) {
|
|||
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, i18n.Msg(i18n.ErrInvalidCredentials), val.String())
|
||||
assert.Equal(t, i18n.Source(i18n.ErrInvalidCredentials), gjson.Get(r.Body.String(), "messageId").String())
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
})
|
||||
t.Run("AliceSuccess", func(t *testing.T) {
|
||||
|
|
@ -231,6 +232,7 @@ func TestCreateSession(t *testing.T) {
|
|||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "bob", "password": "helloworld"}`)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, i18n.Msg(i18n.ErrInvalidCredentials), val.String())
|
||||
assert.Equal(t, i18n.Source(i18n.ErrInvalidCredentials), gjson.Get(r.Body.String(), "messageId").String())
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
})
|
||||
t.Run("TwoFaPasscodeRequired", func(t *testing.T) {
|
||||
|
|
@ -246,6 +248,11 @@ func TestCreateSession(t *testing.T) {
|
|||
assert.Equal(t, "", userEmail.String())
|
||||
assert.Equal(t, "", userName.String())
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
// Code 32 is a continuation request, not an error: the text is in "message", not "error".
|
||||
assert.Equal(t, int64(32), gjson.Get(r.Body.String(), "code").Int())
|
||||
assert.Equal(t, i18n.Msg(i18n.ErrPasscodeRequired), gjson.Get(r.Body.String(), "message").String())
|
||||
assert.Empty(t, gjson.Get(r.Body.String(), "error").String())
|
||||
assert.Equal(t, i18n.Source(i18n.ErrPasscodeRequired), gjson.Get(r.Body.String(), "messageId").String())
|
||||
})
|
||||
t.Run("TwoFaInvalidPasscode", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ func TestSource(t *testing.T) {
|
|||
t.Run("WithoutPlaceholder", func(t *testing.T) {
|
||||
assert.Equal(t, "Permission denied", Source(ErrForbidden))
|
||||
})
|
||||
t.Run("AuthErrorMessages", func(t *testing.T) {
|
||||
assert.Equal(t, "Registration disabled", Source(ErrRegistrationDisabled))
|
||||
assert.Equal(t, "Verified email required", Source(ErrVerifiedEmailRequired))
|
||||
})
|
||||
t.Run("UntranslatedAfterSetLocale", func(t *testing.T) {
|
||||
SetLocale("de")
|
||||
assert.Equal(t, "%s already exists", Source(ErrAlreadyExists))
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ const (
|
|||
ErrTooManyRequests
|
||||
ErrInsufficientStorage
|
||||
ErrQuotaExceeded
|
||||
ErrRegistrationDisabled
|
||||
ErrVerifiedEmailRequired
|
||||
|
||||
MsgChangesSaved
|
||||
MsgAlbumCreated
|
||||
|
|
@ -107,57 +109,59 @@ const (
|
|||
// Messages holds default English message strings.
|
||||
var Messages = MessageMap{
|
||||
// Error messages:
|
||||
ErrUnexpected: gettext("Something went wrong, try again"),
|
||||
ErrBadRequest: gettext("Unable to do that"),
|
||||
ErrSaveFailed: gettext("Changes could not be saved"),
|
||||
ErrDeleteFailed: gettext("Could not be deleted"),
|
||||
ErrAlreadyExists: gettext("%s already exists"),
|
||||
ErrNotFound: gettext("Not found"),
|
||||
ErrFileNotFound: gettext("File not found"),
|
||||
ErrFileTooLarge: gettext("File too large"),
|
||||
ErrUnsupported: gettext("Unsupported"),
|
||||
ErrUnsupportedType: gettext("Unsupported type"),
|
||||
ErrUnsupportedFormat: gettext("Unsupported format"),
|
||||
ErrOriginalsEmpty: gettext("Originals folder is empty"),
|
||||
ErrSelectionNotFound: gettext("Selection not found"),
|
||||
ErrEntityNotFound: gettext("Entity not found"),
|
||||
ErrAccountNotFound: gettext("Account not found"),
|
||||
ErrUserNotFound: gettext("User not found"),
|
||||
ErrLabelNotFound: gettext("Label not found"),
|
||||
ErrCameraNotFound: gettext("Camera not found"),
|
||||
ErrLensNotFound: gettext("Lens not found"),
|
||||
ErrAlbumNotFound: gettext("Album not found"),
|
||||
ErrSubjectNotFound: gettext("Subject not found"),
|
||||
ErrPersonNotFound: gettext("Person not found"),
|
||||
ErrFaceNotFound: gettext("Face not found"),
|
||||
ErrPublic: gettext("Not available in public mode"),
|
||||
ErrReadOnly: gettext("Not available in read-only mode"),
|
||||
ErrUnauthorized: gettext("Please log in to your account"),
|
||||
ErrForbidden: gettext("Permission denied"),
|
||||
ErrPaymentRequired: gettext("Payment required"),
|
||||
ErrOffensiveUpload: gettext("Upload might be offensive"),
|
||||
ErrUploadFailed: gettext("Upload failed"),
|
||||
ErrNoItemsSelected: gettext("No items selected"),
|
||||
ErrCreateFile: gettext("Failed creating file, please check permissions"),
|
||||
ErrCreateFolder: gettext("Failed creating folder, please check permissions"),
|
||||
ErrConnectionFailed: gettext("Could not connect, please try again"),
|
||||
ErrPasscodeRequired: gettext("Enter verification code"),
|
||||
ErrInvalidPasscode: gettext("Invalid verification code, please try again"),
|
||||
ErrInvalidPassword: gettext("Invalid password, please try again"),
|
||||
ErrFeatureDisabled: gettext("Feature disabled"),
|
||||
ErrNoLabelsSelected: gettext("No labels selected"),
|
||||
ErrNoAlbumsSelected: gettext("No albums selected"),
|
||||
ErrNoFilesForDownload: gettext("No files available for download"),
|
||||
ErrZipFailed: gettext("Failed to create zip file"),
|
||||
ErrInvalidCredentials: gettext("Invalid credentials"),
|
||||
ErrInvalidLink: gettext("Invalid link"),
|
||||
ErrInvalidName: gettext("Invalid name"),
|
||||
ErrBusy: gettext("Busy, please try again later"),
|
||||
ErrWakeupInterval: gettext("The wakeup interval is %s, but must be 1h or less"),
|
||||
ErrAccountConnect: gettext("Your account could not be connected"),
|
||||
ErrTooManyRequests: gettext("Too many requests"),
|
||||
ErrInsufficientStorage: gettext("Insufficient storage"),
|
||||
ErrQuotaExceeded: gettext("Quota exceeded"),
|
||||
ErrUnexpected: gettext("Something went wrong, try again"),
|
||||
ErrBadRequest: gettext("Unable to do that"),
|
||||
ErrSaveFailed: gettext("Changes could not be saved"),
|
||||
ErrDeleteFailed: gettext("Could not be deleted"),
|
||||
ErrAlreadyExists: gettext("%s already exists"),
|
||||
ErrNotFound: gettext("Not found"),
|
||||
ErrFileNotFound: gettext("File not found"),
|
||||
ErrFileTooLarge: gettext("File too large"),
|
||||
ErrUnsupported: gettext("Unsupported"),
|
||||
ErrUnsupportedType: gettext("Unsupported type"),
|
||||
ErrUnsupportedFormat: gettext("Unsupported format"),
|
||||
ErrOriginalsEmpty: gettext("Originals folder is empty"),
|
||||
ErrSelectionNotFound: gettext("Selection not found"),
|
||||
ErrEntityNotFound: gettext("Entity not found"),
|
||||
ErrAccountNotFound: gettext("Account not found"),
|
||||
ErrUserNotFound: gettext("User not found"),
|
||||
ErrLabelNotFound: gettext("Label not found"),
|
||||
ErrCameraNotFound: gettext("Camera not found"),
|
||||
ErrLensNotFound: gettext("Lens not found"),
|
||||
ErrAlbumNotFound: gettext("Album not found"),
|
||||
ErrSubjectNotFound: gettext("Subject not found"),
|
||||
ErrPersonNotFound: gettext("Person not found"),
|
||||
ErrFaceNotFound: gettext("Face not found"),
|
||||
ErrPublic: gettext("Not available in public mode"),
|
||||
ErrReadOnly: gettext("Not available in read-only mode"),
|
||||
ErrUnauthorized: gettext("Please log in to your account"),
|
||||
ErrForbidden: gettext("Permission denied"),
|
||||
ErrPaymentRequired: gettext("Payment required"),
|
||||
ErrOffensiveUpload: gettext("Upload might be offensive"),
|
||||
ErrUploadFailed: gettext("Upload failed"),
|
||||
ErrNoItemsSelected: gettext("No items selected"),
|
||||
ErrCreateFile: gettext("Failed creating file, please check permissions"),
|
||||
ErrCreateFolder: gettext("Failed creating folder, please check permissions"),
|
||||
ErrConnectionFailed: gettext("Could not connect, please try again"),
|
||||
ErrPasscodeRequired: gettext("Enter verification code"),
|
||||
ErrInvalidPasscode: gettext("Invalid verification code, please try again"),
|
||||
ErrInvalidPassword: gettext("Invalid password, please try again"),
|
||||
ErrFeatureDisabled: gettext("Feature disabled"),
|
||||
ErrNoLabelsSelected: gettext("No labels selected"),
|
||||
ErrNoAlbumsSelected: gettext("No albums selected"),
|
||||
ErrNoFilesForDownload: gettext("No files available for download"),
|
||||
ErrZipFailed: gettext("Failed to create zip file"),
|
||||
ErrInvalidCredentials: gettext("Invalid credentials"),
|
||||
ErrInvalidLink: gettext("Invalid link"),
|
||||
ErrInvalidName: gettext("Invalid name"),
|
||||
ErrBusy: gettext("Busy, please try again later"),
|
||||
ErrWakeupInterval: gettext("The wakeup interval is %s, but must be 1h or less"),
|
||||
ErrAccountConnect: gettext("Your account could not be connected"),
|
||||
ErrTooManyRequests: gettext("Too many requests"),
|
||||
ErrInsufficientStorage: gettext("Insufficient storage"),
|
||||
ErrQuotaExceeded: gettext("Quota exceeded"),
|
||||
ErrRegistrationDisabled: gettext("Registration disabled"),
|
||||
ErrVerifiedEmailRequired: gettext("Verified email required"),
|
||||
|
||||
// Info and confirmation messages:
|
||||
MsgChangesSaved: gettext("Changes successfully saved"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue