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

Freegle / iznik-nuxt3 / c9965611-96a0-43b5-862c-c056c3247eb6

30 Sep 2025 03:31PM UTC coverage: 44.618% (+9.9%) from 34.712%
c9965611-96a0-43b5-862c-c056c3247eb6

push

circleci

edwh
MT: Allow Support Tools to rename groups

1765 of 4803 branches covered (36.75%)

Branch coverage included in aggregate %.

4063 of 8259 relevant lines covered (49.19%)

96.2 hits per line

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

68.18
/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
36✔
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)
36✔
14
export class APIError extends Error {
36✔
15
  constructor({ request, response }, message) {
16
    super(message)
60✔
17
    Object.assign(this, { request, response })
60✔
18
  }
19
}
20

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

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

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

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

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

51
    try {
755✔
52
      const headers = config.headers ? config.headers : {}
755✔
53

54
      const authStore = useAuthStore()
755✔
55
      const miscStore = useMiscStore()
755✔
56

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

63
      const loggedInAs = authStore.user?.id
755✔
64

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

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

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

77
      if (method === 'GET' && config?.params) {
755✔
78
        // Remove falsey values from the params - unless MT needs to send a zero
79
        if (!config?.params.dontzapfalsey) {
182!
80
          config.params = Object.fromEntries(
182✔
81
            Object.entries(config.params).filter(([_, v]) => v)
82
          )
83
        }
84
        config.params.modtools = miscStore.modtools
182✔
85

86
        // MT cope with arrays and objects eg components: ['me','work'] or context: { "Added": 12345678, "id": 12345 }
87
        Object.keys(config.params).forEach((c) => {
182✔
88
          const v = config.params[c]
472✔
89
          if (Array.isArray(v)) {
472✔
90
            delete config.params[c]
128✔
91
            for (let ix = 0; ix < v.length; ix++) {
128✔
92
              config.params[c + '[' + ix + ']'] = v[ix]
128✔
93
            }
94
          } else if (typeof v === 'object' && v !== null) {
344!
95
            delete config.params[c]
×
96
            Object.keys(v).forEach((cp) => {
×
97
              config.params[c + '[' + cp + ']'] = v[cp]
×
98
            })
99
          }
100
        })
101
        // URL encode the parameters if any
102
        const urlParams = new URLSearchParams(config.params).toString()
182✔
103

104
        if (urlParams.length) {
182!
105
          path += '&' + urlParams
182✔
106
        }
107
      } else if (method !== 'POST') {
573!
108
        // Any parameters are passed in config.params.
109
        if (!config?.params) {
×
110
          config.params = {}
×
111
        }
112

113
        config.params.modtools = miscStore.modtools // MT
×
114

115
        // JSON-encode these for to pass.
116
        body = JSON.stringify(config.params)
×
117
      } else if (!config?.formPost) {
573!
118
        // Parameters will be passed in config.data.
119
        if (!config.data) {
573!
120
          config.data = {}
×
121
        }
122

123
        config.data.modtools = miscStore.modtools // MT
573✔
124
        body = JSON.stringify(config.data)
573✔
125
      }
126

127
      await miscStore.waitForOnline()
755✔
128
      miscStore.api(1)
755✔
129
      ;[status, data] = await ourFetch(this.config.public.APIv1 + path, {
755✔
130
        ...config,
131
        body,
132
        method,
133
        headers,
134
      })
135

136
      if (
755✔
137
        data.jwt &&
939✔
138
        data.jwt !== authStore.auth.jwt &&
139
        data.persistent &&
140
        path.substring(0, 5) !== '/user'
141
      ) {
142
        // Stop MT add user from switching identity
143
        // We've been given a new JWT.  Use it in future.  This can happen after user merge or periodically when
144
        // we renew the JWT.
145
        authStore.setAuth(data.jwt, data.persistent)
43✔
146
      }
147
    } catch (e) {
148
      if (e.message.match(/.*aborted.*/i)) {
21!
149
        // We've seen requests get aborted immediately after beforeunload().  Makes sense to abort the requests
150
        // when you're leaving a page.  No point in rippling those errors up to result in Sentry errors.
151
        // Swallow these by returning a problem that never resolves.  Possible memory leak but it's a rare case.
152
        console.log('Aborted - ignore')
×
153
        return new Promise(function (resolve) {})
×
154
      }
155
    } finally {
156
      useMiscStore().api(-1)
751✔
157
    }
