No address step before signing in when there's only one mail server
The server's own page asks for the username, so with a single server the sign-in page keeps only the own-device choice. With several servers the address still comes first, since its domain picks the server. One new string, in all nine catalogues.
This commit is contained in:
@@ -15,7 +15,11 @@ keep up. Nothing here is pushed there.
|
|||||||
handles a password to sign someone in. Two-factor codes are asked for on
|
handles a password to sign someone in. Two-factor codes are asked for on
|
||||||
that page. Sessions hold sealed tokens and renew them before they expire. A
|
that page. Sessions hold sealed tokens and renew them before they expire. A
|
||||||
password change revokes the tokens, so it signs the person out everywhere,
|
password change revokes the tokens, so it signs the person out everywhere,
|
||||||
this session included.
|
this session included. With one mail server (no `STALWART_SERVERS_FILE`,
|
||||||
|
or one whose domains all map to `STALWART_URL`), the sign-in page asks for
|
||||||
|
no address: only whether this is the person's own device, then the server's
|
||||||
|
page takes it from there. With several servers, the address comes first,
|
||||||
|
since its domain picks the server.
|
||||||
- **Tenants are offered on every server**, whatever edition it reports.
|
- **Tenants are offered on every server**, whatever edition it reports.
|
||||||
`SHOW_ENTERPRISE_NOTICES` still adds the notice for an upstream Stalwart.
|
`SHOW_ENTERPRISE_NOTICES` still adds the notice for an upstream Stalwart.
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -41,7 +41,7 @@ import {
|
|||||||
revokeAppPassword,
|
revokeAppPassword,
|
||||||
} from "./account.js";
|
} from "./account.js";
|
||||||
import { imageProxyHandler } from "./imageproxy.js";
|
import { imageProxyHandler } from "./imageproxy.js";
|
||||||
import { SignInError, finish as finishSignIn, needsRefresh, oauthEnabled, refreshTokens, start as startSignIn, type TokenSet } from "./oauth.js";
|
import { SignInError, finish as finishSignIn, needsRefresh, oauthEnabled, refreshTokens, singleServer, start as startSignIn, type TokenSet } from "./oauth.js";
|
||||||
import { icsProxyHandler } from "./icsproxy.js";
|
import { icsProxyHandler } from "./icsproxy.js";
|
||||||
import { staticHandler } from "./static.js";
|
import { staticHandler } from "./static.js";
|
||||||
|
|
||||||
@@ -384,6 +384,9 @@ export function createApp(basePath = config.basePath): Hono<Env> {
|
|||||||
settingsPolicy: config.settingsPolicy,
|
settingsPolicy: config.settingsPolicy,
|
||||||
/* "oauth": sign in on the mail server's own page (see oauth.ts). */
|
/* "oauth": sign in on the mail server's own page (see oauth.ts). */
|
||||||
signIn: oauthEnabled() ? "oauth" : "password",
|
signIn: oauthEnabled() ? "oauth" : "password",
|
||||||
|
/* With "oauth": true when the server's page can take it from here, so
|
||||||
|
the sign-in form doesn't ask for an address first. */
|
||||||
|
signInDirect: oauthEnabled() && singleServer(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ after(() => {
|
|||||||
test("the configuration tells the web app to use the server's page", async () => {
|
test("the configuration tells the web app to use the server's page", async () => {
|
||||||
const body = await jsonOf(await call("/api/config"));
|
const body = await jsonOf(await call("/api/config"));
|
||||||
assert.equal(body.signIn, "oauth");
|
assert.equal(body.signIn, "oauth");
|
||||||
|
assert.equal(body.signInDirect, true, "one mail server: its page asks for the username, not ihasmail");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with one mail server, sign-in starts without an address", async () => {
|
||||||
|
const res = await call("/api/auth/oauth/start");
|
||||||
|
assert.equal(res.status, 302);
|
||||||
|
const to = new URL(res.headers.get("location")!);
|
||||||
|
assert.equal(to.searchParams.has("login_hint"), false);
|
||||||
|
const approved = await fetch(to, { redirect: "manual" });
|
||||||
|
const back = new URL(approved.headers.get("location")!);
|
||||||
|
assert.equal((await call(`/api/auth/callback${back.search}`)).headers.get("location"), "/");
|
||||||
|
assert.equal((await call("/api/auth/session")).status, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the password form is refused: ihasmail never sees a password", async () => {
|
test("the password form is refused: ihasmail never sees a password", async () => {
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ export function oauthEnabled(): boolean {
|
|||||||
return Boolean(config.oauthClientSecret);
|
return Boolean(config.oauthClientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether every account is on the same server. Then sign-in needs no address
|
||||||
|
* first: the server's page asks for the username itself. With several servers
|
||||||
|
* (STALWART_SERVERS_FILE), the domain picks the server, so the address comes
|
||||||
|
* first.
|
||||||
|
*/
|
||||||
|
export function singleServer(): boolean {
|
||||||
|
return Object.values(config.stalwartServers).every((url) => url === config.stalwartUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/** The one redirect URI registered for this client on the server. */
|
/** The one redirect URI registered for this client on the server. */
|
||||||
export function redirectUri(): string {
|
export function redirectUri(): string {
|
||||||
return `${config.publicUrl}${config.basePath}/api/auth/callback`;
|
return `${config.publicUrl}${config.basePath}/api/auth/callback`;
|
||||||
|
|||||||
@@ -1243,6 +1243,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "Die Lesebestätigung konnte nicht gesendet werden: {error}",
|
"Could not send the receipt: {error}": "Die Lesebestätigung konnte nicht gesendet werden: {error}",
|
||||||
"Could not sign in.": "Anmeldung fehlgeschlagen.",
|
"Could not sign in.": "Anmeldung fehlgeschlagen.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "Ihr Passwort geben Sie auf der Anmeldeseite Ihres Mailservers ein.",
|
"You'll enter your password on your mail server's sign-in page.": "Ihr Passwort geben Sie auf der Anmeldeseite Ihres Mailservers ein.",
|
||||||
|
"You'll sign in on your mail server's own page.": "Sie melden sich auf der eigenen Seite Ihres Mailservers an.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Diese Anmeldung wurde nicht in diesem Browser begonnen. Bitte versuchen Sie es erneut.",
|
"This sign-in didn't start in this browser. Try again.": "Diese Anmeldung wurde nicht in diesem Browser begonnen. Bitte versuchen Sie es erneut.",
|
||||||
"The sign-in took too long. Try again.": "Die Anmeldung hat zu lange gedauert. Bitte versuchen Sie es erneut.",
|
"The sign-in took too long. Try again.": "Die Anmeldung hat zu lange gedauert. Bitte versuchen Sie es erneut.",
|
||||||
"Sign-in was cancelled.": "Die Anmeldung wurde abgebrochen.",
|
"Sign-in was cancelled.": "Die Anmeldung wurde abgebrochen.",
|
||||||
|
|||||||
@@ -1216,6 +1216,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "No se pudo enviar la confirmación de lectura: {error}",
|
"Could not send the receipt: {error}": "No se pudo enviar la confirmación de lectura: {error}",
|
||||||
"Could not sign in.": "No se pudo iniciar sesión.",
|
"Could not sign in.": "No se pudo iniciar sesión.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "Introducirá su contraseña en la página de inicio de sesión de su servidor de correo.",
|
"You'll enter your password on your mail server's sign-in page.": "Introducirá su contraseña en la página de inicio de sesión de su servidor de correo.",
|
||||||
|
"You'll sign in on your mail server's own page.": "Iniciará sesión en la propia página de su servidor de correo.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Este inicio de sesión no empezó en este navegador. Inténtelo de nuevo.",
|
"This sign-in didn't start in this browser. Try again.": "Este inicio de sesión no empezó en este navegador. Inténtelo de nuevo.",
|
||||||
"The sign-in took too long. Try again.": "El inicio de sesión tardó demasiado. Inténtelo de nuevo.",
|
"The sign-in took too long. Try again.": "El inicio de sesión tardó demasiado. Inténtelo de nuevo.",
|
||||||
"Sign-in was cancelled.": "Se canceló el inicio de sesión.",
|
"Sign-in was cancelled.": "Se canceló el inicio de sesión.",
|
||||||
|
|||||||
@@ -1221,6 +1221,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "Impossible d’envoyer l’accusé de lecture : {error}",
|
"Could not send the receipt: {error}": "Impossible d’envoyer l’accusé de lecture : {error}",
|
||||||
"Could not sign in.": "Connexion impossible.",
|
"Could not sign in.": "Connexion impossible.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "Vous saisirez votre mot de passe sur la page de connexion de votre serveur de messagerie.",
|
"You'll enter your password on your mail server's sign-in page.": "Vous saisirez votre mot de passe sur la page de connexion de votre serveur de messagerie.",
|
||||||
|
"You'll sign in on your mail server's own page.": "Vous vous connecterez sur la page de votre serveur de messagerie.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Cette connexion n’a pas commencé dans ce navigateur. Réessayez.",
|
"This sign-in didn't start in this browser. Try again.": "Cette connexion n’a pas commencé dans ce navigateur. Réessayez.",
|
||||||
"The sign-in took too long. Try again.": "La connexion a pris trop de temps. Réessayez.",
|
"The sign-in took too long. Try again.": "La connexion a pris trop de temps. Réessayez.",
|
||||||
"Sign-in was cancelled.": "La connexion a été annulée.",
|
"Sign-in was cancelled.": "La connexion a été annulée.",
|
||||||
|
|||||||
@@ -1224,6 +1224,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "開封確認を送信できませんでした: {error}",
|
"Could not send the receipt: {error}": "開封確認を送信できませんでした: {error}",
|
||||||
"Could not sign in.": "サインインできませんでした。",
|
"Could not sign in.": "サインインできませんでした。",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "パスワードはメールサーバーのサインインページで入力します。",
|
"You'll enter your password on your mail server's sign-in page.": "パスワードはメールサーバーのサインインページで入力します。",
|
||||||
|
"You'll sign in on your mail server's own page.": "メールサーバー自身のページでサインインします。",
|
||||||
"This sign-in didn't start in this browser. Try again.": "このサインインはこのブラウザーで開始されたものではありません。もう一度お試しください。",
|
"This sign-in didn't start in this browser. Try again.": "このサインインはこのブラウザーで開始されたものではありません。もう一度お試しください。",
|
||||||
"The sign-in took too long. Try again.": "サインインに時間がかかりすぎました。もう一度お試しください。",
|
"The sign-in took too long. Try again.": "サインインに時間がかかりすぎました。もう一度お試しください。",
|
||||||
"Sign-in was cancelled.": "サインインはキャンセルされました。",
|
"Sign-in was cancelled.": "サインインはキャンセルされました。",
|
||||||
|
|||||||
@@ -1214,6 +1214,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "De leesbevestiging kon niet worden verzonden: {error}",
|
"Could not send the receipt: {error}": "De leesbevestiging kon niet worden verzonden: {error}",
|
||||||
"Could not sign in.": "Aanmelden mislukt.",
|
"Could not sign in.": "Aanmelden mislukt.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "U voert uw wachtwoord in op de aanmeldpagina van uw mailserver.",
|
"You'll enter your password on your mail server's sign-in page.": "U voert uw wachtwoord in op de aanmeldpagina van uw mailserver.",
|
||||||
|
"You'll sign in on your mail server's own page.": "U meldt zich aan op de eigen pagina van uw mailserver.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Deze aanmelding is niet in deze browser begonnen. Probeer het opnieuw.",
|
"This sign-in didn't start in this browser. Try again.": "Deze aanmelding is niet in deze browser begonnen. Probeer het opnieuw.",
|
||||||
"The sign-in took too long. Try again.": "Het aanmelden duurde te lang. Probeer het opnieuw.",
|
"The sign-in took too long. Try again.": "Het aanmelden duurde te lang. Probeer het opnieuw.",
|
||||||
"Sign-in was cancelled.": "Het aanmelden is geannuleerd.",
|
"Sign-in was cancelled.": "Het aanmelden is geannuleerd.",
|
||||||
|
|||||||
@@ -1219,6 +1219,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "Não foi possível enviar a confirmação de leitura: {error}",
|
"Could not send the receipt: {error}": "Não foi possível enviar a confirmação de leitura: {error}",
|
||||||
"Could not sign in.": "Não foi possível entrar.",
|
"Could not sign in.": "Não foi possível entrar.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "Você vai digitar sua senha na página de entrada do seu servidor de e-mail.",
|
"You'll enter your password on your mail server's sign-in page.": "Você vai digitar sua senha na página de entrada do seu servidor de e-mail.",
|
||||||
|
"You'll sign in on your mail server's own page.": "Você vai entrar na própria página do seu servidor de e-mail.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Esta entrada não começou neste navegador. Tente de novo.",
|
"This sign-in didn't start in this browser. Try again.": "Esta entrada não começou neste navegador. Tente de novo.",
|
||||||
"The sign-in took too long. Try again.": "A entrada demorou demais. Tente de novo.",
|
"The sign-in took too long. Try again.": "A entrada demorou demais. Tente de novo.",
|
||||||
"Sign-in was cancelled.": "A entrada foi cancelada.",
|
"Sign-in was cancelled.": "A entrada foi cancelada.",
|
||||||
|
|||||||
@@ -1218,6 +1218,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "Не удалось отправить уведомление о прочтении: {error}",
|
"Could not send the receipt: {error}": "Не удалось отправить уведомление о прочтении: {error}",
|
||||||
"Could not sign in.": "Не удалось войти.",
|
"Could not sign in.": "Не удалось войти.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "Пароль вводится на странице входа вашего почтового сервера.",
|
"You'll enter your password on your mail server's sign-in page.": "Пароль вводится на странице входа вашего почтового сервера.",
|
||||||
|
"You'll sign in on your mail server's own page.": "Вход выполняется на странице вашего почтового сервера.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Этот вход начат не в этом браузере. Попробуйте ещё раз.",
|
"This sign-in didn't start in this browser. Try again.": "Этот вход начат не в этом браузере. Попробуйте ещё раз.",
|
||||||
"The sign-in took too long. Try again.": "Вход занял слишком много времени. Попробуйте ещё раз.",
|
"The sign-in took too long. Try again.": "Вход занял слишком много времени. Попробуйте ещё раз.",
|
||||||
"Sign-in was cancelled.": "Вход отменён.",
|
"Sign-in was cancelled.": "Вход отменён.",
|
||||||
|
|||||||
@@ -1212,6 +1212,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "Не вдалося надіслати сповіщення про прочитання: {error}",
|
"Could not send the receipt: {error}": "Не вдалося надіслати сповіщення про прочитання: {error}",
|
||||||
"Could not sign in.": "Не вдалося увійти.",
|
"Could not sign in.": "Не вдалося увійти.",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "Пароль вводиться на сторінці входу вашого поштового сервера.",
|
"You'll enter your password on your mail server's sign-in page.": "Пароль вводиться на сторінці входу вашого поштового сервера.",
|
||||||
|
"You'll sign in on your mail server's own page.": "Вхід виконується на сторінці вашого поштового сервера.",
|
||||||
"This sign-in didn't start in this browser. Try again.": "Цей вхід розпочато не в цьому браузері. Спробуйте ще раз.",
|
"This sign-in didn't start in this browser. Try again.": "Цей вхід розпочато не в цьому браузері. Спробуйте ще раз.",
|
||||||
"The sign-in took too long. Try again.": "Вхід тривав занадто довго. Спробуйте ще раз.",
|
"The sign-in took too long. Try again.": "Вхід тривав занадто довго. Спробуйте ще раз.",
|
||||||
"Sign-in was cancelled.": "Вхід скасовано.",
|
"Sign-in was cancelled.": "Вхід скасовано.",
|
||||||
|
|||||||
@@ -1223,6 +1223,7 @@ export const catalog: Catalog = {
|
|||||||
"Could not send the receipt: {error}": "无法发送已读回执:{error}",
|
"Could not send the receipt: {error}": "无法发送已读回执:{error}",
|
||||||
"Could not sign in.": "无法登录。",
|
"Could not sign in.": "无法登录。",
|
||||||
"You'll enter your password on your mail server's sign-in page.": "您将在邮件服务器的登录页面上输入密码。",
|
"You'll enter your password on your mail server's sign-in page.": "您将在邮件服务器的登录页面上输入密码。",
|
||||||
|
"You'll sign in on your mail server's own page.": "您将在邮件服务器自己的页面上登录。",
|
||||||
"This sign-in didn't start in this browser. Try again.": "此次登录不是在这个浏览器中发起的。请重试。",
|
"This sign-in didn't start in this browser. Try again.": "此次登录不是在这个浏览器中发起的。请重试。",
|
||||||
"The sign-in took too long. Try again.": "登录耗时过长。请重试。",
|
"The sign-in took too long. Try again.": "登录耗时过长。请重试。",
|
||||||
"Sign-in was cancelled.": "登录已取消。",
|
"Sign-in was cancelled.": "登录已取消。",
|
||||||
|
|||||||
+13
-4
@@ -33,6 +33,8 @@ export function LoginPage() {
|
|||||||
* and the password form if it never does.
|
* and the password form if it never does.
|
||||||
*/
|
*/
|
||||||
const [signIn, setSignIn] = useState<"oauth" | "password" | null>(null);
|
const [signIn, setSignIn] = useState<"oauth" | "password" | null>(null);
|
||||||
|
/* With "oauth" and one mail server, its page asks for the username: no address here. */
|
||||||
|
const [direct, setDirect] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let live = true;
|
let live = true;
|
||||||
fetch(withBase("/api/config"))
|
fetch(withBase("/api/config"))
|
||||||
@@ -42,6 +44,7 @@ export function LoginPage() {
|
|||||||
if (c.sourceUrl) setSourceUrl(c.sourceUrl as string);
|
if (c.sourceUrl) setSourceUrl(c.sourceUrl as string);
|
||||||
if (typeof c.appName === "string" && c.appName.trim()) setAppName(c.appName.trim());
|
if (typeof c.appName === "string" && c.appName.trim()) setAppName(c.appName.trim());
|
||||||
setSignIn(c.signIn === "oauth" ? "oauth" : "password");
|
setSignIn(c.signIn === "oauth" ? "oauth" : "password");
|
||||||
|
setDirect(c.signIn === "oauth" && c.signInDirect === true);
|
||||||
})
|
})
|
||||||
.catch(() => { /* the default stands */ })
|
.catch(() => { /* the default stands */ })
|
||||||
.finally(() => { if (live) setSignIn((m) => m ?? "password"); });
|
.finally(() => { if (live) setSignIn((m) => m ?? "password"); });
|
||||||
@@ -58,10 +61,10 @@ export function LoginPage() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (signIn === "oauth") {
|
if (signIn === "oauth") {
|
||||||
// Off to the mail server's page, which asks for the password there.
|
// Off to the mail server's page, which asks for the password there.
|
||||||
if (!username.trim()) return;
|
if (!direct && !username.trim()) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
if (trustDevice) localStorage.setItem("ihasmail:lastUser", username.trim());
|
if (trustDevice && !direct) localStorage.setItem("ihasmail:lastUser", username.trim());
|
||||||
const params = new URLSearchParams({ username: username.trim(), ...(trustDevice ? { remember: "1" } : {}) });
|
const params = new URLSearchParams({ ...(direct ? {} : { username: username.trim() }), ...(trustDevice ? { remember: "1" } : {}) });
|
||||||
window.location.assign(withBase(`/api/auth/oauth/start?${params}`));
|
window.location.assign(withBase(`/api/auth/oauth/start?${params}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,12 +103,18 @@ export function LoginPage() {
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!direct && (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="u">{t("Email or username")}</label>
|
<label htmlFor="u">{t("Email or username")}</label>
|
||||||
<input id="u" className="input" type="text" autoComplete="username" autoCapitalize="none" autoCorrect="off" spellCheck={false} value={username} onChange={(e) => setUsername(e.target.value)} autoFocus={!username} required />
|
<input id="u" className="input" type="text" autoComplete="username" autoCapitalize="none" autoCorrect="off" spellCheck={false} value={username} onChange={(e) => setUsername(e.target.value)} autoFocus={!username} required />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{signIn === "oauth" ? (
|
{signIn === "oauth" ? (
|
||||||
<p className="hint" style={{ marginBottom: 12 }}>{t("You'll enter your password on your mail server's sign-in page.")}</p>
|
<p className="hint" style={{ marginBottom: 12 }}>
|
||||||
|
{direct
|
||||||
|
? t("You'll sign in on your mail server's own page.")
|
||||||
|
: t("You'll enter your password on your mail server's sign-in page.")}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="p">{t("Password")}</label>
|
<label htmlFor="p">{t("Password")}</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user