feat: activity logs, gallery storage filter, and upload rate limits
Add per-user activity logs with storage backend tracking, gallery storage filter, and IP/token upload rate limiting. Fix storage delete modal flash, pin local backend in filter order, and link settings version to GitHub releases.
This commit is contained in:
1 parent
901fefb3f5
commit
6fc054328d
34 files changed
+1357
-72
No files matched your search
@@ -23,6 +23,11 @@ const navItems = computed(() => {
|
||||
})
|
||||
|
||||
const accountMenuItems = computed<DropdownMenuItem[]>(() => [
|
||||
{
|
||||
label: t('nav.logs'),
|
||||
icon: 'i-lucide-scroll-text',
|
||||
to: '/logs'
|
||||
},
|
||||
{
|
||||
label: t('nav.changePassword'),
|
||||
icon: 'i-lucide-key-round',
|
||||
|
||||
@@ -9,10 +9,12 @@ const props = withDefaults(defineProps<{
|
||||
deleting?: boolean
|
||||
selectable?: boolean
|
||||
showKey?: boolean
|
||||
showStorage?: boolean
|
||||
compact?: boolean
|
||||
}>(), {
|
||||
selectable: true,
|
||||
showKey: true,
|
||||
showStorage: false,
|
||||
compact: false
|
||||
})
|
||||
|
||||
@@ -62,6 +64,10 @@ const copySuccessTitle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const storageIcon = computed(() =>
|
||||
props.image.storage?.type === 'local' ? 'i-lucide-hard-drive' : 'i-lucide-cloud'
|
||||
)
|
||||
|
||||
function toggleSelected(value: boolean | 'indeterminate') {
|
||||
emit('update:selected', value === true)
|
||||
}
|
||||
@@ -106,7 +112,7 @@ function toggleSelected(value: boolean | 'indeterminate') {
|
||||
</div>
|
||||
<div
|
||||
v-if="image.owner"
|
||||
class="absolute bottom-2 left-2 max-w-[calc(100%-1rem)]"
|
||||
class="absolute bottom-2 left-2 max-w-[calc(50%-0.5rem)]"
|
||||
>
|
||||
<span
|
||||
class="inline-flex max-w-full items-center gap-1 rounded-md bg-black/65 px-2 py-0.5 text-xs font-medium text-white shadow-sm backdrop-blur-sm"
|
||||
@@ -119,6 +125,22 @@ function toggleSelected(value: boolean | 'indeterminate') {
|
||||
<span class="truncate">{{ image.owner.username }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="showStorage && image.storage"
|
||||
class="absolute bottom-2 max-w-[calc(50%-0.5rem)]"
|
||||
:class="image.owner ? 'right-2' : 'left-2'"
|
||||
>
|
||||
<span
|
||||
class="inline-flex max-w-full items-center gap-1 rounded-md bg-black/65 px-2 py-0.5 text-xs font-medium text-white shadow-sm backdrop-blur-sm"
|
||||
:title="t('image.storageBackend', { name: image.storage.name })"
|
||||
>
|
||||
<UIcon
|
||||
:name="storageIcon"
|
||||
class="size-3 shrink-0"
|
||||
/>
|
||||
<span class="truncate">{{ image.storage.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -7,12 +7,14 @@ const props = withDefaults(defineProps<{
|
||||
deletingKeys: Set<string>
|
||||
selectable?: boolean
|
||||
showKey?: boolean
|
||||
showStorage?: boolean
|
||||
emptyText?: string
|
||||
dense?: boolean
|
||||
compact?: boolean
|
||||
}>(), {
|
||||
selectable: true,
|
||||
showKey: true,
|
||||
showStorage: false,
|
||||
dense: false,
|
||||
compact: false
|
||||
})
|
||||
@@ -48,6 +50,7 @@ function updateSelection(key: string, selected: boolean) {
|
||||
:deleting="deletingKeys.has(image.key)"
|
||||
:selectable="selectable"
|
||||
:show-key="showKey"
|
||||
:show-storage="showStorage"
|
||||
:compact="compact"
|
||||
@update:selected="updateSelection(image.key, $event)"
|
||||
@delete="emit('delete', image.key)"
|
||||
|
||||
@@ -136,6 +136,24 @@ function toggleOpen() {
|
||||
{{ t('nav.account') }}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<NuxtLink
|
||||
to="/logs"
|
||||
class="flex items-center justify-between rounded-lg border border-default px-3.5 py-2.5 text-sm text-highlighted transition-colors hover:bg-elevated"
|
||||
@click="open = false"
|
||||
>
|
||||
<span class="flex items-center gap-2.5">
|
||||
<UIcon
|
||||
name="i-lucide-scroll-text"
|
||||
class="size-4 opacity-70"
|
||||
/>
|
||||
{{ t('nav.logs') }}
|
||||
</span>
|
||||
<UIcon
|
||||
name="i-lucide-chevron-right"
|
||||
class="size-4 text-muted"
|
||||
/>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
to="/account/password"
|
||||
class="flex items-center justify-between rounded-lg border border-default px-3.5 py-2.5 text-sm text-highlighted transition-colors hover:bg-elevated"
|
||||
|
||||
@@ -216,7 +216,10 @@ function handleSubmit() {
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.bucket') }}</label>
|
||||
<label class="text-sm">
|
||||
{{ t('storage.bucket') }}
|
||||
<span class="text-error">*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="bucket"
|
||||
class="w-full font-mono text-sm"
|
||||
@@ -241,7 +244,13 @@ function handleSubmit() {
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.accessKey') }}</label>
|
||||
<label class="text-sm">
|
||||
{{ t('storage.accessKey') }}
|
||||
<span
|
||||
v-if="createMode"
|
||||
class="text-error"
|
||||
>*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="accessKeyId"
|
||||
:placeholder="backend?.secretsMasked.accessKeyId || ''"
|
||||
@@ -250,7 +259,13 @@ function handleSubmit() {
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.secretKey') }}</label>
|
||||
<label class="text-sm">
|
||||
{{ t('storage.secretKey') }}
|
||||
<span
|
||||
v-if="createMode"
|
||||
class="text-error"
|
||||
>*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="secretAccessKey"
|
||||
type="password"
|
||||
|
||||
Reference in new issue
Block a user