• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-nuxt3 / d60d1c4b-75bf-46f6-96a2-00a96c5fc870

01 Aug 2025 09:29PM UTC coverage: 49.147% (-0.05%) from 49.193%
d60d1c4b-75bf-46f6-96a2-00a96c5fc870

push

circleci

edwh
Add marketing consent during sign-up/sign-in - test fixes

2243 of 5656 branches covered (39.66%)

Branch coverage included in aggregate %.

5249 of 9588 relevant lines covered (54.75%)

90.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.04
/api/BaseAPI.js
1
import * as Sentry from '@sentry/browser'
2
import { fetchRetry } from '~/composables/useFetchRetry'
3
import { useAuthStore } from '~/stores/auth'
4
import { useMiscStore } from '~/stores/misc'
5

6
let requestId = 0
44✔
7

8
// let timer = 0
9

10
// We add fetch retrying.
11
//
12
// Note that $fetch and useFetch cause problems on Node v18, so we don't use them.
13
const ourFetch = fetchRetry(fetch)
44✔
14
export class APIError extends Error {
44✔
15
  constructor({ request, response }, message) {
16
    super(message)
110✔
17
    Object.assign(this, { request, response })
110✔
18
  }
19
}
20

21
export class MaintenanceError extends Error {
44✔
22
  constructor({ request, response }, message) {
23
    super(message)
×
24
    Object.assign(this, { request, response })
×
25
  }
26
}
27

28
export class LoginError extends Error {
44✔
29
  constructor(ret, status) {
30
    super(status)
×
31
    Object.assign(this, { ret, status })
×
32
  }
33
}
34

35
export class SignUpError extends Error {
44✔
36
  constructor(ret, status) {
37
    super(status)
×
38
    Object.assign(this, { ret, status })
×
39
  }
40
}
41

