AccountApi.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import {
  2. accountBillingPortalUrl,
  3. accountBillingSubscriptionUrl,
  4. accountPasswordUrl,
  5. accountReservationSingleUrl,
  6. accountReservationUrl,
  7. accountSettingsUrl,
  8. accountSubscriptionSingleUrl,
  9. accountSubscriptionUrl,
  10. accountTokenUrl,
  11. accountUrl,
  12. tiersUrl,
  13. withBasicAuth,
  14. withBearerAuth
  15. } from "./utils";
  16. import session from "./Session";
  17. import subscriptionManager from "./SubscriptionManager";
  18. import i18n from "i18next";
  19. import prefs from "./Prefs";
  20. import routes from "../components/routes";
  21. import {fetchOrThrow, throwAppError, UnauthorizedError} from "./errors";
  22. const delayMillis = 45000; // 45 seconds
  23. const intervalMillis = 900000; // 15 minutes
  24. class AccountApi {
  25. constructor() {
  26. this.timer = null;
  27. this.listener = null; // Fired when account is fetched from remote
  28. }
  29. registerListener(listener) {
  30. this.listener = listener;
  31. }
  32. resetListener() {
  33. this.listener = null;
  34. }
  35. async login(user) {
  36. const url = accountTokenUrl(config.base_url);
  37. console.log(`[AccountApi] Checking auth for ${url}`);
  38. const response = await fetchOrThrow(url, {
  39. method: "POST",
  40. headers: withBasicAuth({}, user.username, user.password)
  41. });
  42. const json = await response.json(); // May throw SyntaxError
  43. if (!json.token) {
  44. throw new Error(`Unexpected server response: Cannot find token`);
  45. }
  46. return json.token;
  47. }
  48. async logout() {
  49. const url = accountTokenUrl(config.base_url);
  50. console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
  51. await fetchOrThrow(url, {
  52. method: "DELETE",
  53. headers: withBearerAuth({}, session.token())
  54. });
  55. }
  56. async create(username, password) {
  57. const url = accountUrl(config.base_url);
  58. const body = JSON.stringify({
  59. username: username,
  60. password: password
  61. });
  62. console.log(`[AccountApi] Creating user account ${url}`);
  63. await fetchOrThrow(url, {
  64. method: "POST",
  65. body: body
  66. });
  67. }
  68. async get() {
  69. const url = accountUrl(config.base_url);
  70. console.log(`[AccountApi] Fetching user account ${url}`);
  71. const response = await fetchOrThrow(url, {
  72. headers: withBearerAuth({}, session.token())
  73. });
  74. const account = await response.json(); // May throw SyntaxError
  75. console.log(`[AccountApi] Account`, account);
  76. if (this.listener) {
  77. this.listener(account);
  78. }
  79. return account;
  80. }
  81. async delete(password) {
  82. const url = accountUrl(config.base_url);
  83. console.log(`[AccountApi] Deleting user account ${url}`);
  84. await fetchOrThrow(url, {
  85. method: "DELETE",
  86. headers: withBearerAuth({}, session.token()),
  87. body: JSON.stringify({
  88. password: password
  89. })
  90. });
  91. }
  92. async changePassword(currentPassword, newPassword) {
  93. const url = accountPasswordUrl(config.base_url);
  94. console.log(`[AccountApi] Changing account password ${url}`);
  95. await fetchOrThrow(url, {
  96. method: "POST",
  97. headers: withBearerAuth({}, session.token()),
  98. body: JSON.stringify({
  99. password: currentPassword,
  100. new_password: newPassword
  101. })
  102. });
  103. }
  104. async createToken(label, expires) {
  105. const url = accountTokenUrl(config.base_url);
  106. const body = {
  107. label: label,
  108. expires: (expires > 0) ? Math.floor(Date.now() / 1000) + expires : 0
  109. };
  110. console.log(`[AccountApi] Creating user access token ${url}`);
  111. await fetchOrThrow(url, {
  112. method: "POST",
  113. headers: withBearerAuth({}, session.token()),
  114. body: JSON.stringify(body)
  115. });
  116. }
  117. async updateToken(token, label, expires) {
  118. const url = accountTokenUrl(config.base_url);
  119. const body = {
  120. token: token,
  121. label: label
  122. };
  123. if (expires > 0) {
  124. body.expires = Math.floor(Date.now() / 1000) + expires;
  125. }
  126. console.log(`[AccountApi] Creating user access token ${url}`);
  127. await fetchOrThrow(url, {
  128. method: "PATCH",
  129. headers: withBearerAuth({}, session.token()),
  130. body: JSON.stringify(body)
  131. });
  132. }
  133. async extendToken() {
  134. const url = accountTokenUrl(config.base_url);
  135. console.log(`[AccountApi] Extending user access token ${url}`);
  136. await fetchOrThrow(url, {
  137. method: "PATCH",
  138. headers: withBearerAuth({}, session.token()),
  139. body: JSON.stringify({
  140. token: session.token(),
  141. expires: Math.floor(Date.now() / 1000) + 6220800 // FIXME
  142. })
  143. });
  144. }
  145. async deleteToken(token) {
  146. const url = accountTokenUrl(config.base_url);
  147. console.log(`[AccountApi] Deleting user access token ${url}`);
  148. await fetchOrThrow(url, {
  149. method: "DELETE",
  150. headers: withBearerAuth({"X-Token": token}, session.token())
  151. });
  152. }
  153. async updateSettings(payload) {
  154. const url = accountSettingsUrl(config.base_url);
  155. const body = JSON.stringify(payload);
  156. console.log(`[AccountApi] Updating user account ${url}: ${body}`);
  157. await fetchOrThrow(url, {
  158. method: "PATCH",
  159. headers: withBearerAuth({}, session.token()),
  160. body: body
  161. });
  162. }
  163. async addSubscription(payload) {
  164. const url = accountSubscriptionUrl(config.base_url);
  165. const body = JSON.stringify(payload);
  166. console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
  167. const response = await fetchOrThrow(url, {
  168. method: "POST",
  169. headers: withBearerAuth({}, session.token()),
  170. body: body
  171. });
  172. const subscription = await response.json(); // May throw SyntaxError
  173. console.log(`[AccountApi] Subscription`, subscription);
  174. return subscription;
  175. }
  176. async updateSubscription(remoteId, payload) {
  177. const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
  178. const body = JSON.stringify(payload);
  179. console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
  180. const response = await fetchOrThrow(url, {
  181. method: "PATCH",
  182. headers: withBearerAuth({}, session.token()),
  183. body: body
  184. });
  185. const subscription = await response.json(); // May throw SyntaxError
  186. console.log(`[AccountApi] Subscription`, subscription);
  187. return subscription;
  188. }
  189. async deleteSubscription(remoteId) {
  190. const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
  191. console.log(`[AccountApi] Removing user subscription ${url}`);
  192. await fetchOrThrow(url, {
  193. method: "DELETE",
  194. headers: withBearerAuth({}, session.token())
  195. });
  196. }
  197. async upsertReservation(topic, everyone) {
  198. const url = accountReservationUrl(config.base_url);
  199. console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
  200. await fetchOrThrow(url, {
  201. method: "POST",
  202. headers: withBearerAuth({}, session.token()),
  203. body: JSON.stringify({
  204. topic: topic,
  205. everyone: everyone
  206. })
  207. });
  208. }
  209. async deleteReservation(topic, deleteMessages) {
  210. const url = accountReservationSingleUrl(config.base_url, topic);
  211. console.log(`[AccountApi] Removing topic reservation ${url}`);
  212. const headers = {
  213. "X-Delete-Messages": deleteMessages ? "true" : "false"
  214. }
  215. await fetchOrThrow(url, {
  216. method: "DELETE",
  217. headers: withBearerAuth(headers, session.token())
  218. });
  219. }
  220. async billingTiers() {
  221. const url = tiersUrl(config.base_url);
  222. console.log(`[AccountApi] Fetching billing tiers`);
  223. const response = await fetchOrThrow(url); // No auth needed!
  224. return await response.json(); // May throw SyntaxError
  225. }
  226. async createBillingSubscription(tier) {
  227. console.log(`[AccountApi] Creating billing subscription with ${tier}`);
  228. return await this.upsertBillingSubscription("POST", tier)
  229. }
  230. async updateBillingSubscription(tier) {
  231. console.log(`[AccountApi] Updating billing subscription with ${tier}`);
  232. return await this.upsertBillingSubscription("PUT", tier)
  233. }
  234. async upsertBillingSubscription(method, tier) {
  235. const url = accountBillingSubscriptionUrl(config.base_url);
  236. const response = await fetchOrThrow(url, {
  237. method: method,
  238. headers: withBearerAuth({}, session.token()),
  239. body: JSON.stringify({
  240. tier: tier
  241. })
  242. });
  243. return await response.json(); // May throw SyntaxError
  244. }
  245. async deleteBillingSubscription() {
  246. const url = accountBillingSubscriptionUrl(config.base_url);
  247. console.log(`[AccountApi] Cancelling billing subscription`);
  248. await fetchOrThrow(url, {
  249. method: "DELETE",
  250. headers: withBearerAuth({}, session.token())
  251. });
  252. }
  253. async createBillingPortalSession() {
  254. const url = accountBillingPortalUrl(config.base_url);
  255. console.log(`[AccountApi] Creating billing portal session`);
  256. const response = await fetchOrThrow(url, {
  257. method: "POST",
  258. headers: withBearerAuth({}, session.token())
  259. });
  260. return await response.json(); // May throw SyntaxError
  261. }
  262. async sync() {
  263. try {
  264. if (!session.token()) {
  265. return null;
  266. }
  267. console.log(`[AccountApi] Syncing account`);
  268. const account = await this.get();
  269. if (account.language) {
  270. await i18n.changeLanguage(account.language);
  271. }
  272. if (account.notification) {
  273. if (account.notification.sound) {
  274. await prefs.setSound(account.notification.sound);
  275. }
  276. if (account.notification.delete_after) {
  277. await prefs.setDeleteAfter(account.notification.delete_after);
  278. }
  279. if (account.notification.min_priority) {
  280. await prefs.setMinPriority(account.notification.min_priority);
  281. }
  282. }
  283. if (account.subscriptions) {
  284. await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations);
  285. }
  286. return account;
  287. } catch (e) {
  288. console.log(`[AccountApi] Error fetching account`, e);
  289. if (e instanceof UnauthorizedError) {
  290. session.resetAndRedirect(routes.login);
  291. }
  292. }
  293. }
  294. startWorker() {
  295. if (this.timer !== null) {
  296. return;
  297. }
  298. console.log(`[AccountApi] Starting worker`);
  299. this.timer = setInterval(() => this.runWorker(), intervalMillis);
  300. setTimeout(() => this.runWorker(), delayMillis);
  301. }
  302. async runWorker() {
  303. if (!session.token()) {
  304. return;
  305. }
  306. console.log(`[AccountApi] Extending user access token`);
  307. try {
  308. await this.extendToken();
  309. } catch (e) {
  310. console.log(`[AccountApi] Error extending user access token`, e);
  311. }
  312. }
  313. }
  314. // Maps to user.Role in user/types.go
  315. export const Role = {
  316. ADMIN: "admin",
  317. USER: "user"
  318. };
  319. // Maps to server.visitorLimitBasis in server/visitor.go
  320. export const LimitBasis = {
  321. IP: "ip",
  322. TIER: "tier"
  323. };
  324. // Maps to stripe.SubscriptionStatus
  325. export const SubscriptionStatus = {
  326. ACTIVE: "active",
  327. PAST_DUE: "past_due"
  328. };
  329. // Maps to user.Permission in user/types.go
  330. export const Permission = {
  331. READ_WRITE: "read-write",
  332. READ_ONLY: "read-only",
  333. WRITE_ONLY: "write-only",
  334. DENY_ALL: "deny-all"
  335. };
  336. const accountApi = new AccountApi();
  337. export default accountApi;