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:
O96u committed 2026-08-26 17:09:38 +08:00
1 parent 901fefb3f5
commit 6fc054328d
34 files changed
+1357 -72

No files matched your search

+5
View File
@@ -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',
+23 -1
View File
@@ -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
+3
View File
@@ -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)"
+18
View File
@@ -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"
+18 -3
View File
@@ -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"