Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vale/styles/config/vocabularies/vocab/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@ ACLs
[Rr]etryable
[Uu]psert
[Ii]dempotency
[Ss]andboxed
[Ss]andboxed
DCR
BCP
4 changes: 2 additions & 2 deletions backend/cmd/server/servicemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func registerServices(mux *http.ServeMux) jwt.JWTServiceInterface {

// TODO: Remove entityService dependency after finalizing declarative resource loading pattern
applicationService, applicationExporter, err := application.Initialize(
mux, mcpServer, entityProvider, entityService, inboundClientService, ouService)
mux, mcpServer, entityProvider, entityService, inboundClientService, ouService, i18nService)
if err != nil {
logger.Fatal("Failed to initialize ApplicationService", log.Error(err))
}
Expand Down Expand Up @@ -326,7 +326,7 @@ func registerServices(mux *http.ServeMux) jwt.JWTServiceInterface {
// Initialize OAuth services.
err = oauth.Initialize(mux, applicationService, inboundClientService, authnProvider, jwtService, jweService,
flowExecService, observabilitySvc, pkiService, ouService, attributeCacheService, authZService, entityProvider,
resourceService)
resourceService, i18nService)
if err != nil {
logger.Fatal("Failed to initialize OAuth services", log.Error(err))
}
Expand Down
4 changes: 3 additions & 1 deletion backend/internal/application/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
oupkg "github.com/asgardeo/thunder/internal/ou"
serverconst "github.com/asgardeo/thunder/internal/system/constants"
declarativeresource "github.com/asgardeo/thunder/internal/system/declarative_resource"
i18nmgt "github.com/asgardeo/thunder/internal/system/i18n/mgt"
"github.com/asgardeo/thunder/internal/system/middleware"
)

Expand All @@ -42,9 +43,10 @@ func Initialize(
entityService entity.EntityServiceInterface,
inboundClient inboundclient.InboundClientServiceInterface,
ouService oupkg.OrganizationUnitServiceInterface,
i18nService i18nmgt.I18nServiceInterface,
) (ApplicationServiceInterface, declarativeresource.ResourceExporter, error) {
appService := newApplicationService(
inboundClient, entityProvider, ouService,
inboundClient, entityProvider, ouService, i18nService,
)

if err := entityService.LoadIndexedAttributes(getAppIndexedAttributes()); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/application/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func (suite *InitTestSuite) TestInitialize_WithDeclarativeResourcesDisabled() {
mockEntityService,
inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T()),
nil, // ouService - not needed for this test
nil, // i18nService - not needed for this test
)

// Assert
Expand Down Expand Up @@ -199,6 +200,7 @@ func (suite *InitTestSuite) TestInitialize_WithMCPServer() {
mockEntityService,
inboundclientmock.NewInboundClientServiceInterfaceMock(suite.T()),
nil, // ouService - not needed for this test
nil, // i18nService - not needed for this test
)

// Assert
Expand Down Expand Up @@ -594,6 +596,7 @@ func TestInitialize_Standalone(t *testing.T) {
mockEntityService,
inboundclientmock.NewInboundClientServiceInterfaceMock(t),
nil, // ouService - not needed for this test
nil, // i18nService - not needed for this test
)

// Assert
Expand Down Expand Up @@ -643,6 +646,7 @@ func TestInitialize_WithDeclarativeResources_Standalone(t *testing.T) {
mockEntityService,
mockInboundClient,
nil, // ouService - not needed for this test
nil, // i18nService - not needed for this test
)

// Assert
Expand Down
70 changes: 68 additions & 2 deletions backend/internal/application/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
oupkg "github.com/asgardeo/thunder/internal/ou"
"github.com/asgardeo/thunder/internal/system/error/serviceerror"
"github.com/asgardeo/thunder/internal/system/i18n/core"
i18nmgt "github.com/asgardeo/thunder/internal/system/i18n/mgt"
"github.com/asgardeo/thunder/internal/system/log"
sysutils "github.com/asgardeo/thunder/internal/system/utils"
)
Expand All @@ -62,19 +63,22 @@ type applicationService struct {
inboundClientService inboundclient.InboundClientServiceInterface
entityProvider entityprovider.EntityProviderInterface
ouService oupkg.OrganizationUnitServiceInterface
i18nService i18nmgt.I18nServiceInterface
}

// newApplicationService creates a new instance of ApplicationService.
func newApplicationService(
inboundClient inboundclient.InboundClientServiceInterface,
entityProvider entityprovider.EntityProviderInterface,
ouService oupkg.OrganizationUnitServiceInterface,
i18nService i18nmgt.I18nServiceInterface,
) ApplicationServiceInterface {
return &applicationService{
logger: log.GetLogger().With(log.String(log.LoggerKeyComponentName, "ApplicationService")),
inboundClientService: inboundClient,
entityProvider: entityProvider,
ouService: ouService,
i18nService: i18nService,
}
}