158

159
    // HTTP errors are real errors.
160
    //
161
    // We've sometimes seen 200 response codes with no returned data (I saw this myself on a train with flaky
162
    // signal).  So that's an error if it happens.
163
    //
164
    // data.ret holds the server error.
165
    // - 1 means not logged in, and that's ok.
166
    // - POSTs to session can return errors we want to handle.
167
    // - 999 can happen if people double-click, and we should just quietly drop it because the first click will
168
    //   probably do the right thing.
169
    // - otherwise throw an exception.
170
    if (
755✔
171
      status !== 200 ||
2,334✔
172
      !data ||
173
      (data.ret !== 0 &&
174
        !(data.ret === 1 && data.status === 'Not logged in') &&
112✔
175
        !(path === '/session' && method === 'POST') &&
26!
176
        data.ret !== 999)
177
    ) {
178
      const retstr = data && data.ret ? data.ret : 'Unknown'
21!
179
      const statusstr = data && data.status ? data.status : 'Unknown'
21!
180

181
      if (retstr === 111) {
21!
182
        // Down for maintenance
183
        console.log('Down for maintenance')
×
184
        throw new MaintenanceError(data.ret, 'Maintenance error')
×
185
      } else {
186
        // Whether or not we log this error to Sentry depends.  Most errors are worth logging, because they're unexpected.
187
        // But some API calls are expected to fail, and throw an exception which is then handled in the code.  We don't
188
        // want to log those, otherwise we will spend time investigating them in Sentry.  So we have a parameter which
189
        // indicates whether we want to log this to Sentry - which can be a boolean or a function for more complex
190
        // decisions.
191
        const log = typeof logError === 'function' ? logError(data) : logError
21!
192
        console.log('Log it?', log)
21✔
193

194
        if (
21!
195
          log &&
84✔
196
          (status !== null || retstr !== 'Unknown' || statusstr !== 'Unknown')
197
        ) {
198
          Sentry.captureMessage(
×
199
            'API request failed ' +
200
              path +
201
              ' returned HTTP ' +
202
              status +
203
              ' ret ' +
204
              retstr +
205
              ' status ' +
206
              statusstr
207
          )
208
        }
209

210
        const message = [
21✔
211
          'API Error',
212
          method,
213
          path,
214
          '->',
215
          `ret: ${retstr} status: ${statusstr}`,
216
        ].join(' ')
217

218
        throw new APIError(
21✔
219
          {
220
            request: {
221
              path,
222
              method,
223
              headers: config.headers,
224
              params: config.params,
225
              data: config.data,
226
            },
227
            response: {
228
              status,
229
              data,
230
            },
231
          },
232
          message
233
        )
234
      }
235
    }
236

237
    return data
730✔
238
  }
239

240
  $get(path, params = {}, logError = true) {
364✔
241
    return this.$request('GET', path, { params }, logError)
182✔
242
  }
243

244
  $post(path, data, logError = true) {
529✔
245
    return this.$request(
529✔
246
      'POST',
247
      path,
248
      {
249
        data,
250
      },
251
      logError
252
    )
253
  }
254

255
  $postv2(path, data, logError = true) {
×
256
    const authStore = useAuthStore()
×
257

258
    return this.$requestv2(
×
259
      'POST',
260
      path,
261
      {
262
        headers: {
263
          'Content-Type': 'application/json',
264
          Authorization: 'Iznik ' + JSON.stringify(authStore.auth?.persistent),
×
265
        },
266
        data,
267
      },
268
      logError
269
    )
270
  }
