ntfy/web/src/app/AccountApi.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

436 lines
12 KiB
JavaScript
Raw Normal View History

2023-01-03 16:21:11 +13:00
import i18n from "i18next";
2022-12-26 05:59:44 +13:00
import {
2023-01-31 07:10:45 +13:00
accountBillingPortalUrl,
accountBillingSubscriptionUrl,
2023-05-18 02:39:15 +12:00
accountPasswordUrl,
accountPhoneUrl,
accountPhoneVerifyUrl,
2023-01-13 04:50:09 +13:00
accountReservationSingleUrl,
accountReservationUrl,
2022-12-26 05:59:44 +13:00
accountSettingsUrl,
accountSubscriptionUrl,
accountTokenUrl,
2023-05-18 02:39:15 +12:00
accountUrl,
maybeWithBearerAuth,
2023-01-31 07:10:45 +13:00
tiersUrl,
withBasicAuth,
2023-01-31 07:10:45 +13:00
withBearerAuth,
2022-12-26 05:59:44 +13:00
} from "./utils";
import session from "./Session";
2022-12-26 07:42:44 +13:00
import subscriptionManager from "./SubscriptionManager";
2023-01-03 16:21:11 +13:00
import prefs from "./Prefs";
import routes from "../components/routes";
2023-05-18 02:39:15 +12:00
import { fetchOrThrow, UnauthorizedError } from "./errors";
2022-12-26 07:42:44 +13:00
const delayMillis = 45000; // 45 seconds
const intervalMillis = 900000; // 15 minutes
2022-12-26 05:59:44 +13:00
class AccountApi {
2022-12-26 07:42:44 +13:00
constructor() {
this.timer = null;
2023-01-03 16:21:11 +13:00
this.listener = null; // Fired when account is fetched from remote
2023-02-10 09:24:12 +13:00
this.tiers = null; // Cached
2023-05-24 07:13:01 +12:00
}
2023-01-03 16:21:11 +13:00
registerListener(listener) {
this.listener = listener;
2023-05-24 07:13:01 +12:00
}
2023-01-03 16:21:11 +13:00
resetListener() {
this.listener = null;
2023-05-24 07:13:01 +12:00
}
2022-12-26 05:59:44 +13:00
async login(user) {
2023-01-05 16:47:12 +13:00
const url = accountTokenUrl(config.base_url);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Checking auth for ${url}`);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "POST",
headers: withBasicAuth({}, user.username, user.password),
2023-05-24 07:13:01 +12:00
});
2023-02-03 09:19:37 +13:00
const json = await response.json(); // May throw SyntaxError
2022-12-26 05:59:44 +13:00
if (!json.token) {
throw new Error(`Unexpected server response: Cannot find token`);
2023-01-03 16:21:11 +13:00
}
return json.token;
2022-12-26 07:42:44 +13:00
}
2023-05-24 07:13:01 +12:00
2022-12-26 05:59:44 +13:00
async logout() {
2023-01-05 16:47:12 +13:00
const url = accountTokenUrl(config.base_url);
2022-12-29 09:51:09 +13:00
console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
2022-12-26 07:42:44 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "DELETE",
2022-12-26 07:42:44 +13:00
headers: withBearerAuth({}, session.token()),
2023-05-24 07:13:01 +12:00
});
}
2022-12-26 05:59:44 +13:00
async create(username, password) {
2023-01-05 16:47:12 +13:00
const url = accountUrl(config.base_url);
2022-12-26 05:59:44 +13:00
const body = JSON.stringify({
username,
password,
2023-05-24 07:13:01 +12:00
});
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Creating user account ${url}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "POST",
body,
2023-05-24 07:13:01 +12:00
});
}
2022-12-26 05:59:44 +13:00
async get() {
2023-01-05 16:47:12 +13:00
const url = accountUrl(config.base_url);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Fetching user account ${url}`);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url, {
headers: maybeWithBearerAuth({}, session.token()), // GET /v1/account endpoint can be called by anonymous
2022-12-26 05:59:44 +13:00
});
2023-02-03 09:19:37 +13:00
const account = await response.json(); // May throw SyntaxError
2022-12-26 05:59:44 +13:00
console.log(`[AccountApi] Account`, account);
if (this.listener) {
this.listener(account);
}
2022-12-29 09:51:09 +13:00
return account;
2023-05-24 07:13:01 +12:00
}
2022-12-29 09:51:09 +13:00
async delete(password) {
2023-01-05 16:47:12 +13:00
const url = accountUrl(config.base_url);
2022-12-29 09:51:09 +13:00
console.log(`[AccountApi] Deleting user account ${url}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "DELETE",
2022-12-29 09:51:09 +13:00
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
password,
2023-05-24 07:13:01 +12:00
}),
});
}
async changePassword(currentPassword, newPassword) {
2023-01-05 16:47:12 +13:00
const url = accountPasswordUrl(config.base_url);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Changing account password ${url}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "POST",
headers: withBearerAuth({}, session.token()),
2022-12-26 05:59:44 +13:00
body: JSON.stringify({
password: currentPassword,
new_password: newPassword,
2023-05-24 07:13:01 +12:00
}),
});
}
2023-01-28 17:10:59 +13:00
async createToken(label, expires) {
2023-01-05 16:47:12 +13:00
const url = accountTokenUrl(config.base_url);
2022-12-29 09:51:09 +13:00
const body = {
label,
expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0,
2023-05-24 07:13:01 +12:00
};
2022-12-29 09:51:09 +13:00
console.log(`[AccountApi] Creating user access token ${url}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "POST",
2022-12-29 09:51:09 +13:00
headers: withBearerAuth({}, session.token()),
2023-01-28 17:10:59 +13:00
body: JSON.stringify(body),
2022-12-26 05:59:44 +13:00
});
2023-05-24 07:13:01 +12:00
}
2023-01-28 17:10:59 +13:00
async updateToken(token, label, expires) {
const url = accountTokenUrl(config.base_url);
const body = {
token,
label,
2023-05-24 07:13:01 +12:00
};
2023-01-28 17:10:59 +13:00
if (expires > 0) {
body.expires = Math.floor(Date.now() / 1000) + expires;
2022-12-26 05:59:44 +13:00
}
console.log(`[AccountApi] Creating user access token ${url}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "PATCH",
2023-01-28 17:10:59 +13:00
headers: withBearerAuth({}, session.token()),
body: JSON.stringify(body),
2023-05-24 07:13:01 +12:00
});
}
2022-12-26 05:59:44 +13:00
async extendToken() {
2023-01-05 16:47:12 +13:00
const url = accountTokenUrl(config.base_url);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Extending user access token ${url}`);
2022-12-26 05:59:44 +13:00
await fetchOrThrow(url, {
method: "PATCH",
2023-01-28 17:10:59 +13:00
headers: withBearerAuth({}, session.token()),
2023-05-24 07:13:01 +12:00
});
}
2022-12-26 05:59:44 +13:00
async deleteToken(token) {
2023-01-05 16:47:12 +13:00
const url = accountTokenUrl(config.base_url);
2023-01-28 17:10:59 +13:00
console.log(`[AccountApi] Deleting user access token ${url}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2023-01-28 17:10:59 +13:00
method: "DELETE",
2022-12-26 05:59:44 +13:00
headers: withBearerAuth({ "X-Token": token }, session.token()),
2023-05-24 07:13:01 +12:00
});
}
2022-12-26 05:59:44 +13:00
async updateSettings(payload) {
2023-01-05 16:47:12 +13:00
const url = accountSettingsUrl(config.base_url);
2022-12-26 05:59:44 +13:00
const body = JSON.stringify(payload);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Updating user account ${url}: ${body}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "PATCH",
headers: withBearerAuth({}, session.token()),
2022-12-26 05:59:44 +13:00
body,
2023-05-24 07:13:01 +12:00
});
}
2023-02-13 08:09:44 +13:00
async addSubscription(baseUrl, topic) {
2023-01-05 16:47:12 +13:00
const url = accountSubscriptionUrl(config.base_url);
2023-02-13 08:09:44 +13:00
const body = JSON.stringify({
base_url: baseUrl,
topic,
2023-05-24 07:13:01 +12:00
});
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "POST",
headers: withBearerAuth({}, session.token()),
2022-12-26 05:59:44 +13:00
body,
2023-05-24 07:13:01 +12:00
});
2023-02-03 09:19:37 +13:00
const subscription = await response.json(); // May throw SyntaxError
2022-12-26 16:29:55 +13:00
console.log(`[AccountApi] Subscription`, subscription);
return subscription;
2023-05-24 07:13:01 +12:00
}
2023-02-13 08:09:44 +13:00
async updateSubscription(baseUrl, topic, payload) {
const url = accountSubscriptionUrl(config.base_url);
const body = JSON.stringify({
base_url: baseUrl,
topic,
...payload,
2023-05-24 07:13:01 +12:00
});
2022-12-26 16:29:55 +13:00
console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url, {
2022-12-26 16:29:55 +13:00
method: "PATCH",
2022-12-26 07:42:44 +13:00
headers: withBearerAuth({}, session.token()),
2022-12-26 16:29:55 +13:00
body,
2023-05-24 07:13:01 +12:00
});
2022-12-26 07:42:44 +13:00
const subscription = await response.json(); // May throw SyntaxError
console.log(`[AccountApi] Subscription`, subscription);
2022-12-26 05:59:44 +13:00
return subscription;
2023-05-24 07:13:01 +12:00
}
2023-02-13 08:09:44 +13:00
async deleteSubscription(baseUrl, topic) {
const url = accountSubscriptionUrl(config.base_url);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Removing user subscription ${url}`);
2023-02-13 08:09:44 +13:00
const headers = {
"X-BaseURL": baseUrl,
"X-Topic": topic,
2023-05-24 07:13:01 +12:00
};
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "DELETE",
2023-02-13 08:09:44 +13:00
headers: withBearerAuth(headers, session.token()),
2023-05-24 07:13:01 +12:00
});
}
2023-01-15 00:43:44 +13:00
async upsertReservation(topic, everyone) {
2023-01-13 04:50:09 +13:00
const url = accountReservationUrl(config.base_url);
2023-01-03 15:52:20 +13:00
console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "POST",
2023-01-03 15:52:20 +13:00
headers: withBearerAuth({}, session.token()),
2022-12-26 05:59:44 +13:00
body: JSON.stringify({
2023-01-03 15:52:20 +13:00
topic,
everyone,
2023-05-24 07:13:01 +12:00
}),
});
}
2023-02-01 15:39:30 +13:00
async deleteReservation(topic, deleteMessages) {
2023-01-13 04:50:09 +13:00
const url = accountReservationSingleUrl(config.base_url, topic);
2023-01-03 15:52:20 +13:00
console.log(`[AccountApi] Removing topic reservation ${url}`);
2023-02-01 15:39:30 +13:00
const headers = {
"X-Delete-Messages": deleteMessages ? "true" : "false",
2023-05-24 07:13:01 +12:00
};
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2022-12-26 05:59:44 +13:00
method: "DELETE",
2023-02-13 08:09:44 +13:00
headers: withBearerAuth(headers, session.token()),
2022-12-26 05:59:44 +13:00
});
2023-05-24 07:13:01 +12:00
}
2023-01-18 04:09:37 +13:00
async billingTiers() {
2023-02-10 09:24:12 +13:00
if (this.tiers) {
return this.tiers;
2022-12-26 05:59:44 +13:00
}
2023-01-05 16:47:12 +13:00
const url = tiersUrl(config.base_url);
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Fetching billing tiers`);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url); // No auth needed!
2022-12-26 07:42:44 +13:00
this.tiers = await response.json(); // May throw SyntaxError
2023-02-10 09:24:12 +13:00
return this.tiers;
2023-05-24 07:13:01 +12:00
}
2023-02-22 16:44:30 +13:00
async createBillingSubscription(tier, interval) {
console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
return this.upsertBillingSubscription("POST", tier, interval);
2023-05-24 07:13:01 +12:00
}
2023-02-22 16:44:30 +13:00
async updateBillingSubscription(tier, interval) {
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
return this.upsertBillingSubscription("PUT", tier, interval);
2023-05-24 07:13:01 +12:00
}
2023-02-22 16:44:30 +13:00
async upsertBillingSubscription(method, tier, interval) {
2023-01-16 17:29:46 +13:00
const url = accountBillingSubscriptionUrl(config.base_url);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url, {
method,
2023-01-15 00:43:44 +13:00
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
2023-02-22 16:44:30 +13:00
tier,
interval,
2023-05-24 07:13:01 +12:00
}),
});
return response.json(); // May throw SyntaxError
2023-05-24 07:13:01 +12:00
}
2023-01-16 17:29:46 +13:00
async deleteBillingSubscription() {
2022-12-26 07:42:44 +13:00
const url = accountBillingSubscriptionUrl(config.base_url);
console.log(`[AccountApi] Cancelling billing subscription`);
2023-02-03 09:19:37 +13:00
await fetchOrThrow(url, {
2023-01-16 17:29:46 +13:00
method: "DELETE",
2023-05-13 13:47:41 +12:00
headers: withBearerAuth({}, session.token()),
2023-05-24 07:13:01 +12:00
});
}
2023-01-15 00:43:44 +13:00
async createBillingPortalSession() {
const url = accountBillingPortalUrl(config.base_url);
console.log(`[AccountApi] Creating billing portal session`);
2023-02-03 09:19:37 +13:00
const response = await fetchOrThrow(url, {
2023-02-12 08:13:10 +13:00
method: "POST",
headers: withBearerAuth({}, session.token()),
2022-12-26 05:59:44 +13:00
});
return response.json(); // May throw SyntaxError
2023-05-24 07:13:01 +12:00
}
2023-05-17 14:27:48 +12:00
async verifyPhoneNumber(phoneNumber, channel) {
2023-02-03 09:19:37 +13:00
const url = accountPhoneVerifyUrl(config.base_url);
2023-05-13 13:47:41 +12:00
console.log(`[AccountApi] Sending phone verification ${url}`);
await fetchOrThrow(url, {
2023-05-17 06:15:58 +12:00
method: "PUT",
2023-05-13 13:47:41 +12:00
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
number: phoneNumber,
2023-05-17 14:27:48 +12:00
channel,
2023-05-24 07:13:01 +12:00
}),
});
}
2023-05-17 06:15:58 +12:00
async addPhoneNumber(phoneNumber, code) {
2023-02-03 09:19:37 +13:00
const url = accountPhoneUrl(config.base_url);
2023-05-17 06:15:58 +12:00
console.log(`[AccountApi] Adding phone number with verification code ${url}`);
2023-05-13 13:47:41 +12:00
await fetchOrThrow(url, {
2023-05-17 06:15:58 +12:00
method: "PUT",
2023-05-13 13:47:41 +12:00
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
number: phoneNumber,
code,
2022-12-26 07:42:44 +13:00
}),
2023-05-24 07:13:01 +12:00
});
}
async deletePhoneNumber(phoneNumber) {
2022-12-26 07:42:44 +13:00
const url = accountPhoneUrl(config.base_url);
2023-05-13 13:47:41 +12:00
console.log(`[AccountApi] Deleting phone number ${url}`);
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
number: phoneNumber,
2023-05-24 07:13:01 +12:00
}),
});
}
2023-01-03 16:21:11 +13:00
async sync() {
2023-05-24 07:13:01 +12:00
try {
2023-01-03 16:21:11 +13:00
if (!session.token()) {
return null;
2023-05-24 07:13:01 +12:00
}
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Syncing account`);
2023-01-03 16:21:11 +13:00
const account = await this.get();
if (account.language) {
2023-01-04 05:28:04 +13:00
await i18n.changeLanguage(account.language);
2023-05-24 07:13:01 +12:00
}
2023-01-04 05:28:04 +13:00
if (account.notification) {
if (account.notification.sound) {
await prefs.setSound(account.notification.sound);
2023-01-03 16:21:11 +13:00
}
2022-12-26 07:42:44 +13:00
if (account.notification.delete_after) {
2023-01-28 17:10:59 +13:00
await prefs.setDeleteAfter(account.notification.delete_after);
}
2023-01-05 16:47:12 +13:00
if (account.notification.min_priority) {
2023-02-03 09:19:37 +13:00
await prefs.setMinPriority(account.notification.min_priority);
2023-02-13 08:09:44 +13:00
}
2023-05-24 07:13:01 +12:00
}
2023-02-03 09:19:37 +13:00
if (account.subscriptions) {
await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations);
2023-05-24 07:13:01 +12:00
}
2023-02-03 09:19:37 +13:00
return account;
2022-12-26 07:42:44 +13:00
} catch (e) {
2023-02-03 09:19:37 +13:00
console.log(`[AccountApi] Error fetching account`, e);
2023-02-13 08:09:44 +13:00
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
2022-12-26 05:59:44 +13:00
}
return undefined;
2022-12-26 05:59:44 +13:00
}
2023-05-24 07:13:01 +12:00
}
2022-12-26 07:42:44 +13:00
2023-01-15 00:43:44 +13:00
startWorker() {
if (this.timer !== null) {
2023-01-13 04:50:09 +13:00
return;
2023-01-03 15:52:20 +13:00
}
console.log(`[AccountApi] Starting worker`);
2023-02-01 15:39:30 +13:00
this.timer = setInterval(() => this.runWorker(), intervalMillis);
2023-02-10 09:24:12 +13:00
setTimeout(() => this.runWorker(), delayMillis);
2023-01-18 04:09:37 +13:00
}
2023-05-24 07:13:01 +12:00
stopWorker() {
clearTimeout(this.timer);
}
2023-02-22 16:44:30 +13:00
async runWorker() {
if (!session.token()) {
return;
}
console.log(`[AccountApi] Extending user access token`);
2023-05-13 13:47:41 +12:00
try {
await this.extendToken();
2023-01-03 16:21:11 +13:00
} catch (e) {
2022-12-26 07:42:44 +13:00
console.log(`[AccountApi] Error extending user access token`, e);
}
2023-05-24 07:13:01 +12:00
}
2022-12-26 05:59:44 +13:00
}
2023-01-31 07:10:45 +13:00
// Maps to user.Role in user/types.go
export const Role = {
ADMIN: "admin",
USER: "user",
};
// Maps to server.visitorLimitBasis in server/visitor.go
export const LimitBasis = {
IP: "ip",
TIER: "tier",
};
// Maps to stripe.SubscriptionStatus
export const SubscriptionStatus = {
ACTIVE: "active",
PAST_DUE: "past_due",
};
2023-02-22 16:44:30 +13:00
// Maps to stripe.PriceRecurringInterval
export const SubscriptionInterval = {
MONTH: "month",
YEAR: "year",
};
2023-01-31 07:10:45 +13:00
// Maps to user.Permission in user/types.go
export const Permission = {
READ_WRITE: "read-write",
READ_ONLY: "read-only",
WRITE_ONLY: "write-only",
DENY_ALL: "deny-all",
};
2022-12-26 05:59:44 +13:00
const accountApi = new AccountApi();
export default accountApi;