import { accountReservationSingleUrl, accountReservationUrl, accountPasswordUrl, accountSettingsUrl, accountSubscriptionSingleUrl, accountSubscriptionUrl, accountTokenUrl, accountUrl, maybeWithAuth, topicUrl, withBasicAuth, withBearerAuth, accountBillingSubscriptionUrl, accountBillingPortalUrl, tiersUrl } from "./utils"; import session from "./Session"; import subscriptionManager from "./SubscriptionManager"; import i18n from "i18next"; import prefs from "./Prefs"; import routes from "../components/routes"; import userManager from "./UserManager"; const delayMillis = 45000; // 45 seconds const intervalMillis = 900000; // 15 minutes class AccountApi { constructor() { this.timer = null; this.listener = null; // Fired when account is fetched from remote } registerListener(listener) { this.listener = listener; } resetListener() { this.listener = null; } async login(user) { const url = accountTokenUrl(config.base_url); console.log(`[AccountApi] Checking auth for ${url}`); const response = await fetch(url, { method: "POST", headers: withBasicAuth({}, user.username, user.password) }); 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; } async logout() { const url = accountTokenUrl(config.base_url); console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`); 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}`); } } async create(username, password) { const url = accountUrl(config.base_url); const body = JSON.stringify({ username: username, password: password }); console.log(`[AccountApi] Creating user account ${url}`); 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() { const url = accountUrl(config.base_url); console.log(`[AccountApi] Fetching user account ${url}`); const response = await fetch(url, { 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}`); } const account = await response.json(); console.log(`[AccountApi] Account`, account); if (this.listener) { this.listener(account); } return account; } async delete() { const url = accountUrl(config.base_url); console.log(`[AccountApi] Deleting user account ${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}`); } } async changePassword(newPassword) { const url = accountPasswordUrl(config.base_url); console.log(`[AccountApi] Changing account password ${url}`); const response = await fetch(url, { method: "POST", headers: withBearerAuth({}, session.token()), 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() { const url = accountTokenUrl(config.base_url); console.log(`[AccountApi] Extending user access token ${url}`); const response = await fetch(url, { method: "PATCH", 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}`); } } async updateSettings(payload) { const url = accountSettingsUrl(config.base_url); const body = JSON.stringify(payload); console.log(`[AccountApi] Updating user account ${url}: ${body}`); const response = await fetch(url, { method: "PATCH", headers: withBearerAuth({}, session.token()), 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}`); } } async addSubscription(payload) { const url = accountSubscriptionUrl(config.base_url); const body = JSON.stringify(payload); console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); const response = await fetch(url, { method: "POST", headers: withBearerAuth({}, session.token()), 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}`); } const subscription = await response.json(); console.log(`[AccountApi] Subscription`, subscription); return subscription; } async updateSubscription(remoteId, payload) { const url = accountSubscriptionSingleUrl(config.base_url, remoteId); const body = JSON.stringify(payload); console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); const response = await fetch(url, { method: "PATCH", headers: withBearerAuth({}, session.token()), 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}`); } const subscription = await response.json(); console.log(`[AccountApi] Subscription`, subscription); return subscription; } async deleteSubscription(remoteId) { const url = accountSubscriptionSingleUrl(config.base_url, remoteId); console.log(`[AccountApi] Removing user subscription ${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}`); } } async upsertReservation(topic, everyone) { const url = accountReservationUrl(config.base_url); 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(); } else if (response.status === 409) { throw new TopicReservedError(); } else if (response.status !== 200) { throw new Error(`Unexpected server response ${response.status}`); } } async deleteReservation(topic) { const url = accountReservationSingleUrl(config.base_url, topic); 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}`); } } async billingTiers() { const url = tiersUrl(config.base_url); console.log(`[AccountApi] Fetching billing tiers`); const response = await fetch(url); // No auth needed! if (response.status !== 200) { throw new Error(`Unexpected server response ${response.status}`); } return await response.json(); } async createBillingSubscription(tier) { console.log(`[AccountApi] Creating billing subscription with ${tier}`); return await this.upsertBillingSubscription("POST", tier) } async updateBillingSubscription(tier) { console.log(`[AccountApi] Updating billing subscription with ${tier}`); return await this.upsertBillingSubscription("PUT", tier) } async upsertBillingSubscription(method, tier) { const url = accountBillingSubscriptionUrl(config.base_url); const response = await fetch(url, { method: method, headers: withBearerAuth({}, session.token()), body: JSON.stringify({ tier: tier }) }); if (response.status === 401 || response.status === 403) { throw new UnauthorizedError(); } else if (response.status !== 200) { throw new Error(`Unexpected server response ${response.status}`); } return await response.json(); } async deleteBillingSubscription() { const url = accountBillingSubscriptionUrl(config.base_url); console.log(`[AccountApi] Cancelling billing subscription`); 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}`); } } async createBillingPortalSession() { const url = accountBillingPortalUrl(config.base_url); console.log(`[AccountApi] Creating billing portal session`); const response = await fetch(url, { method: "POST", 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}`); } return await response.json(); } async sync() { try { if (!session.token()) { return null; } console.log(`[AccountApi] Syncing account`); const account = await this.get(); if (account.language) { await i18n.changeLanguage(account.language); } if (account.notification) { if (account.notification.sound) { await prefs.setSound(account.notification.sound); } if (account.notification.delete_after) { await prefs.setDeleteAfter(account.notification.delete_after); } if (account.notification.min_priority) { await prefs.setMinPriority(account.notification.min_priority); } } if (account.subscriptions) { await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations); } return account; } catch (e) { console.log(`[AccountApi] Error fetching account`, e); if ((e instanceof UnauthorizedError)) { session.resetAndRedirect(routes.login); } } } 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); } } } export class UsernameTakenError extends Error { constructor(username) { super("Username taken"); this.username = username; } } export class TopicReservedError extends Error { constructor(topic) { super("Topic already reserved"); this.topic = topic; } } 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;