mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +00:00
feat(idp): provide option to auto link user (#7734)
* init auto linking * prompt handling * working * translations * console * fixes * unify * custom texts * fix tests * linting * fix check of existing user * fix bg translation * set unspecified as default in the form
This commit is contained in:
@@ -172,6 +172,7 @@ func SetLoginTextToDomain(req *admin_pb.SetCustomLoginTextsRequest) *domain.Cust
|
||||
result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText)
|
||||
result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText)
|
||||
result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText)
|
||||
result.LinkingUserPrompt = text.LinkingUserPromptScreenTextPbToDomain(req.LinkingUserPromptText)
|
||||
result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText)
|
||||
result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText)
|
||||
result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText)
|
||||
|
@@ -1060,6 +1060,7 @@ func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages
|
||||
RegistrationUserText: text_grpc.RegistrationUserScreenTextToPb(text.RegistrationUser),
|
||||
ExternalRegistrationUserOverviewText: text_grpc.ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview),
|
||||
RegistrationOrgText: text_grpc.RegistrationOrgScreenTextToPb(text.RegistrationOrg),
|
||||
LinkingUserPromptText: text_grpc.LinkingUserPromptScreenTextToPb(text.LinkingUserPrompt),
|
||||
LinkingUserDoneText: text_grpc.LinkingUserDoneScreenTextToPb(text.LinkingUsersDone),
|
||||
ExternalUserNotFoundText: text_grpc.ExternalUserNotFoundScreenTextToPb(text.ExternalNotFound),
|
||||
SuccessLoginText: text_grpc.SuccessLoginScreenTextToPb(text.LoginSuccess),
|
||||
|
@@ -274,6 +274,20 @@ func OptionsToCommand(options *idp_pb.Options) idp.Options {
|
||||
IsLinkingAllowed: options.IsLinkingAllowed,
|
||||
IsAutoCreation: options.IsAutoCreation,
|
||||
IsAutoUpdate: options.IsAutoUpdate,
|
||||
AutoLinkingOption: autoLinkingOptionToCommand(options.AutoLinking),
|
||||
}
|
||||
}
|
||||
|
||||
func autoLinkingOptionToCommand(linking idp_pb.AutoLinkingOption) domain.AutoLinkingOption {
|
||||
switch linking {
|
||||
case idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME:
|
||||
return domain.AutoLinkingOptionUsername
|
||||
case idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL:
|
||||
return domain.AutoLinkingOptionEmail
|
||||
case idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED:
|
||||
return domain.AutoLinkingOptionUnspecified
|
||||
default:
|
||||
return domain.AutoLinkingOptionUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +412,7 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
|
||||
IsCreationAllowed: config.IsCreationAllowed,
|
||||
IsAutoCreation: config.IsAutoCreation,
|
||||
IsAutoUpdate: config.IsAutoUpdate,
|
||||
AutoLinking: autoLinkingOptionToPb(config.AutoLinking),
|
||||
},
|
||||
}
|
||||
if config.OAuthIDPTemplate != nil {
|
||||
@@ -451,6 +466,19 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
|
||||
return providerConfig
|
||||
}
|
||||
|
||||
func autoLinkingOptionToPb(linking domain.AutoLinkingOption) idp_pb.AutoLinkingOption {
|
||||
switch linking {
|
||||
case domain.AutoLinkingOptionUnspecified:
|
||||
return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED
|
||||
case domain.AutoLinkingOptionUsername:
|
||||
return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME
|
||||
case domain.AutoLinkingOptionEmail:
|
||||
return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL
|
||||
default:
|
||||
return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func oauthConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.OAuthIDPTemplate) {
|
||||
providerConfig.Config = &idp_pb.ProviderConfig_Oauth{
|
||||
Oauth: &idp_pb.OAuthConfig{
|
||||
|
@@ -171,6 +171,7 @@ func SetLoginCustomTextToDomain(req *mgmt_pb.SetCustomLoginTextsRequest) *domain
|
||||
result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText)
|
||||
result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText)
|
||||
result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText)
|
||||
result.LinkingUserPrompt = text.LinkingUserPromptScreenTextPbToDomain(req.LinkingUserPromptText)
|
||||
result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText)
|
||||
result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText)
|
||||
result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText)
|
||||
|
@@ -64,6 +64,7 @@ func CustomLoginTextToPb(text *domain.CustomLoginText) *text_pb.LoginCustomText
|
||||
RegistrationUserText: RegistrationUserScreenTextToPb(text.RegistrationUser),
|
||||
ExternalRegistrationUserOverviewText: ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview),
|
||||
RegistrationOrgText: RegistrationOrgScreenTextToPb(text.RegistrationOrg),
|
||||
LinkingUserPromptText: LinkingUserPromptScreenTextToPb(text.LinkingUserPrompt),
|
||||
LinkingUserDoneText: LinkingUserDoneScreenTextToPb(text.LinkingUsersDone),
|
||||
ExternalUserNotFoundText: ExternalUserNotFoundScreenTextToPb(text.ExternalNotFound),
|
||||
SuccessLoginText: SuccessLoginScreenTextToPb(text.LoginSuccess),
|
||||
@@ -422,6 +423,15 @@ func LinkingUserDoneScreenTextToPb(text domain.LinkingUserDoneScreenText) *text_
|
||||
}
|
||||
}
|
||||
|
||||
func LinkingUserPromptScreenTextToPb(text domain.LinkingUserPromptScreenText) *text_pb.LinkingUserPromptScreenText {
|
||||
return &text_pb.LinkingUserPromptScreenText{
|
||||
Title: text.Title,
|
||||
Description: text.Description,
|
||||
LinkButtonText: text.LinkButtonText,
|
||||
OtherButtonText: text.OtherButtonText,
|
||||
}
|
||||
}
|
||||
|
||||
func ExternalUserNotFoundScreenTextToPb(text domain.ExternalUserNotFoundScreenText) *text_pb.ExternalUserNotFoundScreenText {
|
||||
return &text_pb.ExternalUserNotFoundScreenText{
|
||||
Title: text.Title,
|
||||
@@ -890,6 +900,15 @@ func RegistrationOrgScreenTextPbToDomain(text *text_pb.RegistrationOrgScreenText
|
||||
}
|
||||
}
|
||||
|
||||
func LinkingUserPromptScreenTextPbToDomain(text *text_pb.LinkingUserPromptScreenText) domain.LinkingUserPromptScreenText {
|
||||
return domain.LinkingUserPromptScreenText{
|
||||
Title: text.GetTitle(),
|
||||
Description: text.GetDescription(),
|
||||
LinkButtonText: text.GetLinkButtonText(),
|
||||
OtherButtonText: text.GetOtherButtonText(),
|
||||
}
|
||||
}
|
||||
|
||||
func LinkingUserDoneScreenTextPbToDomain(text *text_pb.LinkingUserDoneScreenText) domain.LinkingUserDoneScreenText {
|
||||
if text == nil {
|
||||
return domain.LinkingUserDoneScreenText{}
|
||||
|
@@ -449,6 +449,59 @@ func (l *Login) handleExternalUserAuthenticated(
|
||||
callback(w, r, authReq)
|
||||
}
|
||||
|
||||
// checkAutoLinking checks if a user with the provided information (username or email) already exists within ZITADEL.
|
||||
// The decision, which information will be checked is based on the IdP template option.
|
||||
// The function returns a boolean whether a user was found or not.
|
||||
func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) bool {
|
||||
queries := make([]query.SearchQuery, 0, 2)
|
||||
var user *query.NotifyUser
|
||||
switch provider.AutoLinking {
|
||||
case domain.AutoLinkingOptionUnspecified:
|
||||
// is auto linking is disable, we shouldn't even get here, but in case we do we can directly return
|
||||
return false
|
||||
case domain.AutoLinkingOptionUsername:
|
||||
// if we're checking for usernames there are to options:
|
||||
//
|
||||
// If no specific org has been requested (by id or domain scope), we'll check the provided username against
|
||||
// all existing loginnames and directly use that result to either prompt or continue with other idp options.
|
||||
if authReq.RequestedOrgID == "" {
|
||||
user, err := l.query.GetNotifyUserByLoginName(r.Context(), false, externalUser.PreferredUsername)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
l.renderLinkingUserPrompt(w, r, authReq, user, nil)
|
||||
return true
|
||||
}
|
||||
// If a specific org has been requested, we'll check the provided username against usernames (of that org).
|
||||
usernameQuery, err := query.NewUserUsernameSearchQuery(externalUser.PreferredUsername, query.TextEqualsIgnoreCase)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
queries = append(queries, usernameQuery)
|
||||
case domain.AutoLinkingOptionEmail:
|
||||
// Email will always be checked against verified email addresses.
|
||||
emailQuery, err := query.NewUserVerifiedEmailSearchQuery(string(externalUser.Email))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
queries = append(queries, emailQuery)
|
||||
}
|
||||
// restrict the possible organization if needed (for email and usernames)
|
||||
if authReq.RequestedOrgID != "" {
|
||||
resourceOwnerQuery, err := query.NewUserResourceOwnerSearchQuery(authReq.RequestedOrgID, query.TextEquals)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
queries = append(queries, resourceOwnerQuery)
|
||||
}
|
||||
user, err := l.query.GetNotifyUser(r.Context(), false, queries...)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
l.renderLinkingUserPrompt(w, r, authReq, user, nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// externalUserNotExisting is called if an externalAuthentication couldn't find a corresponding externalID
|
||||
// possible solutions are:
|
||||
//
|
||||
@@ -470,6 +523,13 @@ func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
human, idpLink, _ := mapExternalUserToLoginUser(externalUser, orgIAMPolicy.UserLoginMustBeDomain)
|
||||
// let's check if auto-linking is enabled and if the user would be found by the corresponding option
|
||||
if provider.AutoLinking != domain.AutoLinkingOptionUnspecified {
|
||||
if l.checkAutoLinking(w, r, authReq, provider, externalUser) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if auto creation or creation itself is disabled, send the user to the notFoundOption
|
||||
if !provider.IsCreationAllowed || !provider.IsAutoCreation {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err)
|
||||
|
62
internal/api/ui/login/link_prompt_handler.go
Normal file
62
internal/api/ui/login/link_prompt_handler.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplLinkingUserPrompt = "link_user_prompt"
|
||||
)
|
||||
|
||||
type linkingUserPromptData struct {
|
||||
userData
|
||||
Username string
|
||||
Linking domain.AutoLinkingOption
|
||||
UserID string
|
||||
}
|
||||
|
||||
type linkingUserPromptFormData struct {
|
||||
OtherUser bool `schema:"other"`
|
||||
UserID string `schema:"userID"`
|
||||
}
|
||||
|
||||
func (l *Login) renderLinkingUserPrompt(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser, err error) {
|
||||
var errID, errMessage string
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
identification := user.PreferredLoginName
|
||||
// hide the suffix in case the option is set and the auth request has been started with the primary domain scope
|
||||
if authReq.RequestedOrgDomain && authReq.LabelPolicy != nil && authReq.LabelPolicy.HideLoginNameSuffix {
|
||||
identification = user.Username
|
||||
}
|
||||
data := &linkingUserPromptData{
|
||||
Username: identification,
|
||||
UserID: user.ID,
|
||||
userData: l.getUserData(r, authReq, translator, "LinkingUserPrompt.Title", "LinkingUserPrompt.Description", errID, errMessage),
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkingUserPrompt], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleLinkingUserPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(linkingUserPromptFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if data.OtherUser {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.SelectUser(r.Context(), authReq.ID, data.UserID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
@@ -83,6 +83,7 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName
|
||||
tmplLDAPLogin: "ldap_login.html",
|
||||
tmplDeviceAuthUserCode: "device_usercode.html",
|
||||
tmplDeviceAuthAction: "device_action.html",
|
||||
tmplLinkingUserPrompt: "link_user_prompt.html",
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"resourceUrl": func(file string) string {
|
||||
@@ -235,6 +236,9 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName
|
||||
"ldapUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointLDAPCallback)
|
||||
},
|
||||
"linkingUserPromptUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointLinkingUserPrompt)
|
||||
},
|
||||
}
|
||||
var err error
|
||||
r.Renderer, err = renderer.NewRenderer(
|
||||
|
@@ -53,6 +53,8 @@ const (
|
||||
|
||||
EndpointDeviceAuth = "/device"
|
||||
EndpointDeviceAuthAction = "/device/{action}"
|
||||
|
||||
EndpointLinkingUserPrompt = "/link/user"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -122,5 +124,6 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router
|
||||
router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently))
|
||||
router.HandleFunc(EndpointDeviceAuth, login.handleDeviceAuthUserCode).Methods(http.MethodGet, http.MethodPost)
|
||||
router.HandleFunc(EndpointDeviceAuthAction, login.handleDeviceAuthAction).Methods(http.MethodGet, http.MethodPost)
|
||||
router.HandleFunc(EndpointLinkingUserPrompt, login.handleLinkingUserPrompt).Methods(http.MethodPost)
|
||||
return router
|
||||
}
|
||||
|
@@ -316,6 +316,11 @@ LogoutDone:
|
||||
Title: Излязъл
|
||||
Description: Вие излязохте успешно.
|
||||
LoginButtonText: Влизам
|
||||
LinkingUserPrompt:
|
||||
Title: Намерен съществуващ потребител
|
||||
Description: „Искате ли да свържете съществуващия си акаунт:“
|
||||
LinkButtonText: Връзка
|
||||
OtherButtonText: Други възможности
|
||||
LinkingUsersDone:
|
||||
Title: Свързване с потребители
|
||||
Description: Свързването с потребители е готово.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: Byli jste úspěšně odhlášeni.
|
||||
LoginButtonText: Přihlásit se
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Nalezen stávající uživatel
|
||||
Description: "Chcete propojit svůj stávající účet:"
|
||||
LinkButtonText: Odkaz
|
||||
OtherButtonText: Jiné možnosti
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Propojení uživatele
|
||||
Description: Uživatel propojen.
|
||||
|
@@ -324,6 +324,12 @@ LogoutDone:
|
||||
Description: Du wurdest erfolgreich abgemeldet.
|
||||
LoginButtonText: Anmelden
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Vorhandener Benutzer gefunden
|
||||
Description: "Möchten Sie Ihr bestehendes Konto verknüpfen:"
|
||||
LinkButtonText: Verknüpfen
|
||||
OtherButtonText: Andere Optionen
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Benutzerkonto verknpüfen
|
||||
Description: Benuzterkonto verknpüft.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: You have logged out successfully.
|
||||
LoginButtonText: Login
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Existing User Found
|
||||
Description: "Do you want to link your existing account:"
|
||||
LinkButtonText: Link
|
||||
OtherButtonText: Other options
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Linking User
|
||||
Description: User linked.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: Cerraste la sesión con éxito.
|
||||
LoginButtonText: iniciar sesión
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Usuario existente encontrado
|
||||
Description: "¿Quieres vincular tu cuenta existente?"
|
||||
LinkButtonText: Vincular
|
||||
OtherButtonText: Otras opciones
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Vinculación de usuario
|
||||
Description: usuario vinculado con éxito.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: Vous vous êtes déconnecté avec succès.
|
||||
LoginButtonText: connexion
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Utilisateur existant trouvé
|
||||
Description: "Souhaitez-vous associer votre compte existant:"
|
||||
LinkButtonText: Lier
|
||||
OtherButtonText: Autres options
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Userlinking
|
||||
Description: Le lien avec l'utilisateur est terminé.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: Ti sei disconnesso con successo.
|
||||
LoginButtonText: Accedi
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Utente esistente trovato
|
||||
Description: "Desideri collegare il tuo account esistente:"
|
||||
LinkButtonText: Collegare
|
||||
OtherButtonText: Altre opzioni
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Collegamento utente
|
||||
Description: Collegamento fatto.
|
||||
|
@@ -317,6 +317,12 @@ LogoutDone:
|
||||
Description: 正常にログアウトしました。
|
||||
LoginButtonText: ログイン
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: 既存のユーザーが見つかりました
|
||||
Description: "既存のアカウントをリンクしますか:"
|
||||
LinkButtonText: リンク
|
||||
OtherButtonText: その他のオプション
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: ユーザーリンク
|
||||
Description: ユーザーリンクが完了しました。
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: Успешно сте одјавени.
|
||||
LoginButtonText: најава
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Пронајден е постоечки корисник
|
||||
Description: "Дали сакате да ја поврзете вашата постоечка сметка:"
|
||||
LinkButtonText: Bрска
|
||||
OtherButtonText: Други опции
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Поврзување на корисници
|
||||
Description: Поврзувањето на корисници е завршено.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: U heeft succesvol uitgelogd.
|
||||
LoginButtonText: Inloggen
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Bestaande gebruiker gevonden
|
||||
Description: "Wilt u uw bestaande account koppelen:"
|
||||
LinkButtonText: Koppeling
|
||||
OtherButtonText: Andere opties
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Koppeling Gebruiker
|
||||
Description: Gebruiker gekoppeld.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: Wylogowano pomyślnie.
|
||||
LoginButtonText: Zaloguj się
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Znaleziono istniejącego użytkownika
|
||||
Description: "Czy chcesz połączyć swoje istniejące konto:"
|
||||
LinkButtonText: Połączyć
|
||||
OtherButtonText: Inne opcje
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Łączenie użytkowników
|
||||
Description: Łączenie użytkowników zakończone pomyślnie.
|
||||
|
@@ -321,6 +321,12 @@ LogoutDone:
|
||||
Description: Você fez logout com sucesso.
|
||||
LoginButtonText: login
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Usuário existente encontrado
|
||||
Description: "Deseja vincular sua conta existente:"
|
||||
LinkButtonText: Link
|
||||
OtherButtonText: Outras opções
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Vinculação de usuários
|
||||
Description: Vinculação de usuários concluída.
|
||||
|
@@ -324,6 +324,12 @@ LogoutDone:
|
||||
Description: Вы успешно вышли из системы.
|
||||
LoginButtonText: вход
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: Существующий пользователь найден
|
||||
Description: "Хотите ли вы связать существующую учетную запись:"
|
||||
LinkButtonText: Связь
|
||||
OtherButtonText: Другие варианты
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Привязка пользователя
|
||||
Description: Привязка пользователя выполнена.
|
||||
|
@@ -325,6 +325,12 @@ LogoutDone:
|
||||
Description: 您已成功退出登录。
|
||||
LoginButtonText: 登录
|
||||
|
||||
LinkingUserPrompt:
|
||||
Title: 已找到现有用户
|
||||
Description: "您想关联您现有的帐户吗:"
|
||||
LinkButtonText: 关联
|
||||
OtherButtonText: 其他选项
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: 用户链接
|
||||
Description: 用户链接完成。
|
||||
|
37
internal/api/ui/login/static/templates/link_user_prompt.html
Normal file
37
internal/api/ui/login/static/templates/link_user_prompt.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<div class="lgn-head">
|
||||
<h1>{{t "LinkingUserPrompt.Title"}}</h1>
|
||||
<p>
|
||||
{{t "LinkingUserPrompt.Description"}}<br>
|
||||
{{.Username}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<form action="{{ linkingUserPromptUrl }}" method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="userID" value="{{ .UserID }}" />
|
||||
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="lgn-actions lgn-reverse-order">
|
||||
<a class="lgn-icon-button lgn-left-action" id="back-button" href="#">
|
||||
<i class="lgn-icon-arrow-left-solid"></i>
|
||||
</a>
|
||||
<button class="lgn-raised-button lgn-primary lgn-initial-focus" id="submit-button" type="submit">{{t "LinkingUserPrompt.LinkButtonText"}}</button>
|
||||
<span class="fill-space"></span>
|
||||
<button class="lgn-stroked-button" name="other" value="true">{{t "LinkingUserPrompt.OtherButtonText"}}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/input_suffix_offset.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/go_back.js" }}"></script>
|
||||
|
||||
{{template "main-bottom" .}}
|
Reference in New Issue
Block a user