Expand Down Expand Up @@ -325,7 +329,7 @@ func (as *applicationService) UpdateApplication(ctx context.Context, appID strin
if as.inboundClientService.IsDeclarative(ctx, appID) {
return nil, &ErrorCannotModifyDeclarativeResource
}
_, inboundAuthConfig, svcErr := as.validateApplicationForUpdate(ctx, appID, app)
existingApp, inboundAuthConfig, svcErr := as.validateApplicationForUpdate(ctx, appID, app)

if svcErr != nil {
return nil, svcErr
Expand Down Expand Up @@ -362,6 +366,10 @@ func (as *applicationService) UpdateApplication(ctx context.Context, appID strin
return nil, svcErr
}

if svcErr := as.cleanupStaleI18nKeys(ctx, appID, existingApp, app); svcErr != nil {
return nil, svcErr
}

appForReturn := *app
appForReturn.AuthFlowID = configDAO.AuthFlowID
appForReturn.RegistrationFlowID = configDAO.RegistrationFlowID
Expand Down Expand Up @@ -463,7 +471,7 @@ func (as *applicationService) DeleteApplication(ctx context.Context, appID strin
return &serviceerror.InternalServerError
}

return nil
return as.deleteLocalizedVariants(ctx, appID)
}

// isIdentifierTaken checks if an entity with the given identifier already exists.
Expand Down Expand Up @@ -1519,3 +1527,61 @@ func (as *applicationService) mapStoreError(err error) *serviceerror.ServiceErro
as.logger.Error("Failed to retrieve application", log.Error(err))
return &serviceerror.InternalServerError
}

// deleteLocalizedVariants removes all i18n translations for an application's fields.
// All fields are attempted; returns an internal server error if any deletion fails.
func (as *applicationService) deleteLocalizedVariants(ctx context.Context, appID string) *serviceerror.ServiceError {
if as.i18nService == nil {
return nil
}
var hasErr bool
for _, field := range []string{"name", "logo_uri", "tos_uri", "policy_uri"} {
if svcErr := as.i18nService.DeleteTranslationsByKey(
ctx, AppI18nNamespace(), AppI18nKey(appID, field)); svcErr != nil {
as.logger.Error("Failed to delete localized variant on app deletion",
log.String("appID", appID),
log.String("field", field),
log.String("namespace", AppI18nNamespace()))
hasErr = true
}
}
if hasErr {
return &serviceerror.InternalServerError
}
return nil
}

// cleanupStaleI18nKeys removes i18n keys for fields that changed from an i18n ref back to plain text.
// Returns an internal server error if any deletion fails.
func (as *applicationService) cleanupStaleI18nKeys(
ctx context.Context, appID string,
existing *model.ApplicationProcessedDTO, updated *model.ApplicationDTO,
) *serviceerror.ServiceError {
if as.i18nService == nil {
return nil
}
type pair struct{ old, updated, field string }
fields := []pair{
{existing.Name, updated.Name, "name"},
{existing.LogoURL, updated.LogoURL, "logo_uri"},
{existing.TosURI, updated.TosURI, "tos_uri"},
{existing.PolicyURI, updated.PolicyURI, "policy_uri"},
}
var hasErr bool
for _, f := range fields {
if isI18nRef(f.old) && !isI18nRef(f.updated) {
if svcErr := as.i18nService.DeleteTranslationsByKey(
ctx, AppI18nNamespace(), AppI18nKey(appID, f.field)); svcErr != nil {
as.logger.Error("Failed to delete stale i18n key",
log.String("appID", appID),
log.String("field", f.field),
log.String("namespace", AppI18nNamespace()))
hasErr = true
}
}
}
if hasErr {
return &serviceerror.InternalServerError
}
return nil
}
43 changes: 43 additions & 0 deletions backend/internal/application/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you 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
*
* http://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 application

import "strings"

// AppI18nNamespace returns the i18n namespace for application localized metadata.
func AppI18nNamespace() string {
return "custom"
}

// AppI18nKey returns the i18n key for an application field.
func AppI18nKey(appID, field string) string {
return "app." + appID + "." + field
}

// AppI18nRef returns the i18n template reference string for an application field.
// The returned value is stored as the application's display field so the UI can
// resolve it to the correct locale at render time.
func AppI18nRef(appID, field string) string {
return "{{t(" + AppI18nNamespace() + ":" + AppI18nKey(appID, field) + ")}}"
}

