// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package text import ( "fmt" "strings" "github.com/ory/x/stringslice" "golang.org/x/text/cases" "golang.org/x/text/language" ) func NewValidationErrorGeneric(reason string) *Message { return &Message{ ID: ErrorValidationGeneric, Text: reason, Type: Error, Context: context(map[string]any{ "reason": reason, }), } } func NewValidationErrorRequired(missing string) *Message { return &Message{ ID: ErrorValidationRequired, Text: fmt.Sprintf("Property %s is missing.", missing), Type: Error, Context: context(map[string]any{ "property": missing, }), } } func NewErrorValidationMinLength(minLength, actualLength int) *Message { return &Message{ ID: ErrorValidationMinLength, Text: fmt.Sprintf("length must be >= %d, but got %d", minLength, actualLength), Type: Error, Context: context(map[string]any{ "min_length": minLength, "actual_length": actualLength, }), } } func NewErrorValidationMaxLength(maxLength, actualLength int) *Message { return &Message{ ID: ErrorValidationMaxLength, Text: fmt.Sprintf("length must be <= %d, but got %d", maxLength, actualLength), Type: Error, Context: context(map[string]any{ "max_length": maxLength, "actual_length": actualLength, }), } } func NewErrorValidationInvalidFormat(pattern string) *Message { return &Message{ ID: ErrorValidationInvalidFormat, Text: fmt.Sprintf("does not match pattern %q", pattern), Type: Error, Context: context(map[string]any{ "pattern": pattern, }), } } func NewErrorValidationMinimum(minimum, actual float64) *Message { return &Message{ ID: ErrorValidationMinimum, Text: fmt.Sprintf("must be >= %v but found %v", minimum, actual), Type: Error, Context: context(map[string]any{ "minimum": minimum, "actual": actual, }), } } func NewErrorValidationExclusiveMinimum(minimum, actual float64) *Message { return &Message{ ID: ErrorValidationExclusiveMinimum, Text: fmt.Sprintf("must be > %v but found %v", minimum, actual), Type: Error, Context: context(map[string]any{ "minimum": minimum, "actual": actual, }), } } func NewErrorValidationMaximum(maximum, actual float64) *Message { return &Message{ ID: ErrorValidationMaximum, Text: fmt.Sprintf("must be <= %v but found %v", maximum, actual), Type: Error, Context: context(map[string]any{ "maximum": maximum, "actual": actual, }), } } func NewErrorValidationExclusiveMaximum(maximum, actual float64) *Message { return &Message{ ID: ErrorValidationExclusiveMaximum, Text: fmt.Sprintf("must be < %v but found %v", maximum, actual), Type: Error, Context: context(map[string]any{ "maximum": maximum, "actual": actual, }), } } func NewErrorValidationMultipleOf(base, actual float64) *Message { return &Message{ ID: ErrorValidationMultipleOf, Text: fmt.Sprintf("%v not multipleOf %v", actual, base), Type: Error, Context: context(map[string]any{ "base": base, "actual": actual, }), } } func NewErrorValidationMaxItems(maxItems, actualItems int) *Message { return &Message{ ID: ErrorValidationMaxItems, Text: fmt.Sprintf("maximum %d items allowed, but found %d items", maxItems, actualItems), Type: Error, Context: context(map[string]any{ "max_items": maxItems, "actual_items": actualItems, }), } } func NewErrorValidationMinItems(minItems, actualItems int) *Message { return &Message{ ID: ErrorValidationMinItems, Text: fmt.Sprintf("minimum %d items allowed, but found %d items", minItems, actualItems), Type: Error, Context: context(map[string]any{ "min_items": minItems, "actual_items": actualItems, }), } } func NewErrorValidationUniqueItems(indexA, indexB int) *Message { return &Message{ ID: ErrorValidationUniqueItems, Text: fmt.Sprintf("items at index %d and %d are equal", indexA, indexB), Type: Error, Context: context(map[string]any{ "index_a": indexA, "index_b": indexB, }), } } func NewErrorValidationWrongType(allowedTypes []string, actualType string) *Message { return &Message{ ID: ErrorValidationWrongType, Text: fmt.Sprintf("expected %s, but got %s", strings.Join(allowedTypes, " or "), actualType), Type: Error, Context: context(map[string]any{ "allowed_types": allowedTypes, "actual_type": actualType, }), } } func NewErrorValidationConst(expected any) *Message { return &Message{ ID: ErrorValidationConst, Text: fmt.Sprintf("must be equal to constant %v", expected), Type: Error, Context: context(map[string]any{ "expected": expected, }), } } func NewErrorValidationConstGeneric() *Message { return &Message{ ID: ErrorValidationConstGeneric, Text: "const failed", Type: Error, } } func NewErrorValidationEmail(value string) *Message { return &Message{ ID: ErrorValidationEmail, Text: "Enter a valid email address", Type: Error, Context: context(map[string]any{ "value": value, }), } } func NewErrorValidationPhone(value string) *Message { return &Message{ ID: ErrorValidationPhone, Text: "Enter a valid phone number", Type: Error, Context: context(map[string]any{ "value": value, }), } } func NewErrorValidationPasswordPolicyViolationGeneric(reason string) *Message { return &Message{ ID: ErrorValidationPasswordPolicyViolationGeneric, Text: fmt.Sprintf("The password can not be used because %s.", reason), Type: Error, Context: context(map[string]any{ "reason": reason, }), } } func NewErrorValidationPasswordIdentifierTooSimilar() *Message { return &Message{ ID: ErrorValidationPasswordIdentifierTooSimilar, Text: "The password can not be used because it is too similar to the identifier.", Type: Error, } } func NewErrorValidationPasswordMinLength(minLength, actualLength int) *Message { return &Message{ ID: ErrorValidationPasswordMinLength, Text: fmt.Sprintf("The password must be at least %d characters long, but got %d.", minLength, actualLength), Type: Error, Context: context(map[string]any{ "min_length": minLength, "actual_length": actualLength, }), } } func NewErrorValidationPasswordMaxLength(maxLength, actualLength int) *Message { return &Message{ ID: ErrorValidationPasswordMaxLength, Text: fmt.Sprintf("The password must be at most %d characters long, but got %d.", maxLength, actualLength), Type: Error, Context: context(map[string]any{ "max_length": maxLength, "actual_length": actualLength, }), } } func NewErrorValidationPasswordNewSameAsOld() *Message { return &Message{ ID: ErrorValidationPasswordNewSameAsOld, Text: "The new password must be different from the old password.", Type: Error, } } func NewErrorValidationPasswordTooManyBreaches(breaches int64) *Message { return &Message{ ID: ErrorValidationPasswordTooManyBreaches, Text: "The password has been found in data breaches and must no longer be used.", Type: Error, Context: context(map[string]any{ "breaches": breaches, }), } } func NewErrorValidationInvalidCredentials() *Message { return &Message{ ID: ErrorValidationInvalidCredentials, Text: "The provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number.", Type: Error, } } func NewErrorValidationAccountNotFound() *Message { return &Message{ ID: ErrorValidationAccountNotFound, Text: "This account does not exist or has no login method configured.", Type: Error, } } func NewErrorValidationDuplicateCredentials() *Message { return &Message{ ID: ErrorValidationDuplicateCredentials, Text: "An account with the same identifier (email, phone, username, ...) exists already.", Type: Error, } } func NewErrorValidationDuplicateCredentialsWithHints(availableCredentialTypes []string, availableOIDCProviders []string, credentialIdentifierHint string) *Message { identifier := credentialIdentifierHint if identifier == "" { identifier = "an email, phone, or username" } oidcProviders := make([]string, 0, len(availableOIDCProviders)) for _, provider := range availableOIDCProviders { if strings.ContainsAny(provider, "{}") { oidcProviders = append(oidcProviders, provider) } else { oidcProviders = append(oidcProviders, cases.Title(language.English).String(provider)) } } reason := fmt.Sprintf("You tried signing in with %s which is already in use by another account.", identifier) if len(availableCredentialTypes) > 0 { humanReadable := make([]string, 0, len(availableCredentialTypes)) for _, cred := range availableCredentialTypes { switch cred { case "password": humanReadable = append(humanReadable, "your password") case "oidc", "saml": humanReadable = append(humanReadable, "social sign in") case "webauthn": humanReadable = append(humanReadable, "your passkey or a security key") case "passkey": humanReadable = append(humanReadable, "your passkey") } } if len(humanReadable) == 0 { // show at least some hint // also our example message generation tool runs into this case humanReadable = append(humanReadable, availableCredentialTypes...) } humanReadable = stringslice.Unique(humanReadable) // Final format: "You can sign in using foo, bar, or baz." if len(humanReadable) > 1 { humanReadable[len(humanReadable)-1] = "or " + humanReadable[len(humanReadable)-1] } if len(humanReadable) > 0 { reason += fmt.Sprintf(" You can sign in using %s.", strings.Join(humanReadable, ", ")) } } if len(oidcProviders) > 0 { reason += fmt.Sprintf(" You can sign in using one of the following social sign in providers: %s.", strings.Join(oidcProviders, ", ")) } return &Message{ ID: ErrorValidationDuplicateCredentialsWithHints, Text: reason, Type: Error, Context: context(map[string]any{ "available_credential_types": availableCredentialTypes, "available_oidc_providers": availableOIDCProviders, "credential_identifier_hint": credentialIdentifierHint, }), } } func NewErrorValidationDuplicateCredentialsOnOIDCLink() *Message { return &Message{ ID: ErrorValidationDuplicateCredentialsOnOIDCLink, Text: "An account with the same identifier (email, phone, username, ...) exists already. " + "Please sign in to your existing account to link your social profile.", Type: Error, } } func NewErrorValidationTOTPVerifierWrong() *Message { return &Message{ ID: ErrorValidationTOTPVerifierWrong, Text: "The provided authentication code is invalid, please try again.", Type: Error, } } func NewErrorValidationWebAuthnVerifierWrong() *Message { return &Message{ ID: ErrorValidationWebAuthnVerifierWrong, Text: "The provided web authn login is invalid, please try again.", Type: Error, } } func NewErrorValidationDeviceAuthnVerifierWrong() *Message { return &Message{ ID: ErrorValidationDeviceAuthnVerifierWrong, Text: "The provided DeviceAuthn signature is invalid.", Type: Error, } } func NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid() *Message { return &Message{ ID: ErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid, Text: "This DeviceAuthn key can no longer be used because relaxed attestation is expired or disabled. Please enroll your device again.", Type: Error, } } func NewErrorValidationDeviceAuthnKeyReenrollmentRequired() *Message { return &Message{ ID: ErrorValidationDeviceAuthnKeyReenrollmentRequired, Text: "This DeviceAuthn key was enrolled before user verification was introduced and can no longer be used. Please enroll your device again.", Type: Error, } } func NewErrorValidationLookupAlreadyUsed() *Message { return &Message{ ID: ErrorValidationLookupAlreadyUsed, Text: "This backup recovery code has already been used.", Type: Error, } } func NewErrorValidationLookupInvalid() *Message { return &Message{ ID: ErrorValidationLookupInvalid, Text: "The backup recovery code is not valid.", Type: Error, } } func NewErrorValidationIdentifierMissing() *Message { return &Message{ ID: ErrorValidationIdentifierMissing, Text: "Could not find any login identifiers. Did you forget to set them? This could also be caused by a server misconfiguration.", Type: Error, } } func NewErrorValidationAddressNotVerified() *Message { return &Message{ ID: ErrorValidationAddressNotVerified, Text: "Account not active yet. Did you forget to verify your email address?", Type: Error, } } func NewErrorValidationNoTOTPDevice() *Message { return &Message{ ID: ErrorValidationNoTOTPDevice, Text: "You have no TOTP device set up.", Type: Error, } } func NewErrorValidationNoLookup() *Message { return &Message{ ID: ErrorValidationNoLookup, Text: "You have no backup recovery codes set up.", Type: Error, } } func NewErrorValidationNoWebAuthnDevice() *Message { return &Message{ ID: ErrorValidationNoWebAuthnDevice, Text: "You have no WebAuthn device set up.", Type: Error, } } func NewErrorValidationSuchNoWebAuthnUser() *Message { return &Message{ ID: ErrorValidationSuchNoWebAuthnUser, Text: "This account does not exist or has no security key set up.", Type: Error, } } func NewErrorValidationNoDeviceAuthnDevice() *Message { return &Message{ ID: ErrorValidationNoDeviceAuthnDevice, Text: "You have no DeviceAuthn device set up.", Type: Error, } } func NewErrorValidationNoCodeUser() *Message { return &Message{ ID: ErrorValidationNoCodeUser, Text: "This account does not exist or has not setup sign in with code.", Type: Error, } } func NewErrorValidationTraitsMismatch() *Message { return &Message{ ID: ErrorValidationTraitsMismatch, Text: "The provided traits do not match the traits previously associated with this flow.", Type: Error, } } func NewErrorCaptchaFailed() *Message { return &Message{ ID: ErrorValidationCaptchaError, Text: "Captcha verification failed, please try again.", Type: Error, } }