271

272
  $postForm(path, data, logError = true) {
×
273
    // Don't set Content-Type - see https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post
274
    return this.$request(
×
275
      'POST',
276
      path,
277
      {
278
        formPost: true,
279
      },
280
      logError,
281
      data
282
    )
283
  }
284

285
  $postOverride(overrideMethod, path, data, logError = true) {
44✔
286
    return this.$request(
44✔
287
      'POST',
288
      path,
289
      {
290
        data,
291
        headers: {
292
          'X-HTTP-Method-Override': overrideMethod,
293
        },
294
      },
295
      logError
296
    )
297
  }
298

299
  $put(path, data, logError = true) {
16✔
300
    return this.$postOverride('PUT', path, data, logError)
16✔
301
  }
302

303
  $patch(path, data, logError = true) {
27✔
304
    return this.$postOverride('PATCH', path, data, logError)
27✔
305
  }
306

307
  $del(path, data, config = {}, logError = true) {
2✔
308
    return this.$postOverride('DELETE', path, data, logError)
1✔
309
  }
310

311
  async $requestv2(method, path, config, logError = true, body = null) {
2,014✔
312
    // timer++
313
    // const timerLabel = path + ' api-' + timer
314

315
    // console.log('Start ', timerLabel)
316
    // console.time(timerLabel)
317

318
    let status = null
1,007✔
319
    let data = null
1,007✔
320
    const headers = config.headers ? config.headers : {}
1,007!
321

322
    try {
1,007✔
323
      const authStore = useAuthStore()
1,007✔
324
      const miscStore = useMiscStore()
1,007✔
325

326
      if (authStore?.auth?.jwt) {
1,007!
327
        // Use the JWT to authenticate the request if possible.
328
        headers.Authorization = JSON.stringify(authStore.auth.jwt)
977✔
329
      }
330

331
      if (authStore?.auth?.persistent) {
1,007!
332
        // The JWT is quick but short-lived; use the persistent token as a fallback.
333
        headers.Authorization2 = JSON.stringify(authStore.auth.persistent)
977✔
334
      }
335

336
      const loggedInAs = authStore.user?.id
1,007✔
337

338
      if (loggedInAs) {
1,007✔
339
        // Add the user ID as a query parameter to the path, checking whether there already are any
340
        // query parameters.
341
        path += (path.includes('?') ? '&' : '?') + 'loggedInAs=' + loggedInAs
946✔
342
      }
343

344
      // Add requestId to the path, checking whether there already are any query parameters.
345
      path += (path.includes('?') ? '&' : '?') + 'requestid=' + requestId++
1,007✔
346

347
      headers['Cache-Control'] =
1,007✔
348
        'max-age=0, must-revalidate, no-cache, no-store, private'
349

350
      if (method === 'GET' && config?.params) {
1,007!
351
        // Remove falsey values from the params.
352
        config.params = Object.fromEntries(
1,007✔
353
          Object.entries(config.params).filter(([_, v]) => v)
354
        )
355

356
        // URL encode the parameters if any
357
        const urlParams = new URLSearchParams(config.params).toString()
1,007✔
358

359
        if (urlParams.length) {
1,007✔
360
          path += '&' + urlParams
256✔
361
        }
362
      } else if (method !== 'POST') {
×
363
        // Any parameters are passed in config.params.
364
        if (!config?.params) {
×
365
          config.params = {}
×
366
        }
367

368
        config.params.modtools = miscStore.modtools // MT
×
369

370
        // JSON-encode these for to pass.
371
        body = JSON.stringify(config.params)
×
372
      } else if (!config?.formPost) {
×
373
        // Parameters will be passed in config.data.
374
        if (!config.data) {
×
375
          config.data = {}
×
376
        }
377

378
        if (!config.params) {
×
379
          config.params = {}
×
380
        }
381

382
        console.log(
×
383
          'Seet MT in config.params',
384
          config,
385
          config.params,
386
          miscStore.modtools
387
        )
388

389
        config.params.modtools = miscStore.modtools
×
390
        body = JSON.stringify(config.data)
×
391
      }
392

393
      await miscStore.waitForOnline()
1,007✔
394
      miscStore.api(1)
1,007✔
395
      ;[status, data] = await ourFetch(this.config.public.APIv2 + path, {
1,007✔
396
        ...config,
397
        body,
398
        method,
399
        headers,
400
      })
401

402
      if (status === 401) {
1,006!
403
        // Not authorised - our JWT and/or persistent token must be wrong.  Clear them.  This may force a login, or
404
        // not, depending on whether the page requires it.
405
        console.log('Not authorised - force logged out')
×
406
        authStore.setAuth(null, null)
×
407
        authStore.setUser(null)
×
408

409
        // For specific paths, we want to silently allow 401 errors and swallow them.
410
        // This can happen if a login token is invalid, and we don't want to show errors to the user.
411
        if (path.startsWith('/chat?includeClosed=true')) {
×
412
          console.log('Silently handling 401 for includeClosed chat request')
×
413
          return new Promise(function (resolve) {})
×
414
        }
415
      }
416
    } catch (e) {
417
      console.log('Fetch error', path, e?.message)
39!
418
      if (e?.response?.status) {
39!
419
        status = e.response.status
×
420
      }
421

422
      if (e.message.match(/.*aborted.*/i)) {
39!
423
        // We've seen requests get aborted immediately after beforeunload().  Makes sense to abort the requests
424
        // when you're leaving a page.  No point in rippling those errors up to result in Sentry errors.
425
        // Swallow these by returning a problem that never resolves.  Possible memory leak but it's a rare case.
426
        console.log('Aborted - ignore')
×
427
        return new Promise(function (resolve) {})
×
428
      }
429
    } finally {
430
      useMiscStore().api(-1)
1,006✔
431
    }
432

433
    // HTTP errors are real errors.
434
    //
435
    // data.ret holds the server error.
436
    // - 1 means not logged in, and that's ok.
437
    // - POSTs to session can return errors we want to handle.
438
    // - 999 can happen if people double-click, and we should just quietly drop it because the first click will
439
    //   probably do the right thing.
440
    // - otherwise throw an exception.
441
    if (status !== 200) {
1,006✔
442
      const statusstr = status?.toString()
39!
443

444
      // For specific paths, we want to silently allow 401 errors and swallow them.
445
      // This can happen if a login token is invalid, and we don't want to show errors to the user.
446
      if (status === 401 && path.startsWith('/chat?includeClosed=true')) {
39!
447
        console.log('Silently handling 401 for includeClosed chat request')
×
448
        return new Promise(function (resolve) {})
×
449
      }
450

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

458
      if (log && (status !== null || statusstr !== 'Unknown')) {
39✔
459
        Sentry.captureMessage(
7✔
460
          'API2 request failed ' +
461
            path +
462
            ' returned HTTP ' +
463
            status +
464
            ' status ' +
465
            statusstr +
466
            ' data length ' +
467
            (data ? data.length : 0)
7!
468
        )
469
      }
470

471
      const message = [
39✔
472
        'API Error',
473
        method,
474
        path,
475
        '->',
476
        `status: ${statusstr}`,
477
      ].join(' ')
478

479
      throw new APIError(
39✔
480
        {
481
          request: {
482
            path,
483
            method,
484
            headers: config.headers,
485
            params: config.params,
486
            data: config.data,
487
          },
488
          response: {
489
            status,
490
            data,
491
          },
492
        },
493
        message
494
      )
495
    }
496

497
    // console.timeEnd(timerLabel)
498

499
    return data
967✔
500
  }
501

502
  $getv2(path, params = {}, logError = true) {
2,014✔
503
    return this.$requestv2('GET', path, { params }, logError)
1,007✔
504
  }
505
}
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