diff --git a/pkg/server/api/complianceportal/v1/oauth_callback_handler.go b/pkg/server/api/complianceportal/v1/oauth_callback_handler.go index afb687a75..b14177279 100644 --- a/pkg/server/api/complianceportal/v1/oauth_callback_handler.go +++ b/pkg/server/api/complianceportal/v1/oauth_callback_handler.go @@ -178,12 +178,23 @@ func (h *OAuthCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) return } - h.sessionCookie.Set(w, session) + identity, err := h.iam.AccountService.GetIdentity(ctx, identityID) + if err != nil { + h.logger.ErrorCtx(ctx, "cannot load identity", log.Error(err)) + httpserver.RenderError(w, http.StatusInternalServerError, errInternal) + + return + } continueURL := state.ContinueURL if continueURL == "" { continueURL = "/" } + if identity.Locale != nil { + continueURL = rewriteContinueURLLocale(continueURL, *identity.Locale) + } + + h.sessionCookie.Set(w, session) h.safeRedirect.Redirect(w, r, continueURL, "/", http.StatusFound) } diff --git a/pkg/server/api/complianceportal/v1/seo.go b/pkg/server/api/complianceportal/v1/seo.go index 4e19b0a28..4b113cf6a 100644 --- a/pkg/server/api/complianceportal/v1/seo.go +++ b/pkg/server/api/complianceportal/v1/seo.go @@ -97,6 +97,36 @@ func isCompliancePortalLocale(value string) bool { return slices.Contains(iam.SupportedIdentityLocales, value) } +// rewriteContinueURLLocale swaps the leading locale segment of continueURL's +// path for locale (a supported short tag). Relative and absolute URLs are +// accepted; query and fragment are preserved. Unprefixed paths get the locale +// prepended. Returns continueURL unchanged when locale is unsupported or the +// URL cannot be parsed. +func rewriteContinueURLLocale(continueURL, locale string) string { + if !isCompliancePortalLocale(locale) { + return continueURL + } + + u, err := url.Parse(continueURL) + if err != nil { + return continueURL + } + + path := u.Path + if path == "" { + path = "/" + } + + _, rest := splitLocaleFromAppPath(path) + if rest == "/" || rest == "" { + u.Path = "/" + locale + } else { + u.Path = "/" + locale + rest + } + + return u.String() +} + func localizedPageURL(pageBaseURL, locale, rest string) string { base := strings.TrimRight(pageBaseURL, "/") segments := []string{locale} diff --git a/pkg/server/api/complianceportal/v1/seo_locale_test.go b/pkg/server/api/complianceportal/v1/seo_locale_test.go new file mode 100644 index 000000000..be4f74053 --- /dev/null +++ b/pkg/server/api/complianceportal/v1/seo_locale_test.go @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package complianceportal_v1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRewriteContinueURLLocale(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + continueURL string + locale string + want string + }{ + { + name: "rewrites relative path locale", + continueURL: "/es/documents", + locale: "fr", + want: "/fr/documents", + }, + { + name: "preserves query markers", + continueURL: "/es/documents?request-document-id=abc", + locale: "fr", + want: "/fr/documents?request-document-id=abc", + }, + { + name: "rewrites absolute url path", + continueURL: "https://acme.probopage.localhost/es/documents?subscribe=true", + locale: "fr", + want: "https://acme.probopage.localhost/fr/documents?subscribe=true", + }, + { + name: "prepends locale on unprefixed path", + continueURL: "/overview", + locale: "fr", + want: "/fr/overview", + }, + { + name: "rewrites locale-only path", + continueURL: "/es", + locale: "fr", + want: "/fr", + }, + { + name: "leaves url unchanged for unsupported locale", + continueURL: "/es/documents", + locale: "xx", + want: "/es/documents", + }, + { + name: "leaves url unchanged when already matching", + continueURL: "/fr/documents", + locale: "fr", + want: "/fr/documents", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := rewriteContinueURLLocale(tt.continueURL, tt.locale) + assert.Equal(t, tt.want, got) + }) + } +}