release: v1.1.2 — activity logs, storage filter, upload rate limits
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s

Bump version and changelog; fix lint issues before release.
This commit is contained in:
O96u committed 2026-08-26 17:14:50 +08:00
1 parent 6fc054328d
commit 55effa204e
9 files changed
+161 -141

No files matched your search

+20 -1
View File
@@ -2,6 +2,24 @@
本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。 本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。
## [1.1.2] - 2026-08-26
### 新增
- 操作日志页(`/logs`):记录上传/删除,管理员可查看全部用户并筛选
- 图库记录支持按存储后端筛选
- 上传 IP / API Token 限流(15 分钟窗口)
### 改进
- 操作日志记录存储后端;图库卡片可展示存储来源
- 存储筛选下拉固定「本地磁盘」排在第二位
- 系统设置页版本号链接至 GitHub Releases
### 修复
- 修复删除存储后端后确认弹窗闪出空名称
## [1.1.1] - 2026-08-26 ## [1.1.1] - 2026-08-26
### 修复 ### 修复
@@ -95,8 +113,9 @@
### 路线图 ### 路线图
- **v1.1.1**(当前 `main`):会话校验与存储占位修复、Docker 日志增强 - **v1.1.2**(当前 `main`):操作日志、图库存储筛选、上传限流
[1.1.2]: https://github.com/O96u/PicHost/releases/tag/v1.1.2
[1.1.1]: https://github.com/O96u/PicHost/releases/tag/v1.1.1 [1.1.1]: https://github.com/O96u/PicHost/releases/tag/v1.1.1
[1.1.0]: https://github.com/O96u/PicHost/releases/tag/v1.1.0 [1.1.0]: https://github.com/O96u/PicHost/releases/tag/v1.1.0
[1.0.4]: https://github.com/O96u/PicHost/releases/tag/v1.0.4 [1.0.4]: https://github.com/O96u/PicHost/releases/tag/v1.0.4
+3 -2
View File
@@ -32,7 +32,7 @@
| Branch | Notes | | Branch | Notes |
| ------ | ----- | | ------ | ----- |
| [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.1** — local disk + multi-backend object storage (R2 / COS / OSS / AWS), [`/storage`](docs/README.md) UI, hybrid URLs | | [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.2** — local disk + multi-backend object storage (R2 / COS / OSS / AWS), [`/storage`](docs/README.md) UI, hybrid URLs |
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2–only line** — streamlined for deployments that use R2 exclusively | | [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2–only line** — streamlined for deployments that use R2 exclusively |
Use **main** for the full product and multiple cloud backends. Use **main** for the full product and multiple cloud backends.
@@ -193,7 +193,8 @@ npm test # Unit tests
| Version | Plan | | Version | Plan |
| ---------- | -------------------------------------------------------------- | | ---------- | -------------------------------------------------------------- |
| **v1.1.1** | Session check fixes, storage placeholder cleanup, Docker logging (current `main`) | | **v1.1.2** | Activity logs, gallery storage filter, upload rate limits (current `main`) |
| **v1.1.1** | Session check fixes, storage placeholder cleanup, Docker logging |
| **v1.1.0** | Multi-backend object storage, hybrid URLs, storage UI | | **v1.1.0** | Multi-backend object storage, hybrid URLs, storage UI |
| **v1.0.4** | Password visibility toggle, legacy ADMIN_SECRET migration | | **v1.0.4** | Password visibility toggle, legacy ADMIN_SECRET migration |
| **v1.0.3** | CI and Docker multi-arch build fixes | | **v1.0.3** | CI and Docker multi-arch build fixes |
+3 -2
View File
@@ -32,7 +32,7 @@
| 分支 | 说明 | | 分支 | 说明 |
| ---- | ---- | | ---- | ---- |
| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.1** 主线:本地磁盘 + 多对象存储后端(R2 / COS / OSS / AWS)、[`/storage`](docs/README.md) 存储管理、混合直链 | | [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.2** 主线:本地磁盘 + 多对象存储后端(R2 / COS / OSS / AWS)、[`/storage`](docs/README.md) 存储管理、混合直链 |
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2 专用线**:面向「只用 R2」的部署,预设与配置更聚焦 R2 | | [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2 专用线**:面向「只用 R2」的部署,预设与配置更聚焦 R2 |
日常使用、需要管理多种存储后端,请直接用 **main**。 日常使用、需要管理多种存储后端,请直接用 **main**。
@@ -198,7 +198,8 @@ npm test # 单元测试
| 版本 | 说明 | | 版本 | 说明 |
| ---------- | ------------------------------------------------- | | ---------- | ------------------------------------------------- |
| **v1.1.1** | 会话校验与存储占位修复、Docker 日志增强(当前 main) | | **v1.1.2** | 操作日志、图库存储筛选、上传限流(当前 main) |
| **v1.1.1** | 会话校验与存储占位修复、Docker 日志增强 |
| **v1.1.0** | S3 / R2 多后端对象存储、混合直链、存储管理页 | | **v1.1.0** | S3 / R2 多后端对象存储、混合直链、存储管理页 |
| **v1.0.4** | 密码显示切换、遗留 ADMIN_SECRET 迁移对齐 | | **v1.0.4** | 密码显示切换、遗留 ADMIN_SECRET 迁移对齐 |
| **v1.0.3** | CI 与 Docker 多架构构建修复 | | **v1.0.3** | CI 与 Docker 多架构构建修复 |
+127 -127
View File
@@ -426,138 +426,138 @@ watch(isAuthenticated, async (authed, prev) => {
</div> </div>
<div class="hidden overflow-x-auto sm:block"> <div class="hidden overflow-x-auto sm:block">
<table class="min-w-full text-left text-sm"> <table class="min-w-full text-left text-sm">
<thead class="border-b border-default bg-muted/30 text-xs text-muted"> <thead class="border-b border-default bg-muted/30 text-xs text-muted">
<tr> <tr>
<th class="px-4 py-3 font-medium sm:px-6"> <th class="px-4 py-3 font-medium sm:px-6">
{{ t('logs.colTime') }} {{ t('logs.colTime') }}
</th> </th>
<th <th
v-if="isAdmin" v-if="isAdmin"
class="px-4 py-3 font-medium" class="px-4 py-3 font-medium"
>
{{ t('logs.colUser') }}
</th>
<th class="px-4 py-3 font-medium">
{{ t('logs.colAction') }}
</th>
<th class="px-4 py-3 font-medium">
{{ t('logs.colSource') }}
</th>
<th class="px-4 py-3 font-medium">
{{ t('logs.colStorage') }}
</th>
<th class="hidden px-4 py-3 font-medium md:table-cell">
{{ t('logs.colFile') }}
</th>
<th class="hidden px-4 py-3 font-medium lg:table-cell">
{{ t('logs.colFolder') }}
</th>
<th class="px-4 py-3 font-medium">
{{ t('logs.colSize') }}
</th>
<th class="hidden px-4 py-3 font-medium xl:table-cell">
{{ t('logs.colKey') }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-default">
<tr
v-for="row in listData.items"
:key="row.id"
class="hover:bg-muted/20"
>
<td class="whitespace-nowrap px-4 py-3 text-xs text-muted sm:px-6">
{{ formatTime(row.createdAt) }}
</td>
<td
v-if="isAdmin"
class="px-4 py-3 text-xs"
>
{{ row.username ?? '—' }}
</td>
<td class="px-4 py-3">
<UBadge
:color="actionColor(row.action)"
variant="subtle"
size="xs"
> >
<UIcon {{ t('logs.colUser') }}
:name="actionIcon(row.action)" </th>
class="mr-1 size-3" <th class="px-4 py-3 font-medium">
/> {{ t('logs.colAction') }}
{{ row.action === 'upload' ? t('logs.actionUpload') : t('logs.actionDelete') }} </th>
</UBadge> <th class="px-4 py-3 font-medium">
</td> {{ t('logs.colSource') }}
<td class="px-4 py-3"> </th>
<UBadge <th class="px-4 py-3 font-medium">
color="neutral" {{ t('logs.colStorage') }}
variant="subtle" </th>
size="xs" <th class="hidden px-4 py-3 font-medium md:table-cell">
> {{ t('logs.colFile') }}
{{ sourceLabel(row.source) }} </th>
</UBadge> <th class="hidden px-4 py-3 font-medium lg:table-cell">
</td> {{ t('logs.colFolder') }}
<td class="px-4 py-3 text-xs"> </th>
<span <th class="px-4 py-3 font-medium">
v-if="row.storage" {{ t('logs.colSize') }}
class="inline-flex max-w-[8rem] items-center gap-1 truncate" </th>
:title="row.storage.name" <th class="hidden px-4 py-3 font-medium xl:table-cell">
> {{ t('logs.colKey') }}
<UIcon </th>
:name="storageIcon(row.storage)" </tr>
class="size-3.5 shrink-0 opacity-70" </thead>
/> <tbody class="divide-y divide-default">
<span class="truncate">{{ row.storage.name }}</span> <tr
</span> v-for="row in listData.items"
<span :key="row.id"
v-else class="hover:bg-muted/20"
class="text-muted"
>—</span>
</td>
<td
class="hidden max-w-[12rem] truncate px-4 py-3 md:table-cell"
:title="row.originalName"
> >
{{ row.originalName }} <td class="whitespace-nowrap px-4 py-3 text-xs text-muted sm:px-6">
</td> {{ formatTime(row.createdAt) }}
<td class="hidden px-4 py-3 font-mono text-xs text-muted lg:table-cell"> </td>
{{ folderFromKey(row.key) }} <td
</td> v-if="isAdmin"
<td class="whitespace-nowrap px-4 py-3 tabular-nums text-muted"> class="px-4 py-3 text-xs"
{{ formatFileSize(row.size) }} >
</td> {{ row.username ?? '—' }}
<td class="hidden px-4 py-3 xl:table-cell"> </td>
<div class="flex max-w-[16rem] items-center gap-1"> <td class="px-4 py-3">
<a <UBadge
v-if="row.action === 'upload'" :color="actionColor(row.action)"
:href="imageUrl(row.key)" variant="subtle"
target="_blank" size="xs"
rel="noopener noreferrer"
class="truncate font-mono text-xs text-primary hover:underline"
:title="row.key"
> >
{{ row.key }} <UIcon
</a> :name="actionIcon(row.action)"
class="mr-1 size-3"
/>
{{ row.action === 'upload' ? t('logs.actionUpload') : t('logs.actionDelete') }}
</UBadge>
</td>
<td class="px-4 py-3">
<UBadge
color="neutral"
variant="subtle"
size="xs"
>
{{ sourceLabel(row.source) }}
</UBadge>
</td>
<td class="px-4 py-3 text-xs">
<span
v-if="row.storage"
class="inline-flex max-w-[8rem] items-center gap-1 truncate"
:title="row.storage.name"
>
<UIcon
:name="storageIcon(row.storage)"
class="size-3.5 shrink-0 opacity-70"
/>
<span class="truncate">{{ row.storage.name }}</span>
</span>
<span <span
v-else v-else
class="truncate font-mono text-xs text-dimmed" class="text-muted"
:title="row.key" >—</span>
> </td>
{{ row.key }} <td
</span> class="hidden max-w-[12rem] truncate px-4 py-3 md:table-cell"
<CopyButton :title="row.originalName"
icon="i-lucide-copy" >
icon-only {{ row.originalName }}
:label="t('common.copy')" </td>
:value="row.key" <td class="hidden px-4 py-3 font-mono text-xs text-muted lg:table-cell">
:success-title="t('copy.copiedUrl')" {{ folderFromKey(row.key) }}
/> </td>
</div> <td class="whitespace-nowrap px-4 py-3 tabular-nums text-muted">
</td> {{ formatFileSize(row.size) }}
</tr> </td>
</tbody> <td class="hidden px-4 py-3 xl:table-cell">
</table> <div class="flex max-w-[16rem] items-center gap-1">
<a
v-if="row.action === 'upload'"
:href="imageUrl(row.key)"
target="_blank"
rel="noopener noreferrer"
class="truncate font-mono text-xs text-primary hover:underline"
:title="row.key"
>
{{ row.key }}
</a>
<span
v-else
class="truncate font-mono text-xs text-dimmed"
:title="row.key"
>
{{ row.key }}
</span>
<CopyButton
icon="i-lucide-copy"
icon-only
:label="t('common.copy')"
:value="row.key"
:success-title="t('copy.copiedUrl')"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div> </div>
</template> </template>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pichost", "name": "pichost",
"version": "1.1.1", "version": "1.1.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pichost", "name": "pichost",
"version": "1.1.1", "version": "1.1.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pichost", "name": "pichost",
"version": "1.1.1", "version": "1.1.2",
"description": "Self-hosted image hosting for your NAS / home server — Nuxt admin UI, local disk or S3-compatible storage, server-side WebP compression, referer protection, Twikoo/EasyImage compatible.", "description": "Self-hosted image hosting for your NAS / home server — Nuxt admin UI, local disk or S3-compatible storage, server-side WebP compression, referer protection, Twikoo/EasyImage compatible.",
"homepage": "https://github.com/O96u/PicHost#readme", "homepage": "https://github.com/O96u/PicHost#readme",
"repository": { "repository": {
+2 -2
View File
@@ -93,8 +93,8 @@ export default defineEventHandler(async (event) => {
summary, summary,
users: userMap users: userMap
? Array.from(userMap.entries()) ? Array.from(userMap.entries())
.map(([id, username]) => ({ id, username })) .map(([id, username]) => ({ id, username }))
.sort((a, b) => a.username.localeCompare(b.username)) .sort((a, b) => a.username.localeCompare(b.username))
: undefined : undefined
} }
} catch (error) { } catch (error) {
+1 -2
View File
@@ -5,9 +5,8 @@ import type { StoredImage } from './storage'
import { getImageBaseUrl } from './env' import { getImageBaseUrl } from './env'
import { getActiveBackendRow } from './storage/resolver' import { getActiveBackendRow } from './storage/resolver'
import { getStorageBackendRow, getStorageBackendFromEnv } from './storage-backends' import { getStorageBackendRow, getStorageBackendFromEnv } from './storage-backends'
import type { StorageBackendType } from './storage/types' import type { StorageBackendType, StorageBackendRow } from './storage/types'
import { buildObjectKey, parseS3Config } from './storage/s3' import { buildObjectKey, parseS3Config } from './storage/s3'
import type { StorageBackendRow } from './storage/types'
export function resolveImageOwner( export function resolveImageOwner(
userId: number | null | undefined, userId: number | null | undefined,
+2 -2
View File
@@ -1,11 +1,11 @@
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { checkUploadRateLimit } from './rate-limit'
vi.mock('./logger', () => ({ vi.mock('./logger', () => ({
clientIp: (event: { __ip?: string }) => event.__ip ?? '127.0.0.1' clientIp: (event: { __ip?: string }) => event.__ip ?? '127.0.0.1'
})) }))
import { checkUploadRateLimit } from './rate-limit'
function mockEvent(ip = '127.0.0.1', authToken = '') { function mockEvent(ip = '127.0.0.1', authToken = '') {
return { return {
__ip: ip, __ip: ip,