maxibase 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/layers/base/app/components/AuthCheck.vue +50 -10
- package/layers/base/app/pages/signin.vue +0 -10
- package/layers/base/app/pages/user/auth.vue +1 -1
- package/layers/base/server/api/files/[...path].get.ts +1 -1
- package/layers/base/server/api/files/chunk.post.ts +13 -9
- package/layers/base/server/api/files/index.post.ts +10 -11
- package/layers/base/server/database/defaults/access.ts +2 -2
- package/layers/base/server/middleware/2.mfa.ts +1 -1
- package/layers/base/server/routes/dev/email/[template].ts +2 -2
- package/layers/base/server/utils/access.ts +1 -1
- package/layers/base/server/utils/auth.ts +1 -1
- package/layers/base/server/utils/files.ts +25 -23
- package/layers/base/server/utils/ip.ts +1 -1
- package/locales/en.json +1 -0
- package/locales/fr.json +1 -0
- package/locales/pages/signin/en.json +0 -1
- package/locales/pages/signin/fr.json +0 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { WebAuthnAbortService } from '@simplewebauthn/browser'
|
|
3
|
+
|
|
2
4
|
const emits = defineEmits(['authenticated', 'cancel'])
|
|
3
5
|
const appStore = useAppStore()
|
|
4
6
|
const { $t } = useI18n()
|
|
@@ -133,17 +135,23 @@ function signIn() {
|
|
|
133
135
|
}, 100)
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
const
|
|
138
|
+
const webAuthnEndpoints = {
|
|
137
139
|
registerEndpoint: '/api/auth/webauthn/register',
|
|
138
140
|
authenticateEndpoint: '/api/auth/webauthn/authenticate',
|
|
141
|
+
}
|
|
142
|
+
const { authenticate } = useWebAuthn(webAuthnEndpoints)
|
|
143
|
+
const { authenticate: authenticateWithAutofill, isAutofillSupported } = useWebAuthn({
|
|
144
|
+
...webAuthnEndpoints,
|
|
145
|
+
useBrowserAutofill: true,
|
|
139
146
|
})
|
|
140
147
|
|
|
141
|
-
async function signInWithPasskey() {
|
|
148
|
+
async function signInWithPasskey(userName?: string, autofill = false) {
|
|
142
149
|
try {
|
|
143
|
-
await authenticate(
|
|
150
|
+
await (autofill ? authenticateWithAutofill() : authenticate(userName))
|
|
144
151
|
await refreshSession()
|
|
145
152
|
} catch (e: any) {
|
|
146
|
-
|
|
153
|
+
// any other passkey prompt aborts the pending autofill request
|
|
154
|
+
if (e.name !== 'AbortError' && !e.message.startsWith('The operation either timed out')) {
|
|
147
155
|
handleError(e)
|
|
148
156
|
}
|
|
149
157
|
}
|
|
@@ -160,7 +168,7 @@ async function getSignInOptions(email: string) {
|
|
|
160
168
|
password.value = route.query.token as string
|
|
161
169
|
if (!options.value.hasTOTP) await signInWithOtp()
|
|
162
170
|
} else if (options.value.hasPasskey) {
|
|
163
|
-
await signInWithPasskey()
|
|
171
|
+
await signInWithPasskey(email)
|
|
164
172
|
} else if (!options.value.hasPassword) {
|
|
165
173
|
await requestOtp()
|
|
166
174
|
}
|
|
@@ -187,8 +195,12 @@ onMounted(async () => {
|
|
|
187
195
|
email.value = user.value?.email || (route.query.email as string) || ''
|
|
188
196
|
if (email.value) {
|
|
189
197
|
await getSignInOptions(email.value)
|
|
198
|
+
} else if (await isAutofillSupported()) {
|
|
199
|
+
signInWithPasskey(undefined, true)
|
|
190
200
|
}
|
|
191
201
|
})
|
|
202
|
+
|
|
203
|
+
onBeforeUnmount(() => WebAuthnAbortService.cancelCeremony())
|
|
192
204
|
</script>
|
|
193
205
|
|
|
194
206
|
<template>
|
|
@@ -286,8 +298,9 @@ onMounted(async () => {
|
|
|
286
298
|
type="button"
|
|
287
299
|
:disabled="isLoading"
|
|
288
300
|
class="flex-center g1 w mt1 bg bg-border"
|
|
289
|
-
@click="signInWithPasskey"
|
|
301
|
+
@click="signInWithPasskey(email)"
|
|
290
302
|
>
|
|
303
|
+
<Icon name="lucide:key-round" />
|
|
291
304
|
{{ $t('authCheck.passkey') }}
|
|
292
305
|
</button>
|
|
293
306
|
<button
|
|
@@ -319,7 +332,7 @@ onMounted(async () => {
|
|
|
319
332
|
type="email"
|
|
320
333
|
required
|
|
321
334
|
:disabled="isLoading"
|
|
322
|
-
autocomplete="username"
|
|
335
|
+
autocomplete="username webauthn"
|
|
323
336
|
/>
|
|
324
337
|
</label>
|
|
325
338
|
<button
|
|
@@ -329,10 +342,23 @@ onMounted(async () => {
|
|
|
329
342
|
{{ $t('continue') }}
|
|
330
343
|
</button>
|
|
331
344
|
</form>
|
|
332
|
-
<
|
|
345
|
+
<div
|
|
333
346
|
v-if="!optionsFetched"
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
class="flex-column"
|
|
348
|
+
>
|
|
349
|
+
<span class="text-center muted-text my1">{{ $t('or') }}</span>
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
:disabled="isLoading"
|
|
353
|
+
class="provider flex-center g2 w mb1 fg b"
|
|
354
|
+
style="font-weight: normal"
|
|
355
|
+
@click="signInWithPasskey()"
|
|
356
|
+
>
|
|
357
|
+
<Icon name="lucide:key-round" />
|
|
358
|
+
{{ $t('authCheck.passkey') }}
|
|
359
|
+
</button>
|
|
360
|
+
<slot name="footer"></slot>
|
|
361
|
+
</div>
|
|
336
362
|
<button
|
|
337
363
|
v-if="appStore.authPromise"
|
|
338
364
|
type="button"
|
|
@@ -352,4 +378,18 @@ onMounted(async () => {
|
|
|
352
378
|
box-sizing: border-box;
|
|
353
379
|
width: min(400px, 100vw - (var(--g-padding-l) + var(--g-padding-r)) * var(--g-padding-ratio2));
|
|
354
380
|
}
|
|
381
|
+
|
|
382
|
+
.provider,
|
|
383
|
+
:slotted(.provider) {
|
|
384
|
+
height: 40px;
|
|
385
|
+
border-radius: var(--g-border-radius);
|
|
386
|
+
font-size: 14px;
|
|
387
|
+
color: inherit;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.provider > :is(img, .iconify),
|
|
391
|
+
:slotted(.provider) > :is(img, .iconify) {
|
|
392
|
+
width: 20px;
|
|
393
|
+
height: 20px;
|
|
394
|
+
}
|
|
355
395
|
</style>
|
|
@@ -71,7 +71,6 @@ async function signInAnonymously() {
|
|
|
71
71
|
v-if="config.public.oauth.microsoft || config.public.anonymousSignup"
|
|
72
72
|
class="flex-column"
|
|
73
73
|
>
|
|
74
|
-
<span class="text-center muted-text my1">{{ $t('or') }}</span>
|
|
75
74
|
<a
|
|
76
75
|
v-if="config.public.oauth.microsoft"
|
|
77
76
|
href="/auth/microsoft"
|
|
@@ -80,8 +79,6 @@ async function signInAnonymously() {
|
|
|
80
79
|
<img
|
|
81
80
|
src="/images/auth/microsoft.svg"
|
|
82
81
|
alt=""
|
|
83
|
-
width="21"
|
|
84
|
-
height="21"
|
|
85
82
|
/>
|
|
86
83
|
{{ $t('signinWith') }} Microsoft
|
|
87
84
|
</a>
|
|
@@ -111,11 +108,4 @@ async function signInAnonymously() {
|
|
|
111
108
|
box-shadow: inset 0 0 40vh var(--g-fg);
|
|
112
109
|
height: 100dvh;
|
|
113
110
|
}
|
|
114
|
-
|
|
115
|
-
.provider {
|
|
116
|
-
height: 40px;
|
|
117
|
-
border-radius: var(--g-border-radius);
|
|
118
|
-
font-size: 14px;
|
|
119
|
-
color: inherit;
|
|
120
|
-
}
|
|
121
111
|
</style>
|
|
@@ -23,11 +23,10 @@ export default defineEventHandler(async (event) => {
|
|
|
23
23
|
const body = await readUploadBody(event, Number(useRuntimeConfig().public.files.chunkSize) || 1)
|
|
24
24
|
if (!body) throw createError({ status: 400, message: 'No data' })
|
|
25
25
|
|
|
26
|
-
const get = (name: string) =>
|
|
27
|
-
body
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.trim()
|
|
26
|
+
const get = (name: string) => {
|
|
27
|
+
const value = body.get(name)
|
|
28
|
+
return typeof value === 'string' ? value.trim() : undefined
|
|
29
|
+
}
|
|
31
30
|
|
|
32
31
|
const uploadId = get('uploadId')
|
|
33
32
|
const chunkIndex = Number(get('chunkIndex'))
|
|
@@ -38,9 +37,14 @@ export default defineEventHandler(async (event) => {
|
|
|
38
37
|
const isPrivate = get('isPrivate') !== 'false'
|
|
39
38
|
const parts = parseParts(get('parts'))
|
|
40
39
|
const maxAge = parseMaxAge(get('maxAge'))
|
|
41
|
-
const chunkPart = body.
|
|
42
|
-
|
|
43
|
-
if (
|
|
40
|
+
const chunkPart = body.get('chunk')
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
!Number.isInteger(chunkIndex) ||
|
|
44
|
+
!Number.isInteger(totalChunks) ||
|
|
45
|
+
!filename ||
|
|
46
|
+
!(chunkPart instanceof File)
|
|
47
|
+
) {
|
|
44
48
|
throw invalidChunkUpload()
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -50,7 +54,7 @@ export default defineEventHandler(async (event) => {
|
|
|
50
54
|
totalChunks,
|
|
51
55
|
filename,
|
|
52
56
|
type,
|
|
53
|
-
chunk: chunkPart.
|
|
57
|
+
chunk: Buffer.from(await chunkPart.arrayBuffer()),
|
|
54
58
|
path,
|
|
55
59
|
isPrivate,
|
|
56
60
|
parts,
|
|
@@ -4,20 +4,19 @@ export default defineEventHandler(async (event) => {
|
|
|
4
4
|
throw createError({ status: 400, message: 'No file uploaded' })
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.trim(),
|
|
16
|
-
)
|
|
7
|
+
const get = (name: string) => {
|
|
8
|
+
const value = body.get(name)
|
|
9
|
+
return typeof value === 'string' ? value.trim() : undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const isPrivate = body.has('isPrivate') ? get('isPrivate') === 'true' : true
|
|
13
|
+
const path = get('path')
|
|
14
|
+
const maxAge = parseMaxAge(get('maxAge'))
|
|
17
15
|
|
|
18
16
|
return await Promise.all(
|
|
19
17
|
body
|
|
20
|
-
.
|
|
18
|
+
.getAll('file')
|
|
19
|
+
.filter((file) => file instanceof File)
|
|
21
20
|
.map((file) => uploadFile(event, file, path, isPrivate, maxAge)),
|
|
22
21
|
)
|
|
23
22
|
})
|
|
@@ -3,12 +3,12 @@ import type { H3Event } from 'h3'
|
|
|
3
3
|
// Users reach their own folder u/{id}, admins every folder
|
|
4
4
|
export async function checkFileAccess(event: H3Event, path: string) {
|
|
5
5
|
const { user } = await requireUserSession(event)
|
|
6
|
-
if (!user) throw createError({
|
|
6
|
+
if (!user) throw createError({ status: 401, message: 'Unauthorized' })
|
|
7
7
|
const root = `u/${user.id}`
|
|
8
8
|
if (path.startsWith(root) && (path[root.length] === '/' || path.length === root.length)) {
|
|
9
9
|
return true
|
|
10
10
|
}
|
|
11
11
|
if (user.role === 'admin') return true
|
|
12
12
|
|
|
13
|
-
throw createError({
|
|
13
|
+
throw createError({ status: 403, message: 'Insufficient permissions' })
|
|
14
14
|
}
|
|
@@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => {
|
|
|
28
28
|
|
|
29
29
|
const path = event.path.split('?')[0]!
|
|
30
30
|
if (path.startsWith('/api/auth/otp/') || path === '/api/auth/password') return // re-authentication
|
|
31
|
-
if (ENROLLMENT_ROUTES.includes(path) && !(path === '/api/auth' && event
|
|
31
|
+
if (ENROLLMENT_ROUTES.includes(path) && !(path === '/api/auth' && !isMethod(event, 'GET'))) return
|
|
32
32
|
|
|
33
33
|
throw createError({ status: 403, message: 'MFA setup required' })
|
|
34
34
|
})
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export default defineEventHandler(async (event) => {
|
|
2
2
|
if (process.env.NODE_ENV === 'production') {
|
|
3
3
|
throw createError({
|
|
4
|
-
|
|
4
|
+
status: 404,
|
|
5
5
|
message: 'Not found',
|
|
6
6
|
})
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const templateId = event
|
|
9
|
+
const templateId = getRouterParam(event, 'template') || ''
|
|
10
10
|
const query = getQuery(event)
|
|
11
11
|
const locale = (query.locale as string) || 'en'
|
|
12
12
|
|
|
@@ -4,5 +4,5 @@ export async function isAdmin(event: H3Event) {
|
|
|
4
4
|
const { user } = await requireUserSession(event)
|
|
5
5
|
if (user.role === 'admin') return true
|
|
6
6
|
|
|
7
|
-
throw createError({
|
|
7
|
+
throw createError({ status: 403, message: 'Insufficient permissions' })
|
|
8
8
|
}
|
|
@@ -133,7 +133,7 @@ export async function requireRecentAuth(event: H3Event) {
|
|
|
133
133
|
if (!session.authenticatedAt || Date.now() - session.authenticatedAt > maxAge) {
|
|
134
134
|
throw createError({
|
|
135
135
|
status: 403,
|
|
136
|
-
|
|
136
|
+
statusText: 'reauth_required',
|
|
137
137
|
message: 'Please verify your identity again to continue.',
|
|
138
138
|
})
|
|
139
139
|
}
|
|
@@ -29,8 +29,8 @@ import type { CompletedPart } from '@aws-sdk/client-s3'
|
|
|
29
29
|
import { checkFileAccess } from '#database/access'
|
|
30
30
|
import { createReadStream, createWriteStream } from 'fs'
|
|
31
31
|
import { pipeline } from 'stream/promises'
|
|
32
|
-
import
|
|
33
|
-
import type { H3Event
|
|
32
|
+
import { Readable } from 'stream'
|
|
33
|
+
import type { H3Event } from 'h3'
|
|
34
34
|
import { createHash } from 'crypto'
|
|
35
35
|
import mime from 'mime'
|
|
36
36
|
|
|
@@ -38,33 +38,34 @@ const MB = 1024 * 1024
|
|
|
38
38
|
|
|
39
39
|
function fileTooLarge() {
|
|
40
40
|
const { maxSize } = useRuntimeConfig().public.files
|
|
41
|
-
return createError({
|
|
41
|
+
return createError({ status: 413, message: `File too large, max ${maxSize} MB` })
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export async function readUploadBody(event: H3Event, maxMB: number) {
|
|
45
45
|
await requireUserSession(event)
|
|
46
46
|
const length = getRequestHeader(event, 'content-length')
|
|
47
|
-
if (!length) throw createError({
|
|
47
|
+
if (!length) throw createError({ status: 411, message: 'Content-Length required' })
|
|
48
48
|
if (Number(length) > (maxMB + 1) * MB) throw fileTooLarge()
|
|
49
|
-
return
|
|
49
|
+
return readFormData(event).catch(() => undefined)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export async function uploadFile(
|
|
53
53
|
event: H3Event,
|
|
54
|
-
file:
|
|
54
|
+
file: File,
|
|
55
55
|
path = 'files',
|
|
56
56
|
isPrivate = true,
|
|
57
57
|
maxAge?: number, // only stored on S3 objects, local files get their headers from nginx
|
|
58
58
|
) {
|
|
59
59
|
await checkFileAccess(event, path)
|
|
60
|
-
path = getUploadPath(path, isPrivate, file.
|
|
60
|
+
path = getUploadPath(path, isPrivate, file.name)
|
|
61
|
+
const data = Buffer.from(await file.arrayBuffer())
|
|
61
62
|
|
|
62
63
|
let url
|
|
63
64
|
if (useS3()) {
|
|
64
65
|
const s3Key = getS3Key(path)
|
|
65
66
|
url = await uploadToS3(
|
|
66
67
|
s3Key,
|
|
67
|
-
|
|
68
|
+
data,
|
|
68
69
|
file.type || 'application/octet-stream',
|
|
69
70
|
getCacheControl(isPrivate, maxAge),
|
|
70
71
|
isPrivate,
|
|
@@ -73,7 +74,7 @@ export async function uploadFile(
|
|
|
73
74
|
const filePath = join(process.cwd(), path)
|
|
74
75
|
const folderPath = filePath.replace(/\/[^/]*$/, '')
|
|
75
76
|
await mkdir(folderPath, { recursive: true })
|
|
76
|
-
await writeFile(filePath,
|
|
77
|
+
await writeFile(filePath, data)
|
|
77
78
|
|
|
78
79
|
url = getFileURL(path, isPrivate)
|
|
79
80
|
}
|
|
@@ -149,7 +150,7 @@ export async function uploadChunk(
|
|
|
149
150
|
},
|
|
150
151
|
) {
|
|
151
152
|
if (chunkIndex < 0 || chunkIndex >= totalChunks || totalChunks < 1) {
|
|
152
|
-
throw createError({
|
|
153
|
+
throw createError({ status: 400, message: 'Invalid chunk index' })
|
|
153
154
|
}
|
|
154
155
|
const { chunkSize, maxSize } = useRuntimeConfig().public.files
|
|
155
156
|
const chunkBytes = (Number(chunkSize) || 1) * MB
|
|
@@ -186,7 +187,7 @@ export async function uploadChunk(
|
|
|
186
187
|
uploadId = crypto.randomUUID()
|
|
187
188
|
parts = []
|
|
188
189
|
} else if (!isUUID(uploadId)) {
|
|
189
|
-
throw createError({
|
|
190
|
+
throw createError({ status: 400, message: 'Invalid upload id' })
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
const tmpDir = join(process.cwd(), 'files', 'temp', uploadId)
|
|
@@ -335,10 +336,10 @@ export async function getFile(
|
|
|
335
336
|
if (response.status === 416) {
|
|
336
337
|
const contentRange = response.headers.get('content-range')
|
|
337
338
|
if (contentRange) setHeader(event, 'Content-Range', contentRange)
|
|
338
|
-
throw createError({
|
|
339
|
+
throw createError({ status: 416, message: 'Range not satisfiable' })
|
|
339
340
|
}
|
|
340
|
-
if (response.status === 404) throw createError({
|
|
341
|
-
if (!response.ok) throw createError({
|
|
341
|
+
if (response.status === 404) throw createError({ status: 404, message: 'File not found' })
|
|
342
|
+
if (!response.ok) throw createError({ status: 502, message: 'Failed to fetch file' })
|
|
342
343
|
|
|
343
344
|
const passthrough = {
|
|
344
345
|
'Content-Type': response.headers.get('content-type'),
|
|
@@ -358,7 +359,7 @@ export async function getFile(
|
|
|
358
359
|
|
|
359
360
|
const stats = await stat(fullPath).catch(() => null)
|
|
360
361
|
if (!stats?.isFile()) {
|
|
361
|
-
throw createError({
|
|
362
|
+
throw createError({ status: 404, message: 'File not found' })
|
|
362
363
|
}
|
|
363
364
|
|
|
364
365
|
setHeader(event, 'Content-Type', mime.getType(fullPath) || 'application/octet-stream')
|
|
@@ -367,7 +368,7 @@ export async function getFile(
|
|
|
367
368
|
const parsedRange = parseRangeHeader(range, stats.size)
|
|
368
369
|
if (parsedRange === 'unsatisfiable') {
|
|
369
370
|
setHeader(event, 'Content-Range', `bytes */${stats.size}`)
|
|
370
|
-
throw createError({
|
|
371
|
+
throw createError({ status: 416, message: 'Range not satisfiable' })
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
if (parsedRange) {
|
|
@@ -375,11 +376,12 @@ export async function getFile(
|
|
|
375
376
|
setResponseStatus(event, 206)
|
|
376
377
|
setHeader(event, 'Content-Range', `bytes ${start}-${end}/${stats.size}`)
|
|
377
378
|
setHeader(event, 'Content-Length', end - start + 1)
|
|
378
|
-
return createReadStream(fullPath, { start, end })
|
|
379
|
+
return Readable.toWeb(createReadStream(fullPath, { start, end }))
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
setHeader(event, 'Content-Length', stats.size)
|
|
382
|
-
|
|
383
|
+
// h3 v2 only streams web streams
|
|
384
|
+
return Readable.toWeb(createReadStream(fullPath))
|
|
383
385
|
}
|
|
384
386
|
|
|
385
387
|
export async function deletePath(event: H3Event, path: string, isPrivate = true) {
|
|
@@ -467,7 +469,7 @@ export async function copyFile(
|
|
|
467
469
|
const destPath = join(process.cwd(), dest)
|
|
468
470
|
const stats = await stat(srcPath).catch(() => null)
|
|
469
471
|
if (!stats?.isFile()) {
|
|
470
|
-
throw createError({
|
|
472
|
+
throw createError({ status: 404, message: 'File not found' })
|
|
471
473
|
}
|
|
472
474
|
await mkdir(dirname(destPath), { recursive: true })
|
|
473
475
|
await fsCopyFile(srcPath, destPath)
|
|
@@ -485,7 +487,7 @@ export async function copyFiles(
|
|
|
485
487
|
deleteSrc = false,
|
|
486
488
|
) {
|
|
487
489
|
const files = await listFolder(event, src, isPrivate)
|
|
488
|
-
if (!files.length) throw createError({
|
|
490
|
+
if (!files.length) throw createError({ status: 404, message: 'File not found' })
|
|
489
491
|
const normalizedSrc = removeRoot(getSecurePath(src, isPrivate), isPrivate)
|
|
490
492
|
const normalizedDest = removeRoot(getSecurePath(dest, isPrivate), isPrivate)
|
|
491
493
|
return Promise.all(
|
|
@@ -519,7 +521,7 @@ export function getSecurePath(path: string, isPrivate = true) {
|
|
|
519
521
|
}
|
|
520
522
|
|
|
521
523
|
function invalidPath() {
|
|
522
|
-
return createError({
|
|
524
|
+
return createError({ status: 400, message: 'Invalid path' })
|
|
523
525
|
}
|
|
524
526
|
|
|
525
527
|
function getUploadPath(path: string, isPrivate: boolean, filename?: string) {
|
|
@@ -583,7 +585,7 @@ export function parseMaxAge(value?: string) {
|
|
|
583
585
|
if (!value) return undefined
|
|
584
586
|
const maxAge = Number(value)
|
|
585
587
|
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
586
|
-
throw createError({
|
|
588
|
+
throw createError({ status: 400, message: 'Invalid maxAge' })
|
|
587
589
|
}
|
|
588
590
|
return maxAge
|
|
589
591
|
}
|
|
@@ -788,7 +790,7 @@ export async function deleteFromS3(path: string, isPrivate = true) {
|
|
|
788
790
|
const result = await client.send(command)
|
|
789
791
|
if (result.Errors?.length) {
|
|
790
792
|
throw createError({
|
|
791
|
-
|
|
793
|
+
status: 502,
|
|
792
794
|
message: `Failed to delete ${result.Errors.length} file(s): ${result.Errors.map((e) => `${e.Key} (${e.Code})`).join(', ')}`,
|
|
793
795
|
})
|
|
794
796
|
}
|
package/locales/en.json
CHANGED
package/locales/fr.json
CHANGED