From 0ebb61343ef34bf74fa0001a253668e9ae2e03fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 15 Oct 2026 23:34:38 +0200 Subject: [PATCH] feat(claim): offer the identity provider on the claim page and name the way in to guardians The claim page shows the password form, a continue-with-provider button, or both, as the preview names them, and explains a refused single-sign-on claim when the callback sends the browser back. The guardian's handover panel says how the person will sign in. --- messages/de.json | 22 +- messages/en.json | 22 +- messages/es.json | 22 +- messages/fr.json | 22 +- messages/it.json | 22 +- messages/ko.json | 22 +- messages/pl.json | 22 +- src/app/auth/claim/page.tsx | 11 +- .../__tests__/claim-profile-invalid.test.tsx | 1 + .../auth/__tests__/claim-profile-sso.test.tsx | 105 +++++++ src/components/auth/claim-profile.tsx | 263 ++++++++++++++---- .../access/managed-profile-handover.tsx | 12 + src/lib/queries/use-managed-profiles.ts | 8 +- 13 files changed, 469 insertions(+), 85 deletions(-) create mode 100644 src/components/auth/__tests__/claim-profile-sso.test.tsx diff --git a/messages/de.json b/messages/de.json index f467801d0..5078d6052 100644 --- a/messages/de.json +++ b/messages/de.json @@ -528,7 +528,7 @@ "loading": "Dein Link wird geprüft…", "invalid": "Dieser Link funktioniert nicht mehr", "invalidHint": "Er ist unbekannt, abgelaufen, zurückgezogen oder schon benutzt. Bitte die Person, die ihn dir gegeben hat, um einen neuen.", - "oidcOnly": "Dieser Server erlaubt nur Single Sign-on, deshalb lässt sich ein Profil hier noch nicht übernehmen.", + "oidcOnly": "Dieser Server erlaubt nur Single Sign-on. Fahre stattdessen mit deinem Identitätsanbieter fort.", "signedInTitle": "Melde dich zuerst ab", "signedInBody": "Du bist als {username} angemeldet. Dieser Link richtet eine Anmeldung für jemand anderen ein, melde dich also ab, bevor du ihn benutzt.", "signOut": "Abmelden und weiter", @@ -545,7 +545,19 @@ "errorTaken": "Dieser Benutzername oder diese E-Mail-Adresse ist schon vergeben.", "errorRateLimit": "Zu viele Versuche. Warte ein paar Minuten und versuche es erneut.", "errorFailed": "Das hat nicht geklappt. Versuche es gleich noch einmal.", - "errorOffline": "Das hat den Server nicht erreicht. Prüfe deine Verbindung und versuche es erneut." + "errorOffline": "Das hat den Server nicht erreicht. Prüfe deine Verbindung und versuche es erneut.", + "ssoContinue": "Weiter mit {provider}", + "ssoStarting": "Weiterleitung…", + "ssoHint": "Ab dann meldest du dich hier mit deinem Konto bei {provider} an.", + "ssoDivider": "oder", + "ssoFallbackName": "Single Sign-on", + "errorTitle": "Die Übernahme hat nicht geklappt", + "errorSubjectBound": "Diese Anmeldung gehört schon zu einem anderen Konto auf diesem Server. Öffne den Link noch einmal und nimm ein anderes Konto bei deinem Identitätsanbieter.", + "errorEmailTaken": "Dein Identitätsanbieter hat eine E-Mail-Adresse geliefert, die hier schon ein anderes Konto nutzt. Öffne den Link noch einmal und nimm ein anderes Konto, oder frag die Person, die den Server betreibt.", + "errorUsernameTaken": "Jemand hat diesen Benutzernamen inzwischen vergeben. Öffne den Link noch einmal und wähle einen anderen.", + "errorSignedIn": "Bei der Rückkehr warst du angemeldet. Melde dich ab und öffne den Link noch einmal.", + "errorSso": "Die Anmeldung bei deinem Identitätsanbieter hat nicht funktioniert. Öffne den Link noch einmal, um es erneut zu versuchen.", + "signInInstead": "Bei diesem Konto anmelden" }, "inviteInvalid": { "title": "Einladung ungültig", @@ -11900,9 +11912,11 @@ "revokeFailed": "Der Link ließ sich nicht zurückziehen. Versuche es erneut.", "errorFailed": "Der Übergabelink ließ sich nicht erstellen.", "errorRosterChanged": "Inzwischen hat sich geändert, wer die Akte betreut. Schließe das hier und öffne es erneut.", - "oidcOnly": "Ein Profil zu übergeben ist nicht möglich, solange dieser Server nur Single Sign-on erlaubt.", + "oidcOnly": "Ein Profil zu übergeben ist auf diesem Server nicht möglich.", "loadError": "Der Stand der Übergabe ließ sich nicht laden.", - "entryHint": "Wenn {name} die Akte selbst führen möchte." + "entryHint": "Wenn {name} die Akte selbst führen möchte.", + "methodSso": "{name} meldet sich beim Einlösen des Links über Single Sign-on an.", + "methodBoth": "{name} kann ein Passwort festlegen oder sich über Single Sign-on anmelden." } }, "toast": { diff --git a/messages/en.json b/messages/en.json index 6afd60c39..1d542da02 100644 --- a/messages/en.json +++ b/messages/en.json @@ -528,7 +528,7 @@ "loading": "Checking your link…", "invalid": "This link no longer works", "invalidHint": "It is unknown, expired, withdrawn or already used. Ask the person who sent it for a new one.", - "oidcOnly": "This server only allows single sign-on, so a profile cannot be taken over here yet.", + "oidcOnly": "This server only allows single sign-on. Continue with your identity provider instead.", "signedInTitle": "Sign out first", "signedInBody": "You are signed in as {username}. This link sets up a sign-in for someone else, so sign out before you use it.", "signOut": "Sign out and continue", @@ -545,7 +545,19 @@ "errorTaken": "That username or email address is already in use.", "errorRateLimit": "Too many attempts. Wait a few minutes and try again.", "errorFailed": "That did not work. Try again in a moment.", - "errorOffline": "That did not reach the server. Check your connection and try again." + "errorOffline": "That did not reach the server. Check your connection and try again.", + "ssoContinue": "Continue with {provider}", + "ssoStarting": "Redirecting…", + "ssoHint": "From then on you sign in here with your {provider} account.", + "ssoDivider": "or", + "ssoFallbackName": "single sign-on", + "errorTitle": "The handover did not go through", + "errorSubjectBound": "This sign-in already belongs to another account on this server. Open the link again and use a different account at your identity provider.", + "errorEmailTaken": "Your identity provider gave an email address that another account here already uses. Open the link again and use a different account, or ask the server's administrator.", + "errorUsernameTaken": "Someone took that username in the meantime. Open the link again and choose another one.", + "errorSignedIn": "You were signed in when you came back. Sign out, then open the link again.", + "errorSso": "Signing in with your identity provider did not work. Open the link again to try once more.", + "signInInstead": "Sign in to that account" }, "inviteInvalid": { "title": "Invitation invalid", @@ -11900,9 +11912,11 @@ "revokeFailed": "The link could not be withdrawn. Try again.", "errorFailed": "The handover link could not be created.", "errorRosterChanged": "The list of guardians changed in the meantime. Close this and open it again.", - "oidcOnly": "Handing over a profile is not available while this server allows single sign-on only.", + "oidcOnly": "Handing over a profile is not available on this server.", "loadError": "The handover status could not be loaded.", - "entryHint": "When {name} is ready to look after this record themselves." + "entryHint": "When {name} is ready to look after this record themselves.", + "methodSso": "{name} signs in through single sign-on when they use the link.", + "methodBoth": "{name} can set a password or sign in through single sign-on." } }, "toast": { diff --git a/messages/es.json b/messages/es.json index 8a5ba0bf7..312bea657 100644 --- a/messages/es.json +++ b/messages/es.json @@ -528,7 +528,7 @@ "loading": "Comprobando tu enlace…", "invalid": "Este enlace ya no funciona", "invalidHint": "Es desconocido, ha caducado, se ha retirado o ya se ha usado. Pide uno nuevo a quien te lo envió.", - "oidcOnly": "Este servidor solo permite el inicio de sesión único, así que aquí todavía no se puede asumir un perfil.", + "oidcOnly": "Este servidor solo permite el inicio de sesión único. Continúa con tu proveedor de identidad.", "signedInTitle": "Cierra la sesión primero", "signedInBody": "Has iniciado sesión como {username}. Este enlace crea un acceso para otra persona, así que cierra la sesión antes de usarlo.", "signOut": "Cerrar sesión y continuar", @@ -545,7 +545,19 @@ "errorTaken": "Ese nombre de usuario o correo electrónico ya está en uso.", "errorRateLimit": "Demasiados intentos. Espera unos minutos y vuelve a intentarlo.", "errorFailed": "No ha funcionado. Vuelve a intentarlo en un momento.", - "errorOffline": "No se llegó al servidor. Comprueba tu conexión y vuelve a intentarlo." + "errorOffline": "No se llegó al servidor. Comprueba tu conexión y vuelve a intentarlo.", + "ssoContinue": "Continuar con {provider}", + "ssoStarting": "Redirigiendo…", + "ssoHint": "A partir de entonces inicias sesión aquí con tu cuenta de {provider}.", + "ssoDivider": "o", + "ssoFallbackName": "inicio de sesión único", + "errorTitle": "La entrega no se ha completado", + "errorSubjectBound": "Este inicio de sesión ya pertenece a otra cuenta de este servidor. Vuelve a abrir el enlace y usa otra cuenta en tu proveedor de identidad.", + "errorEmailTaken": "Tu proveedor de identidad ha enviado una dirección de correo que ya usa otra cuenta aquí. Vuelve a abrir el enlace y usa otra cuenta, o pregunta a quien administra el servidor.", + "errorUsernameTaken": "Alguien ha cogido ese nombre de usuario mientras tanto. Vuelve a abrir el enlace y elige otro.", + "errorSignedIn": "Tenías una sesión iniciada al volver. Cierra la sesión y vuelve a abrir el enlace.", + "errorSso": "No se ha podido iniciar sesión con tu proveedor de identidad. Vuelve a abrir el enlace para intentarlo otra vez.", + "signInInstead": "Iniciar sesión en esa cuenta" }, "inviteInvalid": { "title": "Invitación no válida", @@ -11900,9 +11912,11 @@ "revokeFailed": "No se pudo retirar el enlace. Vuelve a intentarlo.", "errorFailed": "No se pudo crear el enlace de entrega.", "errorRosterChanged": "Mientras tanto ha cambiado quién se ocupa del historial. Cierra esto y vuelve a abrirlo.", - "oidcOnly": "No se puede entregar un perfil mientras este servidor solo permita el inicio de sesión único.", + "oidcOnly": "En este servidor no se puede entregar un perfil.", "loadError": "No se pudo cargar el estado de la entrega.", - "entryHint": "Cuando {name} quiera llevar este historial por su cuenta." + "entryHint": "Cuando {name} quiera llevar este historial por su cuenta.", + "methodSso": "{name} inicia sesión con el inicio de sesión único cuando usa el enlace.", + "methodBoth": "{name} puede elegir una contraseña o iniciar sesión con el inicio de sesión único." } }, "toast": { diff --git a/messages/fr.json b/messages/fr.json index 5d3bd5fd1..7302f9fc3 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -528,7 +528,7 @@ "loading": "Vérification de votre lien…", "invalid": "Ce lien ne fonctionne plus", "invalidHint": "Il est inconnu, expiré, retiré ou déjà utilisé. Demandez-en un nouveau à la personne qui vous l'a envoyé.", - "oidcOnly": "Ce serveur n'autorise que l'authentification unique, un profil ne peut donc pas encore être repris ici.", + "oidcOnly": "Ce serveur n'autorise que l'authentification unique. Continuez plutôt avec votre fournisseur d'identité.", "signedInTitle": "Déconnectez-vous d'abord", "signedInBody": "Vous êtes connecté en tant que {username}. Ce lien crée une connexion pour quelqu'un d'autre, déconnectez-vous donc avant de l'utiliser.", "signOut": "Se déconnecter et continuer", @@ -545,7 +545,19 @@ "errorTaken": "Ce nom d'utilisateur ou cette adresse e-mail est déjà utilisé.", "errorRateLimit": "Trop de tentatives. Attendez quelques minutes puis réessayez.", "errorFailed": "Cela n'a pas fonctionné. Réessayez dans un instant.", - "errorOffline": "Le serveur n'a pas été atteint. Vérifiez votre connexion et réessayez." + "errorOffline": "Le serveur n'a pas été atteint. Vérifiez votre connexion et réessayez.", + "ssoContinue": "Continuer avec {provider}", + "ssoStarting": "Redirection…", + "ssoHint": "Ensuite, vous vous connectez ici avec votre compte {provider}.", + "ssoDivider": "ou", + "ssoFallbackName": "l'authentification unique", + "errorTitle": "La transmission n'a pas abouti", + "errorSubjectBound": "Cette connexion appartient déjà à un autre compte sur ce serveur. Rouvrez le lien et utilisez un autre compte chez votre fournisseur d'identité.", + "errorEmailTaken": "Votre fournisseur d'identité a transmis une adresse e-mail déjà utilisée par un autre compte ici. Rouvrez le lien et utilisez un autre compte, ou contactez la personne qui administre le serveur.", + "errorUsernameTaken": "Quelqu'un a pris ce nom d'utilisateur entre-temps. Rouvrez le lien et choisissez-en un autre.", + "errorSignedIn": "Vous étiez connecté à votre retour. Déconnectez-vous, puis rouvrez le lien.", + "errorSso": "La connexion avec votre fournisseur d'identité n'a pas fonctionné. Rouvrez le lien pour réessayer.", + "signInInstead": "Se connecter à ce compte" }, "inviteInvalid": { "title": "Invitation invalide", @@ -11900,9 +11912,11 @@ "revokeFailed": "Le lien n'a pas pu être retiré. Réessayez.", "errorFailed": "Le lien de transmission n'a pas pu être créé.", "errorRosterChanged": "Entre-temps, les personnes qui s'occupent du dossier ont changé. Fermez ceci et rouvrez-le.", - "oidcOnly": "Transmettre un profil n'est pas possible tant que ce serveur n'autorise que l'authentification unique.", + "oidcOnly": "Transmettre un profil n'est pas possible sur ce serveur.", "loadError": "L'état de la transmission n'a pas pu être chargé.", - "entryHint": "Quand {name} souhaite tenir ce dossier de façon autonome." + "entryHint": "Quand {name} souhaite tenir ce dossier de façon autonome.", + "methodSso": "{name} se connecte par authentification unique en utilisant le lien.", + "methodBoth": "{name} peut définir un mot de passe ou se connecter par authentification unique." } }, "toast": { diff --git a/messages/it.json b/messages/it.json index adaf75f83..c9a714b6e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -528,7 +528,7 @@ "loading": "Controllo del link…", "invalid": "Questo link non funziona più", "invalidHint": "È sconosciuto, scaduto, ritirato o già usato. Chiedine uno nuovo a chi te l'ha inviato.", - "oidcOnly": "Questo server consente solo il single sign-on, quindi qui non è ancora possibile prendere in carico un profilo.", + "oidcOnly": "Questo server consente solo il single sign-on. Continua invece con il tuo provider di identità.", "signedInTitle": "Prima esci", "signedInBody": "Hai effettuato l'accesso come {username}. Questo link crea un accesso per un'altra persona, quindi esci prima di usarlo.", "signOut": "Esci e continua", @@ -545,7 +545,19 @@ "errorTaken": "Questo nome utente o indirizzo email è già in uso.", "errorRateLimit": "Troppi tentativi. Attendi qualche minuto e riprova.", "errorFailed": "Non ha funzionato. Riprova tra un momento.", - "errorOffline": "Il server non è stato raggiunto. Controlla la connessione e riprova." + "errorOffline": "Il server non è stato raggiunto. Controlla la connessione e riprova.", + "ssoContinue": "Continua con {provider}", + "ssoStarting": "Reindirizzamento…", + "ssoHint": "Da quel momento accedi qui con il tuo account {provider}.", + "ssoDivider": "oppure", + "ssoFallbackName": "single sign-on (SSO)", + "errorTitle": "La consegna non è andata a buon fine", + "errorSubjectBound": "Questo accesso appartiene già a un altro account su questo server. Riapri il link e usa un altro account presso il tuo provider di identità.", + "errorEmailTaken": "Il tuo provider di identità ha fornito un indirizzo email che qui usa già un altro account. Riapri il link e usa un altro account, oppure chiedi a chi amministra il server.", + "errorUsernameTaken": "Nel frattempo qualcuno ha preso quel nome utente. Riapri il link e scegline un altro.", + "errorSignedIn": "Al ritorno avevi già effettuato l'accesso. Esci, poi riapri il link.", + "errorSso": "L'accesso con il tuo provider di identità non è riuscito. Riapri il link per riprovare.", + "signInInstead": "Accedi a quell'account" }, "inviteInvalid": { "title": "Invito non valido", @@ -11900,9 +11912,11 @@ "revokeFailed": "Impossibile ritirare il link. Riprova.", "errorFailed": "Impossibile creare il link di consegna.", "errorRosterChanged": "Nel frattempo è cambiato chi si occupa della cartella. Chiudi e riapri.", - "oidcOnly": "Non è possibile consegnare un profilo finché questo server consente solo il single sign-on.", + "oidcOnly": "Su questo server non è possibile consegnare un profilo.", "loadError": "Impossibile caricare lo stato della consegna.", - "entryHint": "Quando {name} vuole tenere questa cartella in autonomia." + "entryHint": "Quando {name} vuole tenere questa cartella in autonomia.", + "methodSso": "{name} accede con il single sign-on quando usa il link.", + "methodBoth": "{name} può impostare una password oppure accedere con il single sign-on." } }, "toast": { diff --git a/messages/ko.json b/messages/ko.json index e958a666a..0c8f8a915 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -528,7 +528,7 @@ "loading": "링크를 확인하는 중…", "invalid": "이 링크는 더 이상 작동하지 않습니다", "invalidHint": "알 수 없거나, 만료되었거나, 철회되었거나, 이미 사용된 링크입니다. 보낸 사람에게 새 링크를 요청하세요.", - "oidcOnly": "이 서버는 통합 로그인(SSO)만 허용하므로 아직 여기서 프로필을 넘겨받을 수 없습니다.", + "oidcOnly": "이 서버는 통합 로그인(SSO)만 허용합니다. 대신 ID 공급자로 계속하세요.", "signedInTitle": "먼저 로그아웃하세요", "signedInBody": "{username} 계정으로 로그인되어 있습니다. 이 링크는 다른 사람의 로그인을 만드는 링크이므로 사용하기 전에 로그아웃하세요.", "signOut": "로그아웃하고 계속", @@ -545,7 +545,19 @@ "errorTaken": "이미 사용 중인 사용자 이름 또는 이메일 주소입니다.", "errorRateLimit": "시도 횟수가 너무 많습니다. 몇 분 후 다시 시도하세요.", "errorFailed": "처리하지 못했습니다. 잠시 후 다시 시도하세요.", - "errorOffline": "서버에 연결하지 못했습니다. 연결을 확인하고 다시 시도하세요." + "errorOffline": "서버에 연결하지 못했습니다. 연결을 확인하고 다시 시도하세요.", + "ssoContinue": "{provider}(으)로 계속", + "ssoStarting": "이동하는 중…", + "ssoHint": "이후에는 {provider} 계정으로 여기에 로그인합니다.", + "ssoDivider": "또는", + "ssoFallbackName": "통합 로그인(SSO)", + "errorTitle": "넘겨받기를 완료하지 못했습니다", + "errorSubjectBound": "이 로그인은 이미 이 서버의 다른 계정에 연결되어 있습니다. 링크를 다시 열고 ID 공급자에서 다른 계정을 사용하세요.", + "errorEmailTaken": "ID 공급자가 보낸 이메일 주소를 이 서버의 다른 계정이 이미 사용하고 있습니다. 링크를 다시 열고 다른 계정을 사용하거나 서버 관리자에게 문의하세요.", + "errorUsernameTaken": "그 사이에 다른 사람이 이 사용자 이름을 사용했습니다. 링크를 다시 열고 다른 이름을 선택하세요.", + "errorSignedIn": "돌아왔을 때 이미 로그인되어 있었습니다. 로그아웃한 다음 링크를 다시 여세요.", + "errorSso": "ID 공급자로 로그인하지 못했습니다. 링크를 다시 열어 한 번 더 시도하세요.", + "signInInstead": "그 계정으로 로그인" }, "inviteInvalid": { "title": "유효하지 않은 초대예요", @@ -11900,9 +11912,11 @@ "revokeFailed": "링크를 철회하지 못했습니다. 다시 시도하세요.", "errorFailed": "넘겨주기 링크를 만들지 못했습니다.", "errorRosterChanged": "그 사이 기록을 돌보는 사람이 바뀌었습니다. 닫았다가 다시 여세요.", - "oidcOnly": "이 서버가 통합 로그인(SSO)만 허용하는 동안에는 프로필을 넘겨줄 수 없습니다.", + "oidcOnly": "이 서버에서는 프로필을 넘겨줄 수 없습니다.", "loadError": "넘겨주기 상태를 불러오지 못했습니다.", - "entryHint": "{name} 님이 이 기록을 직접 관리할 준비가 되었을 때." + "entryHint": "{name} 님이 이 기록을 직접 관리할 준비가 되었을 때.", + "methodSso": "{name}님은 링크를 사용할 때 통합 로그인(SSO)으로 로그인합니다.", + "methodBoth": "{name}님은 비밀번호를 정하거나 통합 로그인(SSO)으로 로그인할 수 있습니다." } }, "toast": { diff --git a/messages/pl.json b/messages/pl.json index 4cf8b75a8..da6ada07c 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -528,7 +528,7 @@ "loading": "Sprawdzanie linku…", "invalid": "Ten link już nie działa", "invalidHint": "Jest nieznany, wygasł, został wycofany albo już użyty. Poproś osobę, która go wysłała, o nowy.", - "oidcOnly": "Ten serwer pozwala tylko na logowanie jednokrotne (SSO), więc profilu nie można tu jeszcze przejąć.", + "oidcOnly": "Ten serwer pozwala tylko na logowanie jednokrotne (SSO). Kontynuuj u swojego dostawcy tożsamości.", "signedInTitle": "Najpierw się wyloguj", "signedInBody": "Masz zalogowaną sesję jako {username}. Ten link tworzy logowanie dla kogoś innego, więc wyloguj się, zanim go użyjesz.", "signOut": "Wyloguj i kontynuuj", @@ -545,7 +545,19 @@ "errorTaken": "Ta nazwa użytkownika lub adres e-mail jest już zajęty.", "errorRateLimit": "Zbyt wiele prób. Odczekaj kilka minut i spróbuj ponownie.", "errorFailed": "Nie udało się. Spróbuj ponownie za chwilę.", - "errorOffline": "Nie udało się połączyć z serwerem. Sprawdź połączenie i spróbuj ponownie." + "errorOffline": "Nie udało się połączyć z serwerem. Sprawdź połączenie i spróbuj ponownie.", + "ssoContinue": "Kontynuuj przez {provider}", + "ssoStarting": "Przekierowywanie…", + "ssoHint": "Od tej pory logujesz się tutaj swoim kontem {provider}.", + "ssoDivider": "lub", + "ssoFallbackName": "logowanie jednokrotne (SSO)", + "errorTitle": "Przejęcie się nie udało", + "errorSubjectBound": "To logowanie należy już do innego konta na tym serwerze. Otwórz link ponownie i użyj innego konta u swojego dostawcy tożsamości.", + "errorEmailTaken": "Twój dostawca tożsamości przekazał adres e-mail, którego używa już inne konto na tym serwerze. Otwórz link ponownie i użyj innego konta albo zapytaj osobę, która administruje serwerem.", + "errorUsernameTaken": "W międzyczasie ktoś zajął tę nazwę użytkownika. Otwórz link ponownie i wybierz inną.", + "errorSignedIn": "Po powrocie byłeś zalogowany. Wyloguj się i otwórz link ponownie.", + "errorSso": "Logowanie u dostawcy tożsamości się nie powiodło. Otwórz link ponownie, aby spróbować jeszcze raz.", + "signInInstead": "Zaloguj się na to konto" }, "inviteInvalid": { "title": "Nieprawidłowe zaproszenie", @@ -11900,9 +11912,11 @@ "revokeFailed": "Nie udało się wycofać linku. Spróbuj ponownie.", "errorFailed": "Nie udało się utworzyć linku przekazania.", "errorRosterChanged": "W międzyczasie zmieniło się, kto opiekuje się kartą. Zamknij to i otwórz ponownie.", - "oidcOnly": "Przekazanie profilu nie jest możliwe, dopóki ten serwer pozwala tylko na logowanie jednokrotne (SSO).", + "oidcOnly": "Przekazanie profilu nie jest możliwe na tym serwerze.", "loadError": "Nie udało się wczytać stanu przekazania.", - "entryHint": "Gdy {name} chce samodzielnie prowadzić tę kartę." + "entryHint": "Gdy {name} chce samodzielnie prowadzić tę kartę.", + "methodSso": "{name} loguje się przez logowanie jednokrotne (SSO), gdy użyje linku.", + "methodBoth": "{name} może ustawić hasło albo zalogować się przez logowanie jednokrotne (SSO)." } }, "toast": { diff --git a/src/app/auth/claim/page.tsx b/src/app/auth/claim/page.tsx index 217302b9c..2a37b693f 100644 --- a/src/app/auth/claim/page.tsx +++ b/src/app/auth/claim/page.tsx @@ -6,7 +6,9 @@ import { ClaimProfile } from "@/components/auth/claim-profile"; /** * v1.42 (#959) — `/auth/claim?token=hlp_…`, where the link a Guardian handed - * over (`/claim/`) lands after the edge redirect. + * over (`/claim/`) lands after the edge redirect. Since v1.43 also + * `/auth/claim?error=`, where a refused single-sign-on claim returns; + * the reason is a fixed word, never a credential. * * A URL-credential surface by shape (`session-surface-guard.test.ts`, S2): the * page reads the one-time token off the query string and passes it to the @@ -16,5 +18,10 @@ import { ClaimProfile } from "@/components/auth/claim-profile"; */ export default function ClaimProfilePage() { const searchParams = useSearchParams(); - return ; + return ( + + ); } diff --git a/src/components/auth/__tests__/claim-profile-invalid.test.tsx b/src/components/auth/__tests__/claim-profile-invalid.test.tsx index b240065c0..5e0881043 100644 --- a/src/components/auth/__tests__/claim-profile-invalid.test.tsx +++ b/src/components/auth/__tests__/claim-profile-invalid.test.tsx @@ -19,6 +19,7 @@ vi.mock("@/hooks/use-auth", () => ({ vi.mock("@/lib/queries/use-profile-claim", () => ({ useProfileClaimPreview: () => ({ data: undefined, error: null }), useClaimProfile: () => ({ mutate: () => {}, isPending: false }), + useClaimProfileSso: () => ({ mutate: () => {}, isPending: false }), })); import { ClaimProfile } from "../claim-profile"; diff --git a/src/components/auth/__tests__/claim-profile-sso.test.tsx b/src/components/auth/__tests__/claim-profile-sso.test.tsx new file mode 100644 index 000000000..75d1c8d99 --- /dev/null +++ b/src/components/auth/__tests__/claim-profile-sso.test.tsx @@ -0,0 +1,105 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { I18nProvider } from "@/lib/i18n/context"; +import type { ProfileClaimPreview } from "@/lib/queries/use-profile-claim"; + +/** + * v1.43 (#959) — the claim page offers the ways in the server names: the + * password form, the identity provider, or both. A refused single-sign-on + * claim comes back with `?error=` and no link, and says what happened. + */ + +let previewData: ProfileClaimPreview | undefined; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ replace: () => {}, refresh: () => {}, push: () => {} }), +})); +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: null }), + clearCachesForSessionEnd: () => {}, +})); +vi.mock("@/lib/queries/use-profile-claim", () => ({ + useProfileClaimPreview: () => ({ data: previewData, error: null }), + useClaimProfile: () => ({ mutate: () => {}, isPending: false }), + useClaimProfileSso: () => ({ mutate: () => {}, isPending: false }), +})); + +import { ClaimProfile, claimSsoErrorKey } from "../claim-profile"; + +const BASE: ProfileClaimPreview = { + displayName: "Robin", + expiresAt: "2030-01-01T00:00:00.000Z", + guardians: [], +}; + +function render(props: { token: string | null; error?: string | null }) { + return renderToStaticMarkup( + + + + + , + ); +} + +describe("ClaimProfile — ways in", () => { + it("offers only the provider on a single-sign-on-only server", () => { + previewData = { ...BASE, methods: ["sso"], ssoProviderName: "Example ID" }; + const html = render({ token: "hlp_x" }); + expect(html).toContain('data-slot="claim-sso-continue"'); + expect(html).toContain("Continue with Example ID"); + expect(html).not.toContain('id="claim-password"'); + expect(html).not.toContain('id="claim-email"'); + expect(html).not.toContain('data-slot="claim-submit"'); + expect(html).toContain('id="claim-username"'); + }); + + it("offers both when both are allowed", () => { + previewData = { + ...BASE, + methods: ["password", "sso"], + ssoProviderName: "Example ID", + }; + const html = render({ token: "hlp_x" }); + expect(html).toContain('data-slot="claim-sso-continue"'); + expect(html).toContain('id="claim-password"'); + expect(html).toContain('data-slot="claim-submit"'); + }); + + it("offers only the password when an older server names no methods", () => { + previewData = { ...BASE }; + const html = render({ token: "hlp_x" }); + expect(html).not.toContain('data-slot="claim-sso-continue"'); + expect(html).toContain('id="claim-password"'); + }); +}); + +describe("ClaimProfile — a refused single-sign-on claim", () => { + it("says what happened and offers a way on", () => { + previewData = undefined; + const html = render({ token: null, error: "claim_sso_subject_bound" }); + expect(html).toContain('data-testid="claim-sso-error"'); + expect(html).toContain("already belongs to another account"); + expect(html).toContain('href="/auth/login"'); + }); + + it("maps every reason the callback sends, and reads an unknown one as a failure", () => { + expect(claimSsoErrorKey("claim_expired")).toBe("auth.claim.invalidHint"); + expect(claimSsoErrorKey("claim_sso_subject_bound")).toBe( + "auth.claim.errorSubjectBound", + ); + expect(claimSsoErrorKey("claim_email_taken")).toBe( + "auth.claim.errorEmailTaken", + ); + expect(claimSsoErrorKey("claim_username_taken")).toBe( + "auth.claim.errorUsernameTaken", + ); + expect(claimSsoErrorKey("claim_signed_in")).toBe( + "auth.claim.errorSignedIn", + ); + expect(claimSsoErrorKey("oidc_failed")).toBe("auth.claim.errorSso"); + expect(claimSsoErrorKey("