mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-12 06:45:20 +00:00
Allow logging in via email
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
accountEmailVerifyUrl,
|
||||
accountEmailPrimaryUrl,
|
||||
accountEmailResendUrl,
|
||||
accountLoginUrl,
|
||||
accountPasswordResetRequestUrl,
|
||||
accountPasswordResetUrl,
|
||||
accountPasswordUrl,
|
||||
@@ -47,8 +48,8 @@ class AccountApi {
|
||||
}
|
||||
|
||||
async login(user) {
|
||||
const url = accountTokenUrl(config.base_url);
|
||||
console.log(`[AccountApi] Checking auth for ${url}`);
|
||||
const url = accountLoginUrl(config.base_url);
|
||||
console.log(`[AccountApi] Logging in at ${url}`);
|
||||
const response = await fetchOrThrow(url, {
|
||||
method: "POST",
|
||||
headers: withBasicAuth({}, user.username, user.password),
|
||||
@@ -57,7 +58,10 @@ class AccountApi {
|
||||
if (!json.token) {
|
||||
throw new Error(`Unexpected server response: Cannot find token`);
|
||||
}
|
||||
return json.token;
|
||||
// The identifier the user typed may be a primary email; login returns the canonical username
|
||||
// so callers can store it and show the real username rather than whatever was typed. Fall back
|
||||
// to the typed identifier if an older server omits the username, so the session still stores one.
|
||||
return { token: json.token, username: json.username || user.username };
|
||||
}
|
||||
|
||||
async logout() {
|
||||
|
||||
@@ -42,15 +42,19 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("AccountApi.login", () => {
|
||||
it("POSTs basic auth to the token URL and returns the token", async () => {
|
||||
fetchMock.mockResolvedValue(ok({ token: "tk_returned" }));
|
||||
const token = await accountApi.login({ username: "phil", password: "secret" });
|
||||
it("POSTs basic auth to the login URL and returns the token and canonical username", async () => {
|
||||
// The typed identifier is an email; the login endpoint returns the canonical username in one
|
||||
// request, so login() surfaces it (callers store it) rather than echoing what was typed.
|
||||
fetchMock.mockResolvedValue(ok({ token: "tk_returned", username: "phil" }));
|
||||
const result = await accountApi.login({ username: "phil@example.com", password: "secret" });
|
||||
|
||||
expect(result).toEqual({ token: "tk_returned", username: "phil" });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(token).toBe("tk_returned");
|
||||
const [url, options] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe("https://ntfy.sh/v1/account/token");
|
||||
expect(url).toBe("https://ntfy.sh/v1/account/login");
|
||||
expect(options.method).toBe("POST");
|
||||
expect(options.headers.Authorization).toBe(`Basic ${btoa("phil:secret")}`);
|
||||
expect(options.headers.Authorization).toBe(`Basic ${btoa("phil@example.com:secret")}`);
|
||||
});
|
||||
|
||||
it("throws when the server response has no token", async () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/aut
|
||||
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
||||
export const webPushUrl = (baseUrl) => `${baseUrl}/v1/webpush`;
|
||||
export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
|
||||
export const accountLoginUrl = (baseUrl) => `${baseUrl}/v1/account/login`;
|
||||
export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
|
||||
export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
|
||||
export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`;
|
||||
|
||||
@@ -24,14 +24,14 @@ const Login = () => {
|
||||
event.preventDefault();
|
||||
const user = { username, password };
|
||||
try {
|
||||
const token = await accountApi.login(user);
|
||||
console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
|
||||
await session.store(user.username, token);
|
||||
const { token, username: canonicalUsername } = await accountApi.login(user);
|
||||
console.log(`[Login] User auth for user ${user.username} successful, logged in as ${canonicalUsername}`);
|
||||
await session.store(canonicalUsername, token);
|
||||
fadeReload(routes.app);
|
||||
} catch (e) {
|
||||
console.log(`[Login] User auth for user ${user.username} failed`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
setError(t("Login failed: Invalid username or password"));
|
||||
setError(t("Login failed: Invalid username/email or password"));
|
||||
} else {
|
||||
setError(e.message);
|
||||
}
|
||||
@@ -53,7 +53,7 @@ const Login = () => {
|
||||
required
|
||||
fullWidth
|
||||
id="username"
|
||||
label={t("signup_form_username")}
|
||||
label={t("login_form_username_label")}
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={(ev) => setUsername(ev.target.value.trim())}
|
||||
|
||||
@@ -28,9 +28,9 @@ const Signup = () => {
|
||||
const user = { username, password };
|
||||
try {
|
||||
await accountApi.create(user.username, user.password, email);
|
||||
const token = await accountApi.login(user);
|
||||
console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`);
|
||||
await session.store(user.username, token);
|
||||
const { token, username: canonicalUsername } = await accountApi.login(user);
|
||||
console.log(`[Signup] User signup for user ${user.username} successful, logged in as ${canonicalUsername}`);
|
||||
await session.store(canonicalUsername, token);
|
||||
fadeReload(routes.app);
|
||||
} catch (e) {
|
||||
console.log(`[Signup] Signup for user ${user.username} failed`, e);
|
||||
|
||||
Reference in New Issue
Block a user