42
export default class BaseAPI {
44✔
43
  constructor(config) {
44
    this.config = config
78,351✔
45
  }
46

47
  async $request(method, path, config, logError = true, body = null) {
2,148✔
48
    let status = null
1,074✔
49
    let data = null
1,074✔
50

51
    try {
1,074✔
52
      const headers = config.headers ? config.headers : {}
1,074✔
53

54
      const authStore = useAuthStore()
1,074✔
55

56
      if (authStore.auth.persistent) {
1,074✔
57
        // Use the persistent token (a kind of JWT) to authenticate the request.
58
        headers.Authorization =
553✔
59
          'Iznik ' + JSON.stringify(authStore.auth.persistent)
60
      }
61

62
      const loggedInAs = authStore.user?.id
1,074✔
63

64
      if (loggedInAs) {
1,074✔
65
        // Add the user ID as a query parameter to the path, checking whether there already are any
66
        // query parameters.
67
        path += (path.includes('?') ? '&' : '?') + 'loggedInAs=' + loggedInAs
505!
68
      }
69

70
      // Add requestId to the path, checking whether there already are any query parameters.
71
      path += (path.includes('?') ? '&' : '?') + 'requestid=' + requestId++
1,074✔
72

73
      headers['Cache-Control'] =
1,074✔
74
        'max-age=0, must-revalidate, no-cache, no-store, private'
75

76
      if (method === 'GET' && config?.params) {
1,074✔
77
        // Remove falsey values from the params.
78
        config.params = Object.fromEntries(
268✔
79
          Object.entries(config.params).filter(([_, v]) => v)
80
        )
81

82
        // URL encode the parameters if any
83
        const urlParams = new URLSearchParams(config.params).toString()
268✔
84

85
        if (urlParams.length) {
268✔
86
          path += '&' + urlParams
244✔
87
        }
88
      } else if (method !== 'POST') {
806!
89
        // Any parameters are passed in config.params.
90
        if (!config?.params) {
×
91
          config.params = {}
×
92
        }
93

94
        config.params.modtools = false
×
95

96
        // JSON-encode these for to pass.
97
        body = JSON.stringify(config.params)
×
98
      } else if (!config?.formPost) {
806!
99
        // Parameters will be passed in config.data.
100
        if (!config.data) {
806!
101
          config.data = {}
×
102
        }
103

104
        config.data.modtools = false
806✔
105
        body = JSON.stringify(config.data)
806✔
106
      }
107

108
      const miscStore = useMiscStore()
1,074✔
109
      await miscStore.waitForOnline()
1,074✔
110
      miscStore.api(1)
1,074✔
111
      ;[status, data] = await ourFetch(this.config.public.APIv1 + path, {
1,074✔
112
        ...config,
113
        body,
114
        method,
115
        headers,
116
      })
117

118
      if (data.jwt && data.jwt !== authStore.auth.jwt && data.persistent) {
1,074✔
119
        // We've been given a new JWT.  Use it in future.  This can happen after user merge or periodically when
120
        // we renew the JWT.
121
        authStore.setAuth(data.jwt, data.persistent)
94✔
122
      }
123
    } catch (e) {
124
      if (e.message.match(/.*aborted.*/i)) {
35!
125
        // We've seen requests get aborted immediately after beforeunload().  Makes sense to abort the requests
126
        // when you're leaving a page.  No point in rippling those errors up to result in Sentry errors.
127
        // Swallow these by returning a problem that never resolves.  Possible memory leak but it's a rare case.
128
        console.log('Aborted - ignore')
×
129
        return new Promise(function (resolve) {})
×
130
      }
131
    } finally {
132
      useMiscStore().api(-1)
1,063✔
133
    }
134

135
    // HTTP errors are real errors.
136
    //
137
    // We've sometimes seen 200 response codes with no returned data (I saw this myself on a train with flaky
138
    // signal).  So that's an error if it happens.
139
    //
140
    // data.ret holds the server error.
141
    // - 1 means not logged in, and that's ok.
142
    // - POSTs to session can return errors we want to handle.
143
    // - 999 can happen if people double-click, and we should just quietly drop it because the first click will
144
    //   probably do the right thing.
145
    // - otherwise throw an exception.
146
    if (
1,074✔
147
      status !== 200 ||
3,308✔
148
      !data ||
149
      (data.ret !== 0 &&
150
        !(data.ret === 1 && data.status === 'Not logged in') &&
174✔
151
        !(path === '/session' && method === 'POST') &&
38!
152
        data.ret !== 999)
153
    ) {
154
      const retstr = data && data.ret ? data.ret : 'Unknown'
35!
155
      const statusstr = data && data.status ? data.status : 'Unknown'
35!
156

157
      if (retstr === 111) {
35!
158
        // Down for maintenance
159
        console.log('Down for maintenance')
×
160
        throw new MaintenanceError(data.ret, 'Maintenance error')
×
161
      } else {
162
        // Whether or not we log this error to Sentry depends.  Most errors are worth logging, because they're unexpected.
163
        // But some API calls are expected to fail, and throw an exception which is then handled in the code.  We don't
164
        // want to log those, otherwise we will spend time investigating them in Sentry.  So we have a parameter which
165
        // indicates whether we want to log this to Sentry - which can be a boolean or a function for more complex
166
        // decisions.
167
        const log = typeof logError === 'function' ? logError(data) : logError
35✔
168
        console.log('Log it?', log)
35✔
169

170
        if (
35!
171
          log &&
140✔
172
          (status !== null || retstr !== 'Unknown' || statusstr !== 'Unknown')
173
        ) {
174
          Sentry.captureMessage(
×
175
            'API request failed ' +
176
              path +
177
              ' returned HTTP ' +
178
              status +
179
              ' ret ' +
180
              retstr +
181
              ' status ' +
182
              statusstr
183
          )
184
        }
185

186
        const message = [
35✔
187
          'API Error',
188
          method,
189
          path,
190
          '->',
191
          `ret: ${retstr} status: ${statusstr}`,
192
        ].join(' ')
193

194
        throw new APIError(
35✔
195
          {
196
            request: {
197
              path,
198
              method,
199
              headers: config.headers,
200
              params: config.params,
201
              data: config.data,
202
            },
203
            response: {
204
              status,
205
              data,
206
            },
207
          },
208
          message
209
        )
210
      }
211
    }
212

213
    return data
1,028✔
214
  }
215

216
  $get(path, params = {}, logError = true) {
536✔
217
    return this.$request('GET', path, { params }, logError)
268✔
218
  }
219

220
  $post(path, data, logError = true) {
727✔
221
    return this.$request(
727✔
222
      'POST',
223
      path,
224
      {
225
        data,
226
      },
227
      logError
228
    )
229
  }
230

231
  $postv2(path, data, logError = true) {
4✔
232
    const authStore = useAuthStore()
4✔
233

234
    return this.$requestv2(
4✔
235
      'POST',
236
      path,
237
      {
238
        headers: {
239
          'Content-Type': 'application/json',
240
          Authorization: 'Iznik ' + JSON.stringify(authStore.auth?.persistent),
4!
241
        },
242
        data,
243
      },
244
      logError
245
    )
246
  }
247

248
  $postForm(path, data, logError = true) {
×
249
    // Don't set Content-Type - see https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post
250
    return this.$request(
×
251
      'POST',
252
      path,
253
      {
254
        formPost: true,
255
      },
256
      logError,
257
      data
258
    )
259
  }
260

261
  $postOverride(overrideMethod, path, data, logError = true) {
79✔
262
    return this.$request(
79✔
263
      'POST',
264
      path,
265
      {
266
        data,
267
        headers: {
268
          'X-HTTP-Method-Override': overrideMethod,
269
        },
270
      },
271
      logError
272
    )
273
  }
274

275
  $put(path, data, logError = true) {
26✔
276
    return this.$postOverride('PUT', path, data, logError)
26✔
277
  }
278

279
  $patch(path, data, logError = true) {
52✔
280
    return this.$postOverride('PATCH', path, data, logError)
52✔
281
  }
282

283
  $del(path, data, config = {}, logError = true) {
2✔
284
    return this.$postOverride('DELETE', path, data, logError)
1✔
285
  }
286

287
  async $requestv2(method, path, config, logError = true, body = null) {
2,696✔
288
    // timer++
289
    // const timerLabel = path + ' api-' + timer
290

291
    // console.log('Start ', timerLabel)
292
    // console.time(timerLabel)
293

294
    let status = null
1,348✔
295
    let data = null
1,348✔
296
    const headers = config.headers ? config.headers : {}
1,348✔
297

298
    try {
1,348✔
299
      const authStore = useAuthStore()
1,348✔
300

301
      if (authStore?.auth?.jwt) {
1,348!
302
        // Use the JWT to authenticate the request if possible.
303
        headers.Authorization = JSON.stringify(authStore.auth.jwt)
1,306✔
304
      }
305

306
      if (authStore?.auth?.persistent) {
1,348!
307
        // The JWT is quick but short-lived; use the persistent token as a fallback.
308
        headers.Authorization2 = JSON.stringify(authStore.auth.persistent)
1,306✔
309
      }
310

311
      const loggedInAs = authStore.user?.id
1,348✔
312

313
      if (loggedInAs) {
1,348✔
314
        // Add the user ID as a query parameter to the path, checking whether there already are any
315
        // query parameters.
316
        path += (path.includes('?') ? '&' : '?') + 'loggedInAs=' + loggedInAs
1,257✔
317
      }
318

319
      // Add requestId to the path, checking whether there already are any query parameters.
320
      path += (path.includes('?') ? '&' : '?') + 'requestid=' + requestId++
1,348✔
321

322
      headers['Cache-Control'] =
1,348✔
323
        'max-age=0, must-revalidate, no-cache, no-store, private'
324

325
      if (method === 'GET' && config?.params) {
1,348✔
326
        // Remove falsey values from the params.
327
        config.params = Object.fromEntries(
1,344✔
328
          Object.entries(config.params).filter(([_, v]) => v)
329
        )
330

331
        // URL encode the parameters if any
332
        const urlParams = new URLSearchParams(config.params).toString()
1,344✔
333

334
        if (urlParams.length) {
1,344✔
335
          path += '&' + urlParams
396✔
336
        }
337
      } else if (method !== 'POST') {
4!
338
        // Any parameters are passed in config.params.
339
        if (!config?.params) {
×
340
          config.params = {}
×
341
        }
342

343
        config.params.modtools = false
×
344

345
        // JSON-encode these for to pass.
346
        body = JSON.stringify(config.params)
×
347
      } else if (!config?.formPost) {
4!
348
        // Parameters will be passed in config.data.
349
        if (!config.data) {
4!
350
          config.data = {}
×
351
        }
352

353
        config.data.modtools = false
4✔
354
        body = JSON.stringify(config.data)
4✔
355
      }
356

357
      const miscStore = useMiscStore()
1,348✔
358
      await miscStore.waitForOnline()
1,348✔
359
      miscStore.api(1)
1,348✔
360
      ;[status, data] = await ourFetch(this.config.public.APIv2 + path, {
1,348✔
361
        ...config,
362
        body,
363
        method,
364
        headers,
365
      })
366

367
      if (status === 401) {
1,347!
368
        // Not authorised - our JWT and/or persistent token must be wrong.  Clear them.  This may force a login, or
369
        // not, depending on whether the page requires it.
370
        console.log('Not authorised - force logged out')
×
371
        authStore.setAuth(null, null)
×
372
        authStore.setUser(null)
×
373
      }
374
    } catch (e) {
375
      console.log('Fetch error', path, e?.message)
75!
376
      if (e?.response?.status) {
75!
377
        status = e.response.status
×
378
      }
379

380
      if (e.message.match(/.*aborted.*/i)) {
75!
381
        // We've seen requests get aborted immediately after beforeunload().  Makes sense to abort the requests
382
        // when you're leaving a page.  No point in rippling those errors up to result in Sentry errors.
383
        // Swallow these by returning a problem that never resolves.  Possible memory leak but it's a rare case.
384
        console.log('Aborted - ignore')
×
385
        return new Promise(function (resolve) {})
×
386
      }
387
    } finally {
388
      useMiscStore().api(-1)
1,342✔
389
    }
390

391
    // HTTP errors are real errors.
392
    //
393
    // data.ret holds the server error.
394
    // - 1 means not logged in, and that's ok.
395
    // - POSTs to session can return errors we want to handle.
396
    // - 999 can happen if people double-click, and we should just quietly drop it because the first click will
397
    //   probably do the right thing.
398
    // - otherwise throw an exception.
399
    if (status !== 200) {
1,347✔
400
      const statusstr = status?.toString()
75!
401

402
      // Whether or not we log this error to Sentry depends.  Most errors are worth logging, because they're unexpected.
403
      // But some API calls are expected to fail, and throw an exception which is then handled in the code.  We don't
404
      // want to log those, otherwise we will spend time investigating them in Sentry.  So we have a parameter which
405
      // indicates whether we want to log this to Sentry - which can be a boolean or a function for more complex
406
      // decisions.
407
      const log = typeof logError === 'function' ? logError(data) : logError
75!
408

409
      if (log && (status !== null || statusstr !== 'Unknown')) {
75✔
410
        Sentry.captureMessage(
16✔
411
          'API2 request failed ' +
412
            path +
413
            ' returned HTTP ' +
414
            status +
415
            ' status ' +
416
            statusstr +
417
            ' data length ' +
418
            (data ? data.length : 0)
16!
419
        )
420
      }
421

422
      const message = [
75✔
423
        'API Error',
424
        method,
425
        path,
426
        '->',
427
        `status: ${statusstr}`,
428
      ].join(' ')
429

430
      throw new APIError(
75✔
431
        {
432
          request: {
433
            path,
434
            method,
435
            headers: config.headers,
436
            params: config.params,
437
            data: config.data,
438
          },
439
          response: {
440
            status,
441
            data,
442
          },
443
        },
444
        message
445
      )
446
    }
447

448
    // console.timeEnd(timerLabel)
449

450
    return data
1,267✔
451
  }
452

453
  $getv2(path, params = {}, logError = true) {
2,688✔
454
    return this.$requestv2('GET', path, { params }, logError)
1,344✔
455
  }
456
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc