ntfy/web/src/app/AccountApi.js

378 lines
13 KiB
JavaScript
Raw Normal View History

2022-12-25 17:59:44 +01:00
import {
2023-01-12 16:50:09 +01:00
accountReservationSingleUrl,
accountReservationUrl,
2022-12-25 17:59:44 +01:00
accountPasswordUrl,
accountSettingsUrl,
accountSubscriptionSingleUrl,
accountSubscriptionUrl,
accountTokenUrl,
2023-01-12 03:38:10 +01:00
accountUrl, maybeWithAuth, topicUrl,
withBasicAuth,
2023-01-03 04:21:11 +01:00
withBearerAuth
2022-12-25 17:59:44 +01:00
} from "./utils";
import session from "./Session";
2022-12-25 19:42:44 +01:00
import subscriptionManager from "./SubscriptionManager";
2023-01-03 04:21:11 +01:00
import i18n from "i18next";
import prefs from "./Prefs";
import routes from "../components/routes";
2023-01-12 03:38:10 +01:00
import userManager from "./UserManager";
2022-12-25 19:42:44 +01:00
const delayMillis = 45000; // 45 seconds
const intervalMillis = 900000; // 15 minutes
2022-12-25 17:59:44 +01:00
class AccountApi {
2022-12-25 19:42:44 +01:00
constructor() {
this.timer = null;
2023-01-03 04:21:11 +01:00
this.listener = null; // Fired when account is fetched from remote
2023-01-12 03:38:10 +01:00
// Random ID used to identify this client when sending/receiving "sync" events
// to the sync topic of an account. This ID doesn't matter much, but it will prevent
// a client from reacting to its own message.
this.identity = Math.floor(Math.random() * 2586000);
2023-01-03 04:21:11 +01:00
}
registerListener(listener) {
this.listener = listener;
}
resetListener() {
this.listener = null;
2022-12-25 19:42:44 +01:00
}
2022-12-25 17:59:44 +01:00
async login(user) {
2023-01-05 04:47:12 +01:00
const url = accountTokenUrl(config.base_url);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Checking auth for ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "POST",
headers: withBasicAuth({}, user.username, user.password)
2022-12-25 17:59:44 +01:00
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const json = await response.json();
if (!json.token) {
throw new Error(`Unexpected server response: Cannot find token`);
}
return json.token;
}
2022-12-28 21:51:09 +01:00
async logout() {
2023-01-05 04:47:12 +01:00
const url = accountTokenUrl(config.base_url);
2022-12-28 21:51:09 +01:00
console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "DELETE",
2022-12-28 21:51:09 +01:00
headers: withBearerAuth({}, session.token())
2022-12-25 17:59:44 +01:00
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async create(username, password) {
2023-01-05 04:47:12 +01:00
const url = accountUrl(config.base_url);
2022-12-25 17:59:44 +01:00
const body = JSON.stringify({
username: username,
password: password
});
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Creating user account ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "POST",
body: body
});
if (response.status === 409) {
throw new UsernameTakenError(username);
} else if (response.status === 429) {
throw new AccountCreateLimitReachedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async get() {
2023-01-05 04:47:12 +01:00
const url = accountUrl(config.base_url);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Fetching user account ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
headers: withBearerAuth({}, session.token())
2022-12-25 17:59:44 +01:00
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const account = await response.json();
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Account`, account);
2023-01-03 04:21:11 +01:00
if (this.listener) {
this.listener(account);
}
2022-12-25 17:59:44 +01:00
return account;
}
async delete() {
2023-01-05 04:47:12 +01:00
const url = accountUrl(config.base_url);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Deleting user account ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token())
2022-12-25 17:59:44 +01:00
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async changePassword(newPassword) {
2023-01-05 04:47:12 +01:00
const url = accountPasswordUrl(config.base_url);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Changing account password ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
2022-12-25 17:59:44 +01:00
body: JSON.stringify({
password: newPassword
})
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async extendToken() {
2023-01-05 04:47:12 +01:00
const url = accountTokenUrl(config.base_url);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Extending user access token ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token())
2022-12-25 17:59:44 +01:00
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async updateSettings(payload) {
2023-01-05 04:47:12 +01:00
const url = accountSettingsUrl(config.base_url);
2022-12-25 17:59:44 +01:00
const body = JSON.stringify(payload);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Updating user account ${url}: ${body}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token()),
2022-12-25 17:59:44 +01:00
body: body
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
2023-01-12 03:38:10 +01:00
this.triggerChange(); // Dangle!
2022-12-25 17:59:44 +01:00
}
async addSubscription(payload) {
2023-01-05 04:47:12 +01:00
const url = accountSubscriptionUrl(config.base_url);
2022-12-25 17:59:44 +01:00
const body = JSON.stringify(payload);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
2022-12-25 17:59:44 +01:00
body: body
});
2022-12-26 04:29:55 +01:00
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const subscription = await response.json();
console.log(`[AccountApi] Subscription`, subscription);
2023-01-12 03:38:10 +01:00
this.triggerChange(); // Dangle!
2022-12-26 04:29:55 +01:00
return subscription;
}
async updateSubscription(remoteId, payload) {
2023-01-05 04:47:12 +01:00
const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
2022-12-26 04:29:55 +01:00
const body = JSON.stringify(payload);
console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
const response = await fetch(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token()),
2022-12-26 04:29:55 +01:00
body: body
});
2022-12-25 17:59:44 +01:00
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const subscription = await response.json();
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Subscription`, subscription);
2023-01-12 03:38:10 +01:00
this.triggerChange(); // Dangle!
2022-12-25 17:59:44 +01:00
return subscription;
}
async deleteSubscription(remoteId) {
2023-01-05 04:47:12 +01:00
const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
2022-12-25 19:42:44 +01:00
console.log(`[AccountApi] Removing user subscription ${url}`);
2022-12-25 17:59:44 +01:00
const response = await fetch(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token())
2022-12-25 17:59:44 +01:00
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
2023-01-12 03:38:10 +01:00
this.triggerChange(); // Dangle!
2022-12-25 17:59:44 +01:00
}
2022-12-25 19:42:44 +01:00
2023-01-03 03:52:20 +01:00
async upsertAccess(topic, everyone) {
2023-01-12 16:50:09 +01:00
const url = accountReservationUrl(config.base_url);
2023-01-03 03:52:20 +01:00
console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
const response = await fetch(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
topic: topic,
everyone: everyone
})
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
2023-01-05 02:34:22 +01:00
} else if (response.status === 409) {
throw new TopicReservedError();
2023-01-03 03:52:20 +01:00
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
2023-01-12 03:38:10 +01:00
this.triggerChange(); // Dangle!
2023-01-03 03:52:20 +01:00
}
async deleteAccess(topic) {
2023-01-12 16:50:09 +01:00
const url = accountReservationSingleUrl(config.base_url, topic);
2023-01-03 03:52:20 +01:00
console.log(`[AccountApi] Removing topic reservation ${url}`);
const response = await fetch(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
2023-01-12 03:38:10 +01:00
this.triggerChange(); // Dangle!
2023-01-03 03:52:20 +01:00
}
2023-01-03 04:21:11 +01:00
async sync() {
try {
if (!session.token()) {
return null;
}
console.log(`[AccountApi] Syncing account`);
2023-01-03 17:28:04 +01:00
const account = await this.get();
if (account.language) {
await i18n.changeLanguage(account.language);
2023-01-03 04:21:11 +01:00
}
2023-01-03 17:28:04 +01:00
if (account.notification) {
if (account.notification.sound) {
await prefs.setSound(account.notification.sound);
2023-01-03 04:21:11 +01:00
}
2023-01-03 17:28:04 +01:00
if (account.notification.delete_after) {
await prefs.setDeleteAfter(account.notification.delete_after);
2023-01-03 04:21:11 +01:00
}
2023-01-03 17:28:04 +01:00
if (account.notification.min_priority) {
await prefs.setMinPriority(account.notification.min_priority);
2023-01-03 04:21:11 +01:00
}
}
2023-01-03 17:28:04 +01:00
if (account.subscriptions) {
await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations);
2023-01-03 04:21:11 +01:00
}
2023-01-03 17:28:04 +01:00
return account;
2023-01-03 04:21:11 +01:00
} catch (e) {
console.log(`[AccountApi] Error fetching account`, e);
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
}
}
}
2023-01-12 03:38:10 +01:00
async triggerChange() {
const account = await this.get();
if (!account.sync_topic) {
return;
}
const url = topicUrl(config.base_url, account.sync_topic);
console.log(`[AccountApi] Triggering account change to ${url}`);
const user = await userManager.get(config.base_url);
const headers = {
Cache: "no" // We really don't need to store this!
};
try {
const response = await fetch(url, {
method: 'PUT',
body: JSON.stringify({
event: "sync",
source: this.identity
}),
headers: maybeWithAuth(headers, user)
});
if (response.status < 200 || response.status > 299) {
throw new Error(`Unexpected response: ${response.status}`);
}
} catch (e) {
console.log(`[AccountApi] Publishing to sync topic failed`, e);
}
}
2022-12-25 19:42:44 +01:00
startWorker() {
if (this.timer !== null) {
return;
}
console.log(`[AccountApi] Starting worker`);
this.timer = setInterval(() => this.runWorker(), intervalMillis);
setTimeout(() => this.runWorker(), delayMillis);
}
async runWorker() {
if (!session.token()) {
return;
}
console.log(`[AccountApi] Extending user access token`);
try {
await this.extendToken();
} catch (e) {
console.log(`[AccountApi] Error extending user access token`, e);
}
}
2022-12-25 17:59:44 +01:00
}
export class UsernameTakenError extends Error {
constructor(username) {
super("Username taken");
this.username = username;
}
}
2023-01-05 02:34:22 +01:00
export class TopicReservedError extends Error {
constructor(topic) {
super("Topic already reserved");
this.topic = topic;
}
}
2022-12-25 17:59:44 +01:00
export class AccountCreateLimitReachedError extends Error {
constructor() {
super("Account creation limit reached");
}
}
export class UnauthorizedError extends Error {
constructor() {
super("Unauthorized");
}
}
const accountApi = new AccountApi();
export default accountApi;