Api.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import {
  2. fetchLinesIterator,
  3. maybeWithBasicAuth, maybeWithBearerAuth,
  4. topicShortUrl,
  5. topicUrl,
  6. topicUrlAuth,
  7. topicUrlJsonPoll,
  8. topicUrlJsonPollWithSince,
  9. accountSettingsUrl,
  10. accountTokenUrl,
  11. userStatsUrl, accountSubscriptionUrl, accountSubscriptionSingleUrl, accountUrl, accountPasswordUrl
  12. } from "./utils";
  13. import userManager from "./UserManager";
  14. class Api {
  15. async poll(baseUrl, topic, since) {
  16. const user = await userManager.get(baseUrl);
  17. const shortUrl = topicShortUrl(baseUrl, topic);
  18. const url = (since)
  19. ? topicUrlJsonPollWithSince(baseUrl, topic, since)
  20. : topicUrlJsonPoll(baseUrl, topic);
  21. const messages = [];
  22. const headers = maybeWithBasicAuth({}, user);
  23. console.log(`[Api] Polling ${url}`);
  24. for await (let line of fetchLinesIterator(url, headers)) {
  25. console.log(`[Api, ${shortUrl}] Received message ${line}`);
  26. messages.push(JSON.parse(line));
  27. }
  28. return messages;
  29. }
  30. async publish(baseUrl, topic, message, options) {
  31. const user = await userManager.get(baseUrl);
  32. console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`);
  33. const headers = {};
  34. const body = {
  35. topic: topic,
  36. message: message,
  37. ...options
  38. };
  39. const response = await fetch(baseUrl, {
  40. method: 'PUT',
  41. body: JSON.stringify(body),
  42. headers: maybeWithBasicAuth(headers, user)
  43. });
  44. if (response.status < 200 || response.status > 299) {
  45. throw new Error(`Unexpected response: ${response.status}`);
  46. }
  47. return response;
  48. }
  49. /**
  50. * Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request.
  51. * Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used.
  52. *
  53. * Firefox XHR bug:
  54. * Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error,
  55. * so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the
  56. * correct headers are clearly set. It's quite the odd behavior.
  57. *
  58. * There is an example, and the bug report here:
  59. * - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755
  60. * - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345
  61. */
  62. publishXHR(url, body, headers, onProgress) {
  63. console.log(`[Api] Publishing message to ${url}`);
  64. const xhr = new XMLHttpRequest();
  65. const send = new Promise(function (resolve, reject) {
  66. xhr.open("PUT", url);
  67. if (body.type) {
  68. xhr.overrideMimeType(body.type);
  69. }
  70. for (const [key, value] of Object.entries(headers)) {
  71. xhr.setRequestHeader(key, value);
  72. }
  73. xhr.upload.addEventListener("progress", onProgress);
  74. xhr.addEventListener('readystatechange', (ev) => {
  75. if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
  76. console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response);
  77. resolve(xhr.response);
  78. } else if (xhr.readyState === 4) {
  79. // Firefox bug; see description above!
  80. console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText);
  81. let errorText;
  82. try {
  83. const error = JSON.parse(xhr.responseText);
  84. if (error.code && error.error) {
  85. errorText = `Error ${error.code}: ${error.error}`;
  86. }
  87. } catch (e) {
  88. // Nothing
  89. }
  90. xhr.abort();
  91. reject(errorText ?? "An error occurred");
  92. }
  93. })
  94. xhr.send(body);
  95. });
  96. send.abort = () => {
  97. console.log(`[Api] Publish aborted by user`);
  98. xhr.abort();
  99. }
  100. return send;
  101. }
  102. async topicAuth(baseUrl, topic, user) {
  103. const url = topicUrlAuth(baseUrl, topic);
  104. console.log(`[Api] Checking auth for ${url}`);
  105. const response = await fetch(url, {
  106. headers: maybeWithBasicAuth({}, user)
  107. });
  108. if (response.status >= 200 && response.status <= 299) {
  109. return true;
  110. } else if (!user && response.status === 404) {
  111. return true; // Special case: Anonymous login to old servers return 404 since /<topic>/auth doesn't exist
  112. } else if (response.status === 401 || response.status === 403) { // See server/server.go
  113. return false;
  114. }
  115. throw new Error(`Unexpected server response ${response.status}`);
  116. }
  117. async login(baseUrl, user) {
  118. const url = accountTokenUrl(baseUrl);
  119. console.log(`[Api] Checking auth for ${url}`);
  120. const response = await fetch(url, {
  121. headers: maybeWithBasicAuth({}, user)
  122. });
  123. if (response.status === 401 || response.status === 403) {
  124. return false;
  125. } else if (response.status !== 200) {
  126. throw new Error(`Unexpected server response ${response.status}`);
  127. }
  128. const json = await response.json();
  129. if (!json.token) {
  130. throw new Error(`Unexpected server response: Cannot find token`);
  131. }
  132. return json.token;
  133. }
  134. async logout(baseUrl, token) {
  135. const url = accountTokenUrl(baseUrl);
  136. console.log(`[Api] Logging out from ${url} using token ${token}`);
  137. const response = await fetch(url, {
  138. method: "DELETE",
  139. headers: maybeWithBearerAuth({}, token)
  140. });
  141. if (response.status !== 200) {
  142. throw new Error(`Unexpected server response ${response.status}`);
  143. }
  144. }
  145. async createAccount(baseUrl, username, password) {
  146. const url = accountUrl(baseUrl);
  147. const body = JSON.stringify({
  148. username: username,
  149. password: password
  150. });
  151. console.log(`[Api] Creating user account ${url}`);
  152. const response = await fetch(url, {
  153. method: "POST",
  154. body: body
  155. });
  156. if (response.status !== 200) {
  157. throw new Error(`Unexpected server response ${response.status}`);
  158. }
  159. }
  160. async getAccount(baseUrl, token) {
  161. const url = accountUrl(baseUrl);
  162. console.log(`[Api] Fetching user account ${url}`);
  163. const response = await fetch(url, {
  164. headers: maybeWithBearerAuth({}, token)
  165. });
  166. if (response.status !== 200) {
  167. throw new Error(`Unexpected server response ${response.status}`);
  168. }
  169. const account = await response.json();
  170. console.log(`[Api] Account`, account);
  171. return account;
  172. }
  173. async deleteAccount(baseUrl, token) {
  174. const url = accountUrl(baseUrl);
  175. console.log(`[Api] Deleting user account ${url}`);
  176. const response = await fetch(url, {
  177. method: "DELETE",
  178. headers: maybeWithBearerAuth({}, token)
  179. });
  180. if (response.status !== 200) {
  181. throw new Error(`Unexpected server response ${response.status}`);
  182. }
  183. }
  184. async changePassword(baseUrl, token, password) {
  185. const url = accountPasswordUrl(baseUrl);
  186. console.log(`[Api] Changing account password ${url}`);
  187. const response = await fetch(url, {
  188. method: "POST",
  189. headers: maybeWithBearerAuth({}, token),
  190. body: JSON.stringify({
  191. password: password
  192. })
  193. });
  194. if (response.status !== 200) {
  195. throw new Error(`Unexpected server response ${response.status}`);
  196. }
  197. }
  198. async updateAccountSettings(baseUrl, token, payload) {
  199. const url = accountSettingsUrl(baseUrl);
  200. const body = JSON.stringify(payload);
  201. console.log(`[Api] Updating user account ${url}: ${body}`);
  202. const response = await fetch(url, {
  203. method: "POST",
  204. headers: maybeWithBearerAuth({}, token),
  205. body: body
  206. });
  207. if (response.status !== 200) {
  208. throw new Error(`Unexpected server response ${response.status}`);
  209. }
  210. }
  211. async addAccountSubscription(baseUrl, token, payload) {
  212. const url = accountSubscriptionUrl(baseUrl);
  213. const body = JSON.stringify(payload);
  214. console.log(`[Api] Adding user subscription ${url}: ${body}`);
  215. const response = await fetch(url, {
  216. method: "POST",
  217. headers: maybeWithBearerAuth({}, token),
  218. body: body
  219. });
  220. if (response.status !== 200) {
  221. throw new Error(`Unexpected server response ${response.status}`);
  222. }
  223. const subscription = await response.json();
  224. console.log(`[Api] Subscription`, subscription);
  225. return subscription;
  226. }
  227. async deleteAccountSubscription(baseUrl, token, remoteId) {
  228. const url = accountSubscriptionSingleUrl(baseUrl, remoteId);
  229. console.log(`[Api] Removing user subscription ${url}`);
  230. const response = await fetch(url, {
  231. method: "DELETE",
  232. headers: maybeWithBearerAuth({}, token)
  233. });
  234. if (response.status !== 200) {
  235. throw new Error(`Unexpected server response ${response.status}`);
  236. }
  237. }
  238. }
  239. const api = new Api();
  240. export default api;