History

Use fetch helpers instead of fetch (#27026)

WIP because: - [x] Some calls set a `content-type` but send no body, can likely remove the header - [x] Need to check whether `charset=utf-8` has any significance on the webauthn calls, I assume not as it is the default for json content. - [x] Maybe `no-restricted-globals` is better for eslint, but will require a lot of duplication in the yaml or moving eslint config to a `.js` extension. - [x] Maybe export `request` as `fetch`, shadowing the global.

silverwind committed 3 years ago

Commit ae8e8f01 parent 809923817 files · +70 −98Browse files

Unified · Split

17 files
  1. .eslintrc.yaml +4 −1
  2. docs/content/contributing/guidelines-frontend.en-us.md +1 −1
  3. web_src/js/components/DashboardRepoList.vue +3 −2
  4. web_src/js/components/DiffCommitSelector.vue +2 −1
  5. web_src/js/components/RepoBranchTagSelector.vue +2 −2
  6. web_src/js/features/common-global.js +4 −3
  7. web_src/js/features/common-issue-list.js +3 −2
  8. web_src/js/features/comp/ImagePaste.js +2 −7
  9. web_src/js/features/comp/ReactionSelector.js +3 −11
  10. web_src/js/features/copycontent.js +2 −1
  11. web_src/js/features/install.js +2 −1
  12. web_src/js/features/pull-view-file.js +3 −6
  13. web_src/js/features/repo-diff-commit.js +2 −1
  14. web_src/js/features/repo-issue-list.js +3 −15
  15. web_src/js/features/repo-migrate.js +4 −9
  16. web_src/js/features/user-auth-webauthn.js +13 −25
  17. web_src/js/modules/fetch.js +17 −10
.eslintrc.yaml+4−1
@@ -46,6 +46,9 @@ overrides: · 45 unmodified lines
46 - files: ["*.config.*"]46 - files: ["*.config.*"]
47 rules:47 rules:
48 import/no-unused-modules: [0]48 import/no-unused-modules: [0]
49 - files: ["web_src/js/modules/fetch.js", "web_src/js/standalone/**/*"]
50 rules:
51 no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
49 52
50rules:53rules:
51 "@eslint-community/eslint-comments/disable-enable-pair": [2]54 "@eslint-community/eslint-comments/disable-enable-pair": [2]
@@ -420,7 +423,7 @@ rules: · 368 unmodified lines
420 no-restricted-exports: [0]423 no-restricted-exports: [0]
421 no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]424 no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
422 no-restricted-imports: [0]425 no-restricted-imports: [0]
423 no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]426 no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.js instead"}]
424 no-return-assign: [0]427 no-return-assign: [0]
425 no-script-url: [2]428 no-script-url: [2]
426 no-self-assign: [2, {props: true}]429 no-self-assign: [2, {props: true}]
docs/content/contributing/guidelines-frontend.en-us.md+1−1
@@ -95,7 +95,7 @@ Some lint rules and IDEs also have warnings if the returned Promise is not handl · 94 unmodified lines
95### Fetching data95### Fetching data
96 96
97To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They97To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They
98accept a `data` option for the content, will automatically set CSFR token and return a98accept a `data` option for the content, will automatically set CSRF token and return a
99Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).99Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
100 100
101### HTML Attributes and `dataset`101### HTML Attributes and `dataset`
web_src/js/components/DashboardRepoList.vue+3−2
@@ -2,6 +2,7 @@ · 1 unmodified line
2import {createApp, nextTick} from 'vue';2import {createApp, nextTick} from 'vue';
3import $ from 'jquery';3import $ from 'jquery';
4import {SvgIcon} from '../svg.js';4import {SvgIcon} from '../svg.js';
5import {GET} from '../modules/fetch.js';
5 6
6const {appSubUrl, assetUrlPrefix, pageData} = window.config;7const {appSubUrl, assetUrlPrefix, pageData} = window.config;
7 8
@@ -233,11 +234,11 @@ const sfc = { · 225 unmodified lines
233 try {234 try {
234 if (!this.reposTotalCount) {235 if (!this.reposTotalCount) {
235 const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;236 const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
236 response = await fetch(totalCountSearchURL);237 response = await GET(totalCountSearchURL);
237 this.reposTotalCount = response.headers.get('X-Total-Count');238 this.reposTotalCount = response.headers.get('X-Total-Count');
238 }239 }
239 240
240 response = await fetch(searchedURL);241 response = await GET(searchedURL);
241 json = await response.json();242 json = await response.json();
242 } catch {243 } catch {
243 if (searchedURL === this.searchURL) {244 if (searchedURL === this.searchURL) {
web_src/js/components/DiffCommitSelector.vue+2−1
@@ -1,5 +1,6 @@
1<script>1<script>
2import {SvgIcon} from '../svg.js';2import {SvgIcon} from '../svg.js';
3import {GET} from '../modules/fetch.js';
3 4
4export default {5export default {
5 components: {SvgIcon},6 components: {SvgIcon},
@@ -123,7 +124,7 @@ export default { · 117 unmodified lines
123 },124 },
124 /** Load the commits to show in this dropdown */125 /** Load the commits to show in this dropdown */
125 async fetchCommits() {126 async fetchCommits() {
126 const resp = await fetch(`${this.issueLink}/commits/list`);127 const resp = await GET(`${this.issueLink}/commits/list`);
127 const results = await resp.json();128 const results = await resp.json();
128 this.commits.push(...results.commits.map((x) => {129 this.commits.push(...results.commits.map((x) => {
129 x.hovered = false;130 x.hovered = false;
web_src/js/components/RepoBranchTagSelector.vue+2−2
@@ -4,6 +4,7 @@ import $ from 'jquery'; · 3 unmodified lines
4import {SvgIcon} from '../svg.js';4import {SvgIcon} from '../svg.js';
5import {pathEscapeSegments} from '../utils/url.js';5import {pathEscapeSegments} from '../utils/url.js';
6import {showErrorToast} from '../modules/toast.js';6import {showErrorToast} from '../modules/toast.js';
7import {GET} from '../modules/fetch.js';
7 8
8const sfc = {9const sfc = {
9 components: {SvgIcon},10 components: {SvgIcon},
@@ -190,8 +191,7 @@ const sfc = { · 180 unmodified lines
190 }191 }
191 this.isLoading = true;192 this.isLoading = true;
192 try {193 try {
193 const reqUrl = `${this.repoLink}/${this.mode}/list`;194 const resp = await GET(`${this.repoLink}/${this.mode}/list`);
194 const resp = await fetch(reqUrl);
195 const {results} = await resp.json();195 const {results} = await resp.json();
196 for (const result of results) {196 for (const result of results) {
197 let selected = false;197 let selected = false;
web_src/js/features/common-global.js+4−3
@@ -11,6 +11,7 @@ import {htmlEscape} from 'escape-goat'; · 10 unmodified lines
11import {showTemporaryTooltip} from '../modules/tippy.js';11import {showTemporaryTooltip} from '../modules/tippy.js';
12import {confirmModal} from './comp/ConfirmModal.js';12import {confirmModal} from './comp/ConfirmModal.js';
13import {showErrorToast} from '../modules/toast.js';13import {showErrorToast} from '../modules/toast.js';
14import {request} from '../modules/fetch.js';
14 15
15const {appUrl, appSubUrl, csrfToken, i18n} = window.config;16const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
16 17
@@ -81,7 +82,7 @@ function fetchActionDoRedirect(redirect) { · 64 unmodified lines
81 82
82async function fetchActionDoRequest(actionElem, url, opt) {83async function fetchActionDoRequest(actionElem, url, opt) {
83 try {84 try {
84 const resp = await fetch(url, opt);85 const resp = await request(url, opt);
85 if (resp.status === 200) {86 if (resp.status === 200) {
86 let {redirect} = await resp.json();87 let {redirect} = await resp.json();
87 redirect = redirect || actionElem.getAttribute('data-redirect');88 redirect = redirect || actionElem.getAttribute('data-redirect');
@@ -127,7 +128,7 @@ async function formFetchAction(e) { · 39 unmodified lines
127 }128 }
128 129
129 let reqUrl = formActionUrl;130 let reqUrl = formActionUrl;
130 const reqOpt = {method: formMethod.toUpperCase(), headers: {'X-Csrf-Token': csrfToken}};131 const reqOpt = {method: formMethod.toUpperCase()};
131 if (formMethod.toLowerCase() === 'get') {132 if (formMethod.toLowerCase() === 'get') {
132 const params = new URLSearchParams();133 const params = new URLSearchParams();
133 for (const [key, value] of formData) {134 for (const [key, value] of formData) {
@@ -264,7 +265,7 @@ async function linkAction(e) { · 130 unmodified lines
264 const url = el.getAttribute('data-url');265 const url = el.getAttribute('data-url');
265 const doRequest = async () => {266 const doRequest = async () => {
266 el.disabled = true;267 el.disabled = true;
267 await fetchActionDoRequest(el, url, {method: 'POST', headers: {'X-Csrf-Token': csrfToken}});268 await fetchActionDoRequest(el, url, {method: 'POST'});
268 el.disabled = false;269 el.disabled = false;
269 };270 };
270 271
web_src/js/features/common-issue-list.js+3−2
@@ -1,7 +1,8 @@
1import $ from 'jquery';1import $ from 'jquery';
2import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js';2import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js';
3const {appSubUrl} = window.config;3import {GET} from '../modules/fetch.js';
4 4
5const {appSubUrl} = window.config;
5const reIssueIndex = /^(\d+)$/; // eg: "123"6const reIssueIndex = /^(\d+)$/; // eg: "123"
6const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"7const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
7const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}"8const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}"
@@ -54,7 +55,7 @@ export function initCommonIssueListQuickGoto() { · 46 unmodified lines
54 // try to check whether the parsed goto link is valid55 // try to check whether the parsed goto link is valid
55 let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);56 let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
56 if (targetUrl) {57 if (targetUrl) {
57 const res = await fetch(`${targetUrl}/info`);58 const res = await GET(`${targetUrl}/info`);
58 if (res.status !== 200) targetUrl = '';59 if (res.status !== 200) targetUrl = '';
59 }60 }
60 61
web_src/js/features/comp/ImagePaste.js+2−7
@@ -1,16 +1,11 @@
1import $ from 'jquery';1import $ from 'jquery';
2 2import {POST} from '../../modules/fetch.js';
3const {csrfToken} = window.config;
4 3
5async function uploadFile(file, uploadUrl) {4async function uploadFile(file, uploadUrl) {
6 const formData = new FormData();5 const formData = new FormData();
7 formData.append('file', file, file.name);6 formData.append('file', file, file.name);
8 7
9 const res = await fetch(uploadUrl, {8 const res = await POST(uploadUrl, {data: formData});
10 method: 'POST',
11 headers: {'X-Csrf-Token': csrfToken},
12 body: formData,
13 });
14 return await res.json();9 return await res.json();
15}10}
16 11
web_src/js/features/comp/ReactionSelector.js+3−11
@@ -1,6 +1,5 @@
1import $ from 'jquery';1import $ from 'jquery';
2 2import {POST} from '../../modules/fetch.js';
3const {csrfToken} = window.config;
4 3
5export function initCompReactionSelector($parent) {4export function initCompReactionSelector($parent) {
6 $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {5 $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {
@@ -12,15 +11,8 @@ export function initCompReactionSelector($parent) { · 5 unmodified lines
12 const reactionContent = $(this).attr('data-reaction-content');11 const reactionContent = $(this).attr('data-reaction-content');
13 const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true';12 const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true';
14 13
15 const res = await fetch(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {14 const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
16 method: 'POST',15 data: new URLSearchParams({content: reactionContent}),
17 headers: {
18 'content-type': 'application/x-www-form-urlencoded',
19 },
20 body: new URLSearchParams({
21 _csrf: csrfToken,
22 content: reactionContent,
23 }),
24 });16 });
25 17
26 const data = await res.json();18 const data = await res.json();
web_src/js/features/copycontent.js+2−1
@@ -1,6 +1,7 @@
1import {clippie} from 'clippie';1import {clippie} from 'clippie';
2import {showTemporaryTooltip} from '../modules/tippy.js';2import {showTemporaryTooltip} from '../modules/tippy.js';
3import {convertImage} from '../utils.js';3import {convertImage} from '../utils.js';
4import {GET} from '../modules/fetch.js';
4 5
5const {i18n} = window.config;6const {i18n} = window.config;
6 7
@@ -20,7 +21,7 @@ export function initCopyContent() { · 13 unmodified lines
20 if (link) {21 if (link) {
21 btn.classList.add('is-loading', 'small-loading-icon');22 btn.classList.add('is-loading', 'small-loading-icon');
22 try {23 try {
23 const res = await fetch(link, {credentials: 'include', redirect: 'follow'});24 const res = await GET(link, {credentials: 'include', redirect: 'follow'});
24 const contentType = res.headers.get('content-type');25 const contentType = res.headers.get('content-type');
25 26
26 if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {27 if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {
web_src/js/features/install.js+2−1
@@ -1,5 +1,6 @@
1import $ from 'jquery';1import $ from 'jquery';
2import {hideElem, showElem} from '../utils/dom.js';2import {hideElem, showElem} from '../utils/dom.js';
3import {GET} from '../modules/fetch.js';
3 4
4export function initInstall() {5export function initInstall() {
5 const $page = $('.page-content.install');6 const $page = $('.page-content.install');
@@ -111,7 +112,7 @@ function initPostInstall() { · 105 unmodified lines
111 const targetUrl = el.getAttribute('href');112 const targetUrl = el.getAttribute('href');
112 let tid = setInterval(async () => {113 let tid = setInterval(async () => {
113 try {114 try {
114 const resp = await fetch(targetUrl);115 const resp = await GET(targetUrl);
115 if (tid && resp.status === 200) {116 if (tid && resp.status === 200) {
116 clearInterval(tid);117 clearInterval(tid);
117 tid = null;118 tid = null;
web_src/js/features/pull-view-file.js+3−6
@@ -1,7 +1,8 @@
1import {diffTreeStore} from '../modules/stores.js';1import {diffTreeStore} from '../modules/stores.js';
2import {setFileFolding} from './file-fold.js';2import {setFileFolding} from './file-fold.js';
3import {POST} from '../modules/fetch.js';
3 4
4const {csrfToken, pageData} = window.config;5const {pageData} = window.config;
5const prReview = pageData.prReview || {};6const prReview = pageData.prReview || {};
6const viewedStyleClass = 'viewed-file-checked-form';7const viewedStyleClass = 'viewed-file-checked-form';
7const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found8const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found
@@ -68,11 +69,7 @@ export function initViewedCheckboxListenerFor() { · 60 unmodified lines
68 const data = {files};69 const data = {files};
69 const headCommitSHA = form.getAttribute('data-headcommit');70 const headCommitSHA = form.getAttribute('data-headcommit');
70 if (headCommitSHA) data.headCommitSHA = headCommitSHA;71 if (headCommitSHA) data.headCommitSHA = headCommitSHA;
71 fetch(form.getAttribute('data-link'), {72 POST(form.getAttribute('data-link'), {data});
72 method: 'POST',
73 headers: {'X-Csrf-Token': csrfToken},
74 body: JSON.stringify(data),
75 });
76 73
77 // Fold the file accordingly74 // Fold the file accordingly
78 const parentBox = form.closest('.diff-file-header');75 const parentBox = form.closest('.diff-file-header');
web_src/js/features/repo-diff-commit.js+2−1
@@ -1,9 +1,10 @@
1import {hideElem, showElem, toggleElem} from '../utils/dom.js';1import {hideElem, showElem, toggleElem} from '../utils/dom.js';
2import {GET} from '../modules/fetch.js';
2 3
3async function loadBranchesAndTags(area, loadingButton) {4async function loadBranchesAndTags(area, loadingButton) {
4 loadingButton.classList.add('disabled');5 loadingButton.classList.add('disabled');
5 try {6 try {
6 const res = await fetch(loadingButton.getAttribute('data-fetch-url'));7 const res = await GET(loadingButton.getAttribute('data-fetch-url'));
7 const data = await res.json();8 const data = await res.json();
8 hideElem(loadingButton);9 hideElem(loadingButton);
9 addTags(area, data.tags);10 addTags(area, data.tags);
web_src/js/features/repo-issue-list.js+3−15
@@ -5,6 +5,7 @@ import {htmlEscape} from 'escape-goat'; · 4 unmodified lines
5import {confirmModal} from './comp/ConfirmModal.js';5import {confirmModal} from './comp/ConfirmModal.js';
6import {showErrorToast} from '../modules/toast.js';6import {showErrorToast} from '../modules/toast.js';
7import {createSortable} from '../modules/sortable.js';7import {createSortable} from '../modules/sortable.js';
8import {DELETE, POST} from '../modules/fetch.js';
8 9
9function initRepoIssueListCheckboxes() {10function initRepoIssueListCheckboxes() {
10 const $issueSelectAll = $('.issue-checkbox-all');11 const $issueSelectAll = $('.issue-checkbox-all');
@@ -146,13 +147,7 @@ function initPinRemoveButton() { · 135 unmodified lines
146 const id = Number(el.getAttribute('data-issue-id'));147 const id = Number(el.getAttribute('data-issue-id'));
147 148
148 // Send the unpin request149 // Send the unpin request
149 const response = await fetch(el.getAttribute('data-unpin-url'), {150 const response = await DELETE(el.getAttribute('data-unpin-url'));
150 method: 'delete',
151 headers: {
152 'X-Csrf-Token': window.config.csrfToken,
153 'Content-Type': 'application/json',
154 },
155 });
156 if (response.ok) {151 if (response.ok) {
157 // Delete the tooltip152 // Delete the tooltip
158 el._tippy.destroy();153 el._tippy.destroy();
@@ -166,14 +161,7 @@ function initPinRemoveButton() { · 7 unmodified lines
166async function pinMoveEnd(e) {161async function pinMoveEnd(e) {
167 const url = e.item.getAttribute('data-move-url');162 const url = e.item.getAttribute('data-move-url');
168 const id = Number(e.item.getAttribute('data-issue-id'));163 const id = Number(e.item.getAttribute('data-issue-id'));
169 await fetch(url, {164 await POST(url, {data: {id, position: e.newIndex + 1}});
170 method: 'post',
171 body: JSON.stringify({id, position: e.newIndex + 1}),
172 headers: {
173 'X-Csrf-Token': window.config.csrfToken,
174 'Content-Type': 'application/json',
175 },
176 });
177}165}
178 166
179async function initIssuePinSort() {167async function initIssuePinSort() {
web_src/js/features/repo-migrate.js+4−9
@@ -1,7 +1,8 @@
1import $ from 'jquery';1import $ from 'jquery';
2import {hideElem, showElem} from '../utils/dom.js';2import {hideElem, showElem} from '../utils/dom.js';
3import {GET, POST} from '../modules/fetch.js';
3 4
4const {appSubUrl, csrfToken} = window.config;5const {appSubUrl} = window.config;
5 6
6export function initRepoMigrationStatusChecker() {7export function initRepoMigrationStatusChecker() {
7 const $repoMigrating = $('#repo_migrating');8 const $repoMigrating = $('#repo_migrating');
@@ -13,7 +14,7 @@ export function initRepoMigrationStatusChecker() { · 5 unmodified lines
13 14
14 // returns true if the refresh still need to be called after a while15 // returns true if the refresh still need to be called after a while
15 const refresh = async () => {16 const refresh = async () => {
16 const res = await fetch(`${appSubUrl}/user/task/${task}`);17 const res = await GET(`${appSubUrl}/user/task/${task}`);
17 if (res.status !== 200) return true; // continue to refresh if network error occurs18 if (res.status !== 200) return true; // continue to refresh if network error occurs
18 19
19 const data = await res.json();20 const data = await res.json();
@@ -58,12 +59,6 @@ export function initRepoMigrationStatusChecker() { · 38 unmodified lines
58}59}
59 60
60async function doMigrationRetry(e) {61async function doMigrationRetry(e) {
61 await fetch($(e.target).attr('data-migrating-task-retry-url'), {62 await POST($(e.target).attr('data-migrating-task-retry-url'));
62 method: 'post',
63 headers: {
64 'X-Csrf-Token': csrfToken,
65 'Content-Type': 'application/json',
66 },
67 });
68 window.location.reload();63 window.location.reload();
69}64}
web_src/js/features/user-auth-webauthn.js+13−25
@@ -1,7 +1,8 @@
1import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';1import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
2import {showElem} from '../utils/dom.js';2import {showElem} from '../utils/dom.js';
3import {GET, POST} from '../modules/fetch.js';
3 4
4const {appSubUrl, csrfToken} = window.config;5const {appSubUrl} = window.config;
5 6
6export async function initUserAuthWebAuthn() {7export async function initUserAuthWebAuthn() {
7 const elPrompt = document.querySelector('.user.signin.webauthn-prompt');8 const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
@@ -13,7 +14,7 @@ export async function initUserAuthWebAuthn() { · 5 unmodified lines
13 return;14 return;
14 }15 }
15 16
16 const res = await fetch(`${appSubUrl}/user/webauthn/assertion`);17 const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
17 if (res.status !== 200) {18 if (res.status !== 200) {
18 webAuthnError('unknown');19 webAuthnError('unknown');
19 return;20 return;
@@ -53,12 +54,8 @@ async function verifyAssertion(assertedCredential) { · 33 unmodified lines
53 const sig = new Uint8Array(assertedCredential.response.signature);54 const sig = new Uint8Array(assertedCredential.response.signature);
54 const userHandle = new Uint8Array(assertedCredential.response.userHandle);55 const userHandle = new Uint8Array(assertedCredential.response.userHandle);
55 56
56 const res = await fetch(`${appSubUrl}/user/webauthn/assertion`, {57 const res = await POST(`${appSubUrl}/user/webauthn/assertion`, {
57 method: 'POST',58 data: {
58 headers: {
59 'Content-Type': 'application/json; charset=utf-8'
60 },
61 body: JSON.stringify({
62 id: assertedCredential.id,59 id: assertedCredential.id,
63 rawId: encodeURLEncodedBase64(rawId),60 rawId: encodeURLEncodedBase64(rawId),
64 type: assertedCredential.type,61 type: assertedCredential.type,
@@ -69,7 +66,7 @@ async function verifyAssertion(assertedCredential) { · 4 unmodified lines
69 signature: encodeURLEncodedBase64(sig),66 signature: encodeURLEncodedBase64(sig),
70 userHandle: encodeURLEncodedBase64(userHandle),67 userHandle: encodeURLEncodedBase64(userHandle),
71 },68 },
72 }),69 },
73 });70 });
74 if (res.status === 500) {71 if (res.status === 500) {
75 webAuthnError('unknown');72 webAuthnError('unknown');
@@ -88,13 +85,8 @@ async function webauthnRegistered(newCredential) { · 12 unmodified lines
88 const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);85 const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
89 const rawId = new Uint8Array(newCredential.rawId);86 const rawId = new Uint8Array(newCredential.rawId);
90 87
91 const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/register`, {88 const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, {
92 method: 'POST',89 data: {
93 headers: {
94 'X-Csrf-Token': csrfToken,
95 'Content-Type': 'application/json; charset=utf-8',
96 },
97 body: JSON.stringify({
98 id: newCredential.id,90 id: newCredential.id,
99 rawId: encodeURLEncodedBase64(rawId),91 rawId: encodeURLEncodedBase64(rawId),
100 type: newCredential.type,92 type: newCredential.type,
@@ -102,7 +94,7 @@ async function webauthnRegistered(newCredential) { · 1 unmodified line
102 attestationObject: encodeURLEncodedBase64(attestationObject),94 attestationObject: encodeURLEncodedBase64(attestationObject),
103 clientDataJSON: encodeURLEncodedBase64(clientDataJSON),95 clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
104 },96 },
105 }),97 },
106 });98 });
107 99
108 if (res.status === 409) {100 if (res.status === 409) {
@@ -165,15 +157,11 @@ export function initUserAuthWebAuthnRegister() { · 56 unmodified lines
165async function webAuthnRegisterRequest() {157async function webAuthnRegisterRequest() {
166 const elNickname = document.getElementById('nickname');158 const elNickname = document.getElementById('nickname');
167 159
168 const body = new FormData();160 const formData = new FormData();
169 body.append('name', elNickname.value);161 formData.append('name', elNickname.value);
170 162
171 const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/request_register`, {163 const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
172 method: 'POST',164 data: formData,
173 headers: {
174 'X-Csrf-Token': csrfToken,
175 },
176 body,
177 });165 });
178 166
179 if (res.status === 409) {167 if (res.status === 409) {
web_src/js/modules/fetch.js+17−10
@@ -2,17 +2,18 @@ import {isObject} from '../utils.js'; · 1 unmodified line
2 2
3const {csrfToken} = window.config;3const {csrfToken} = window.config;
4 4
5// safe HTTP methods that don't need a csrf token
6const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
7
5// fetch wrapper, use below method name functions and the `data` option to pass in data8// fetch wrapper, use below method name functions and the `data` option to pass in data
6// which will automatically set an appropriate content-type header. For json content,9// which will automatically set an appropriate headers. For json content, only object
7// only object and array types are currently supported.10// and array types are currently supported.
8function request(url, {headers, data, body, ...other} = {}) {11export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
9 let contentType;12 let contentType;
10 if (!body) {13 if (!body) {
11 if (data instanceof FormData) {14 if (data instanceof FormData) {
12 contentType = 'multipart/form-data';
13 body = data;15 body = data;
14 } else if (data instanceof URLSearchParams) {16 } else if (data instanceof URLSearchParams) {
15 contentType = 'application/x-www-form-urlencoded';
16 body = data;17 body = data;
17 } else if (isObject(data) || Array.isArray(data)) {18 } else if (isObject(data) || Array.isArray(data)) {
18 contentType = 'application/json';19 contentType = 'application/json';
@@ -20,12 +21,18 @@ function request(url, {headers, data, body, ...other} = {}) { · 1 unmodified line
20 }21 }
21 }22 }
22 23
24 const headersMerged = new Headers({
25 ...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}),
26 ...(contentType && {'content-type': contentType}),
27 });
28
29 for (const [name, value] of Object.entries(headers)) {
30 headersMerged.set(name, value);
31 }
32
23 return fetch(url, {33 return fetch(url, {
24 headers: {34 method,
25 'x-csrf-token': csrfToken,35 headers: headersMerged,
26 ...(contentType && {'content-type': contentType}),
27 ...headers,
28 },
29 ...(body && {body}),36 ...(body && {body}),
30 ...other,37 ...other,
31 });38 });