// isI18nRef reports whether s is an i18n template reference (e.g. "{{t(custom:app.123.name)}}").
func isI18nRef(s string) bool {
return strings.HasPrefix(s, "{{t(") && strings.HasSuffix(s, ")}}")
}
4 changes: 3 additions & 1 deletion backend/internal/oauth/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/asgardeo/thunder/internal/system/crypto/pki"
"github.com/asgardeo/thunder/internal/system/database/provider"
syshttp "github.com/asgardeo/thunder/internal/system/http"
i18nmgt "github.com/asgardeo/thunder/internal/system/i18n/mgt"
"github.com/asgardeo/thunder/internal/system/jose/jwe"
"github.com/asgardeo/thunder/internal/system/jose/jwt"
"github.com/asgardeo/thunder/internal/system/observability"
Expand All @@ -65,6 +66,7 @@ func Initialize(
authzService authz.AuthorizationServiceInterface,
entityProvider entityprovider.EntityProviderInterface,
resourceService resource.ResourceServiceInterface,
i18nService i18nmgt.I18nServiceInterface,
) error {
// Fetch runtime transactioner for OAuth services.
transactioner, err := provider.GetDBProvider().GetRuntimeDBTransactioner()
Expand Down Expand Up @@ -92,6 +94,6 @@ func Initialize(
return syshttp.IsSSRFSafeURL(req.URL.String())
}),
tokenValidator, inboundClient, ouService, attributeCacheSvc, transactioner)
dcr.Initialize(mux, applicationService, ouService, transactioner)
dcr.Initialize(mux, applicationService, ouService, i18nService, transactioner)
Comment thread
nandhu-kumar marked this conversation as resolved.
return nil
}
19 changes: 19 additions & 0 deletions backend/internal/oauth/oauth2/dcr/error_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,29 @@
package dcr

import (
"strconv"

"github.com/asgardeo/thunder/internal/system/error/serviceerror"
"github.com/asgardeo/thunder/internal/system/i18n/core"
)

// errInvalidBCP47Tag is returned when a language tag in a DCR request field is not valid BCP 47.
type errInvalidBCP47Tag struct{ key string }

// Error implements the error interface.
func (e *errInvalidBCP47Tag) Error() string {
Comment thread
nandhu-kumar marked this conversation as resolved.
return "invalid BCP 47 language tag in field \"" + e.key + "\""
}

// errTooManyLocalizedVariants is returned when a localizable field exceeds maxLocalizedVariantsPerField.
type errTooManyLocalizedVariants struct{ field string }

// Error implements the error interface.
func (e *errTooManyLocalizedVariants) Error() string {
Comment thread
nandhu-kumar marked this conversation as resolved.
return "field \"" + e.field + "\" exceeds the maximum of " +
strconv.Itoa(maxLocalizedVariantsPerField) + " localized variants"
}

// DCR standard service error constants
var (
// ErrorInvalidRequestFormat is used for nil request validation
Expand Down
5 changes: 3 additions & 2 deletions backend/internal/oauth/oauth2/dcr/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/asgardeo/thunder/internal/application"
"github.com/asgardeo/thunder/internal/ou"
i18nmgt "github.com/asgardeo/thunder/internal/system/i18n/mgt"
"github.com/asgardeo/thunder/internal/system/middleware"
"github.com/asgardeo/thunder/internal/system/transaction"
)
Expand All @@ -33,9 +34,10 @@ func Initialize(
mux *http.ServeMux,
appService application.ApplicationServiceInterface,
ouService ou.OrganizationUnitServiceInterface,
i18nService i18nmgt.I18nServiceInterface,
transactioner transaction.Transactioner,
) DCRServiceInterface {
dcrService := newDCRService(appService, ouService, transactioner)
dcrService := newDCRService(appService, ouService, i18nService, transactioner)
dcrHandler := newDCRHandler(dcrService)
registerRoutes(mux, dcrHandler)
return dcrService
Expand All @@ -49,7 +51,6 @@ func registerRoutes(mux *http.ServeMux, dcrHandler *dcrHandler) {
AllowCredentials: true,
MaxAge: 600,
}

mux.HandleFunc(middleware.WithCORS("POST /oauth2/dcr/register",
dcrHandler.HandleDCRRegistration, opts))
mux.HandleFunc(middleware.WithCORS("OPTIONS /oauth2/dcr/register",
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/oauth/oauth2/dcr/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (suite *InitTestSuite) TearDownTest() {
func (suite *InitTestSuite) TestInitialize() {
mux := http.NewServeMux()

service := Initialize(mux, suite.mockAppService, suite.mockOUService, &MockTransactioner{})
service := Initialize(mux, suite.mockAppService, suite.mockOUService, nil, &MockTransactioner{})

assert.NotNil(suite.T(), service)
assert.Implements(suite.T(), (*DCRServiceInterface)(nil), service)
Expand All @@ -71,7 +71,7 @@ func (suite *InitTestSuite) TestInitialize() {
func (suite *InitTestSuite) TestInitialize_RegistersRoutes() {
mux := http.NewServeMux()

Initialize(mux, suite.mockAppService, suite.mockOUService, &MockTransactioner{})
Initialize(mux, suite.mockAppService, suite.mockOUService, nil, &MockTransactioner{})

// Verify that the routes are registered by attempting to get a handler for them.
// The pattern includes the method because of CORS middleware wrapping.
Expand Down
Loading
Loading