402 lines
12 KiB
Go
402 lines
12 KiB
Go
/*
|
|
Copyright CAcert Inc.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/yuin/goldmark"
|
|
|
|
"code.cacert.org/cacert/oidc-idp/translations"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
type MessageCatalog struct {
|
|
messages map[string]*i18n.Message
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
|
|
for key, value := range messages {
|
|
m.messages[key] = value
|
|
}
|
|
}
|
|
|
|
func (s *I18NService) LookupMessage(
|
|
id string,
|
|
templateData map[string]interface{},
|
|
localizer *i18n.Localizer,
|
|
) string {
|
|
if message, ok := s.catalog.messages[id]; ok {
|
|
translation, err := localizer.Localize(&i18n.LocalizeConfig{
|
|
DefaultMessage: message,
|
|
TemplateData: templateData,
|
|
})
|
|
if err != nil {
|
|
return s.catalog.handleLocalizeError(id, translation, err)
|
|
}
|
|
|
|
return translation
|
|
}
|
|
|
|
s.logger.Warn("no translation found for id", "id", id)
|
|
|
|
return id
|
|
}
|
|
|
|
func (s *I18NService) LookupHTTPErrorMessage(httpStatusCode int, localizer *i18n.Localizer) string {
|
|
id := fmt.Sprintf("http%d", httpStatusCode)
|
|
translation := s.LookupMessage(id, nil, localizer)
|
|
|
|
if translation != id {
|
|
return translation
|
|
}
|
|
|
|
return http.StatusText(httpStatusCode)
|
|
}
|
|
|
|
func (s *I18NService) LookupMarkdownMessage(
|
|
id string,
|
|
templateData map[string]interface{},
|
|
localizer *i18n.Localizer,
|
|
) string {
|
|
if message, ok := s.catalog.messages[id]; ok {
|
|
translation, err := localizer.Localize(&i18n.LocalizeConfig{
|
|
DefaultMessage: message,
|
|
TemplateData: templateData,
|
|
})
|
|
if err != nil {
|
|
s.logger.Warn("message localization failed", "error", err, "message", message)
|
|
}
|
|
|
|
if translation == "" {
|
|
return id
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
err = goldmark.Convert([]byte(translation), buf)
|
|
if err != nil {
|
|
return s.catalog.handleLocalizeError(id, translation, fmt.Errorf("markdown conversion error: %w", err))
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
s.logger.Warn("no translation found for id", "id", id)
|
|
|
|
return id
|
|
}
|
|
|
|
func (s *I18NService) LookupMessagePlural(
|
|
id string,
|
|
templateData map[string]interface{},
|
|
localizer *i18n.Localizer,
|
|
count int,
|
|
) string {
|
|
if message, ok := s.catalog.messages[id]; ok {
|
|
translation, err := localizer.Localize(&i18n.LocalizeConfig{
|
|
DefaultMessage: message,
|
|
TemplateData: templateData,
|
|
PluralCount: count,
|
|
})
|
|
if err != nil {
|
|
return s.catalog.handleLocalizeError(id, translation, err)
|
|
}
|
|
|
|
return translation
|
|
}
|
|
|
|
s.logger.Warn("no translation found for id", "id", id)
|
|
|
|
return id
|
|
}
|
|
|
|
func (m *MessageCatalog) handleLocalizeError(id string, translation string, err error) string {
|
|
var messageNotFound *i18n.MessageNotFoundErr
|
|
|
|
if errors.As(err, &messageNotFound) {
|
|
m.logger.Warn("message not found", "error", err, "message", id)
|
|
|
|
if translation != "" {
|
|
return translation
|
|
}
|
|
} else {
|
|
m.logger.Error("translation error", "error", err, "message", id)
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
type I18NService struct {
|
|
logger *slog.Logger
|
|
bundle *i18n.Bundle
|
|
catalog *MessageCatalog
|
|
}
|
|
|
|
func (s *I18NService) AddMessages() error {
|
|
messages := make(map[string]*i18n.Message)
|
|
messages["http404"] = &i18n.Message{
|
|
ID: "http404",
|
|
Description: "HTTP error 404 not found",
|
|
Other: "Not found",
|
|
}
|
|
messages["unknown"] = &i18n.Message{
|
|
ID: "ErrorUnknown",
|
|
Other: "Unknown error",
|
|
}
|
|
messages["AuthServerErrorTitle"] = &i18n.Message{
|
|
ID: "AuthServerErrorTitle",
|
|
Other: "Authorization server returned an error",
|
|
}
|
|
messages["AuthServerErrorExplanation"] = &i18n.Message{
|
|
ID: "AuthServerErrorExplanation",
|
|
Other: "A request that your browser sent to the authorization server caused an error." +
|
|
" The authorization server returned details about the error that are printed below.",
|
|
}
|
|
messages["ButtonTitleCancel"] = &i18n.Message{
|
|
ID: "ButtonTitleCancel",
|
|
Description: "Title for a button to cancel an action",
|
|
Other: "Cancel",
|
|
}
|
|
messages["ButtonTitleConsent"] = &i18n.Message{
|
|
ID: "ButtonTitleConsent",
|
|
Description: "Title for a button to give consent",
|
|
Other: "Consent",
|
|
}
|
|
messages["ButtonTitleDeny"] = &i18n.Message{
|
|
ID: "ButtonTitleDeny",
|
|
Description: "Title for a button to deny consent",
|
|
Other: "Deny",
|
|
}
|
|
messages["ButtonTitleRevoke"] = &i18n.Message{
|
|
ID: "ButtonTitleRevoke",
|
|
Description: "Title for a button to revoke consent",
|
|
Other: "Revoke",
|
|
}
|
|
messages["ButtonTitleConfirmRevoke"] = &i18n.Message{
|
|
ID: "ButtonTitleConfirmRevoke",
|
|
Description: "Title for a button to confirm consent revocation",
|
|
Other: "Yes, Revoke!",
|
|
}
|
|
messages["ColumnNameApplication"] = &i18n.Message{
|
|
ID: "ColumnNameApplication",
|
|
Description: "Title for a table column showing application names",
|
|
Other: "Application",
|
|
}
|
|
messages["ColumnNameActions"] = &i18n.Message{
|
|
ID: "ColumnNameActions",
|
|
Description: "Title for a table column showing available actions",
|
|
Other: "Actions",
|
|
}
|
|
messages["ColumnNameExpires"] = &i18n.Message{
|
|
ID: "ColumnNameExpires",
|
|
Description: "Title for a table column showing the expiry date for a consent",
|
|
Other: "Expires",
|
|
}
|
|
messages["ColumnNameGranted"] = &i18n.Message{
|
|
ID: "ColumnNameGranted",
|
|
Description: "Title for a table column showing the time when consent has been granted",
|
|
Other: "Granted at",
|
|
}
|
|
messages["ColumnNameSubject"] = &i18n.Message{
|
|
ID: "ColumnNameSubject",
|
|
Description: "Title for a table column showing the subject of a consent",
|
|
Other: "Subject",
|
|
}
|
|
messages["ConfirmRevokeTitle"] = &i18n.Message{
|
|
ID: "ConfirmRevokeTitle",
|
|
Other: "Revoke consent",
|
|
}
|
|
messages["ConfirmRevokeExplanation"] = &i18n.Message{
|
|
ID: "ConfirmRevokeExplanation",
|
|
Other: "Do you want to revoke your consent to allow **{{ .Application }}**" +
|
|
" access to identity data for **{{ .Subject }}**?",
|
|
}
|
|
messages["TitleRequestConsent"] = &i18n.Message{
|
|
ID: "TitleRequestConsent",
|
|
Other: "Application requests your consent",
|
|
}
|
|
messages["LabelSubmit"] = &i18n.Message{
|
|
ID: "LabelSubmit",
|
|
Other: "Submit",
|
|
}
|
|
messages["LabelConsent"] = &i18n.Message{
|
|
ID: "LabelConsent",
|
|
Other: "I hereby agree that the application may get the requested permissions.",
|
|
}
|
|
messages["LabelNever"] = &i18n.Message{
|
|
ID: "LabelNever",
|
|
Other: "Never",
|
|
}
|
|
messages["LabelUnknown"] = &i18n.Message{
|
|
ID: "LabelUnknown",
|
|
Other: "Unknown",
|
|
}
|
|
messages["IntroConsentRequested"] = &i18n.Message{
|
|
ID: "IntroConsentRequested",
|
|
Other: "The **{{ .client }}** application requested your consent for the following set of permissions:",
|
|
}
|
|
messages["IntroConsentMoreInformation"] = &i18n.Message{
|
|
ID: "IntroConsentMoreInformation",
|
|
Other: "You can find more information about **{{ .client }}** at [its description page]({{ .clientLink }}).",
|
|
}
|
|
messages["ClaimsInformation"] = &i18n.Message{
|
|
ID: "ClaimsInformation",
|
|
Other: "In addition the application wants access to the following information:",
|
|
}
|
|
messages["WrongOrLockedUserOrInvalidPassword"] = &i18n.Message{
|
|
ID: "WrongOrLockedUserOrInvalidPassword",
|
|
Other: "You entered an invalid username or password or your account has been locked.",
|
|
}
|
|
messages["CertLoginIntroText"] = &i18n.Message{
|
|
ID: "CertLoginIntroText",
|
|
Other: "The application **{{ .ClientName }}** requests a login.",
|
|
}
|
|
messages["EmailChoiceText"] = &i18n.Message{
|
|
ID: "EmailChoiceText",
|
|
One: "You have presented a valid client certificate for the following email address:",
|
|
Other: "You have presented a valid client certificate for multiple email addresses. " +
|
|
"Please choose which one you want to present to the application:",
|
|
}
|
|
messages["IndexTitle"] = &i18n.Message{
|
|
ID: "IndexTitle",
|
|
Other: "Welcome to your identity provider",
|
|
}
|
|
messages["IndexWelcomeMessage"] = &i18n.Message{
|
|
ID: "IndexWelcomeMessage",
|
|
Other: "Go to [manage consent]({{ .ManageConsentHRef }}) to show or revoke consent" +
|
|
" you have given to client applications.",
|
|
}
|
|
messages["LoginTitle"] = &i18n.Message{
|
|
ID: "LoginTitle",
|
|
Other: "Authenticate with a client certificate",
|
|
}
|
|
messages["CertLoginRequestText"] = &i18n.Message{
|
|
ID: "CertLoginRequestText",
|
|
Other: "Do you want to use the chosen identity from the certificate for authentication?",
|
|
}
|
|
messages["LabelAcceptCertLogin"] = &i18n.Message{
|
|
ID: "LabelAcceptCertLogin",
|
|
Description: "Label for a button to accept certificate login",
|
|
Other: "Yes, please use this identity",
|
|
}
|
|
messages["LabelRejectCertLogin"] = &i18n.Message{
|
|
ID: "LabelRejectCertLogin",
|
|
Description: "Label for a button to reject certificate login",
|
|
Other: "No, please send me back",
|
|
}
|
|
messages["LoginDeniedByUser"] = &i18n.Message{
|
|
ID: "LoginDeniedByUser",
|
|
Other: "Login has been denied by the user.",
|
|
}
|
|
messages["LogoutSuccessfulTitle"] = &i18n.Message{
|
|
ID: "LogoutSuccessfulTitle",
|
|
Other: "Logout successful",
|
|
}
|
|
messages["LogoutSuccessfulText"] = &i18n.Message{
|
|
ID: "LogoutSuccessfulText",
|
|
Other: "You have been logged out successfully.",
|
|
}
|
|
messages["ManageConsentTitle"] = &i18n.Message{
|
|
ID: "ManageConsentTitle",
|
|
Other: "Manage consent",
|
|
}
|
|
messages["ManageConsentDescription"] = &i18n.Message{
|
|
ID: "ManageConsentDescription",
|
|
Other: "This page allows you to see consent that you have given to client applications in the past.",
|
|
}
|
|
messages["HintChooseAnIdentityForAuthentication"] = &i18n.Message{
|
|
ID: "HintChooseAnIdentityForAuthentication",
|
|
Other: "Choose an identity for authentication.",
|
|
}
|
|
messages["NoConsentGiven"] = &i18n.Message{
|
|
ID: "NoConsentGiven",
|
|
Other: "You have not given consent to use your data to any application yet.",
|
|
}
|
|
messages["NoEmailAddressSelected"] = &i18n.Message{
|
|
ID: "NoEmailAddressSelected",
|
|
Other: "You did not select an email address. Please select an email address to continue.",
|
|
}
|
|
messages["NoEmailsInClientCertificateTitle"] = &i18n.Message{
|
|
ID: "NoEmailsInClientCertificateTitle",
|
|
Other: "No email addresses in client certificate",
|
|
}
|
|
messages["NoEmailsInClientCertificateExplanation"] = &i18n.Message{
|
|
ID: "NoEmailsInClientCertificateExplanation",
|
|
Other: `The presented client certificate does not contain any email address value.
|
|
An email address is required to authenticate yourself.`,
|
|
}
|
|
messages["NoChallengeInRequestTitle"] = &i18n.Message{
|
|
ID: "NoChallengeInRequestTitle",
|
|
Other: "No challenge parameter in your authentication request",
|
|
}
|
|
messages["NoChallengeInRequestExplanation"] = &i18n.Message{
|
|
ID: "NoChallengeInRequestExplanation",
|
|
Other: "Your authentication request did not contain the necessary `login_challenge` parameter." +
|
|
" You can find more information about this parameter in" +
|
|
" [the ORY Hydra documentation](https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow).",
|
|
}
|
|
|
|
s.catalog.AddMessages(messages)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *I18NService) Localizer(languages string) *i18n.Localizer {
|
|
return i18n.NewLocalizer(s.bundle, languages)
|
|
}
|
|
|
|
func InitI18n(logger *slog.Logger, languages []string) *I18NService {
|
|
bundle := i18n.NewBundle(language.English)
|
|
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
|
|
|
for _, lang := range languages {
|
|
bundleName := fmt.Sprintf("active.%s.toml", lang)
|
|
|
|
bundleBytes, err := translations.Bundles.ReadFile(bundleName)
|
|
if err != nil {
|
|
logger.Warn("message bundle not found", "bundle", bundleName)
|
|
|
|
continue
|
|
}
|
|
|
|
bundle.MustParseMessageFileBytes(bundleBytes, bundleName)
|
|
}
|
|
|
|
catalog := initMessageCatalog(logger)
|
|
|
|
return &I18NService{logger: logger, bundle: bundle, catalog: catalog}
|
|
}
|
|
|
|
func initMessageCatalog(logger *slog.Logger) *MessageCatalog {
|
|
messages := make(map[string]*i18n.Message)
|
|
messages["ErrorTitle"] = &i18n.Message{
|
|
ID: "ErrorTitle",
|
|
Other: "An error has occurred",
|
|
}
|
|
|
|
return &MessageCatalog{messages: messages, logger: logger}
|
|
}
|