Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ per module is tracked in the API refactor plan.

Tracked in `docs/plans/2026-05-19-api-refactor-design.md`. Snapshot:

1. **snake_case data model leak** — `Config.config_key/config_value`, `Album.album_value`, `Image.image_name`, `image_sorting`, `show_on_mainpage`, `Exif.data_time` are exposed in API responses and request bodies. Server-side mapping layer pending. (PR-07, PR-08, PR-09.)
1. **snake_case data model leak** — `Album.album_value`, `Image.image_name`, `image_sorting`, `show_on_mainpage` (PR-09) and `Exif.data_time` (PR-08) and the settings `Config.config_key/config_value` shape (PR-07) have been migrated to camelCase at the API boundary. Remaining snake_case fields still surfaced through `ImageType` / `AlbumType` (e.g. `preview_url`, `video_url`, `album_name`, `album_license`, `random_show`, `exposure_time`, `f_number`, `daily_weight`) and through the on-disk backup format are intentionally left for follow-up PRs — they are not part of the PR-07/08/09 scope.
2. **`/api/public/download/:id` dual return type** — Returns binary blob OR `{ url, filename }` JSON depending on direct-download config. To be split into `/download/:id` (binary) and `/download/:id/presigned` (JSON envelope). (PR-01.)
3. ~~`/api/public/images/image-blob` SSRF~~ — **DONE (PR-02):** Endpoint deleted. No in-repo consumers existed.
4. ~~`/api/v1/images/camera-lens-list` & `/api/public/camera-lens-list`~~ — **DONE (PR-04):** Both endpoints consolidated under Hono. Public moved to `GET /api/public/camera-lens` and reuses `fetchClientCameraAndLensList` / `fetchDailyCameraAndLensList`, which filter to `show=0` (visible) images and, for album-scoped requests, `albums.show=0` as well. Admin remains at `GET /api/v1/images/camera-lens-list` and returns the unfiltered set.
Expand Down
2 changes: 1 addition & 1 deletion app/(theme)/[...album]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function Page({
}) {
const { album } = await params

const data: AlbumType = await fetchAlbumByRouter(`/${album}`)
const data: AlbumType | null = await fetchAlbumByRouter(`/${album}`)

const props: ImageHandleProps = {
handle: getImagesData,
Expand Down
4 changes: 2 additions & 2 deletions app/admin/settings/daily/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function DailySettings() {
const [totalCount, setTotalCount] = useState('30')
const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [albumWeights, setAlbumWeights] = useState<Array<{ id: string, name: string, album_value: string, daily_weight: number, photo_count: number }>>([])
const [albumWeights, setAlbumWeights] = useState<Array<{ id: string, name: string, albumValue: string, daily_weight: number, photo_count: number }>>([])
const t = useTranslations()

const { data: configData, isValidating: configValidating, isLoading: configLoading, mutate: mutateConfig } = useSWR<DailyConfig>('/api/v1/daily/config', fetcher)
Expand All @@ -44,7 +44,7 @@ export default function DailySettings() {

useEffect(() => {
if (albumsData) {
setAlbumWeights(albumsData.map((a: { id: string, name: string, album_value: string, daily_weight: number, photo_count: number }) => ({ ...a, daily_weight: Number(a.daily_weight) })))
setAlbumWeights(albumsData.map((a: { id: string, name: string, albumValue: string, daily_weight: number, photo_count: number }) => ({ ...a, daily_weight: Number(a.daily_weight) })))
}
}, [albumsData])

Expand Down
4 changes: 2 additions & 2 deletions app/rss.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export async function GET(request: Request) {
<div>
<img src="${item.preview_url || item.url}" alt="${item.detail}" />
<p>${item.detail}</p>
<a href="${url.origin + (item.album_value === '/' ? '/preview/' : item.album_value + '/preview/') + item.id}" target="_blank">查看图片信息</a>
<a href="${url.origin + (item.albumValue === '/' ? '/preview/' : item.albumValue + '/preview/') + item.id}" target="_blank">查看图片信息</a>
</div>
`,
url: url.origin + (item.album_value === '/' ? '/preview/' : item.album_value + '/preview/') + item.id,
url: url.origin + (item.albumValue === '/' ? '/preview/' : item.albumValue + '/preview/') + item.id,
guid: item.id,
date: item.created_at,
enclosure: {
Expand Down
12 changes: 6 additions & 6 deletions components/admin/album/album-add-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export default function AlbumAddSheet(props : Readonly<HandleProps>) {
const t = useTranslations()

async function submit() {
if (!data.name || !data.album_value) {
if (!data.name || !data.albumValue) {
toast.error(t('Album.requiredFields'))
return
}
if (data.album_value && data.album_value.charAt(0) !== '/') {
if (data.albumValue && data.albumValue.charAt(0) !== '/') {
toast.error(t('Album.routerStartWithSlash'))
return
}
Expand Down Expand Up @@ -92,9 +92,9 @@ export default function AlbumAddSheet(props : Readonly<HandleProps>) {
<input
type="text"
id="album_value"
value={data?.album_value}
value={data?.albumValue}
placeholder={t('Album.inputRouter')}
onChange={(e) => setData({...data, album_value: e.target.value})}
onChange={(e) => setData({...data, albumValue: e.target.value})}
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
Expand Down Expand Up @@ -189,11 +189,11 @@ export default function AlbumAddSheet(props : Readonly<HandleProps>) {
<div className="flex flex-col gap-1 rounded-lg border p-3 shadow-sm">
<div className="text-medium">{t('Album.imageSortRule')}</div>
<Select
value={typeof data.image_sorting === 'number' ? data.image_sorting.toString() : '1'}
value={typeof data.imageSorting === 'number' ? data.imageSorting.toString() : '1'}
onValueChange={(value) => {
setData({
...data,
image_sorting: parseInt(value),
imageSorting: parseInt(value),
})
}}
>
Expand Down
12 changes: 6 additions & 6 deletions components/admin/album/album-edit-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ export default function AlbumEditSheet(props : Readonly<HandleProps>) {
}, [album])

async function submit() {
if (!data.name || !data.album_value) {
if (!data.name || !data.albumValue) {
toast.error(t('Album.requiredFields'))
return
}
if (data.album_value && data.album_value.charAt(0) !== '/') {
if (data.albumValue && data.albumValue.charAt(0) !== '/') {
toast.error(t('Album.routerStartWithSlash'))
return
}
Expand Down Expand Up @@ -96,9 +96,9 @@ export default function AlbumEditSheet(props : Readonly<HandleProps>) {
<input
type="text"
id="album_value"
value={data?.album_value || ''}
value={data?.albumValue || ''}
placeholder={t('Album.inputRouter')}
onChange={(e) => setData({...data, album_value: e.target.value})}
onChange={(e) => setData({...data, albumValue: e.target.value})}
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
Expand Down Expand Up @@ -193,11 +193,11 @@ export default function AlbumEditSheet(props : Readonly<HandleProps>) {
<div className="flex flex-col gap-1 rounded-lg border p-3 shadow-sm">
<div className="text-medium">{t('Album.imageSortRule')}</div>
<Select
value={typeof data.image_sorting === 'number' ? data.image_sorting.toString() : '1'}
value={typeof data.imageSorting === 'number' ? data.imageSorting.toString() : '1'}
onValueChange={(value) => {
setData({
...data,
image_sorting: parseInt(value),
imageSorting: parseInt(value),
})
}}
>
Expand Down
4 changes: 2 additions & 2 deletions components/admin/album/album-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default function AlbumList(props : Readonly<HandleProps>) {
<Card key={album.id} className="flex flex-col h-72 show-up-motion items-center gap-0 py-0">
<div className="flex h-12 justify-start w-full p-2 space-x-2">
<Badge aria-label={t('Album.albumName')}>{album.name}</Badge>
<Badge variant="secondary" aria-label={t('Album.router')}>{album.album_value}</Badge>
<Badge variant="secondary" aria-label={t('Album.router')}>{album.albumValue}</Badge>
</div>
<div className="flex justify-start w-full p-2 h-48">
{album.detail || t('Album.noTips')}
Expand Down Expand Up @@ -166,7 +166,7 @@ export default function AlbumList(props : Readonly<HandleProps>) {
<div>
<p>{t('Album.albumId')}:{album.id}</p>
<p>{t('Album.albumName')}:{album.name}</p>
<p>{t('Album.albumRouter')}:{album.album_value}</p>
<p>{t('Album.albumRouter')}:{album.albumValue}</p>
</div>
<DialogFooter>
<Button
Expand Down
4 changes: 2 additions & 2 deletions components/admin/list/image-edit-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ export default function ImageEditSheet(props : Readonly<ImageServerHandleProps &
</div>
<Switch
className="cursor-pointer"
checked={image?.show_on_mainpage === 0}
onCheckedChange={(value) => setImageEditData({...image, show_on_mainpage: value ? 0 : 1})}
checked={image?.showOnMainpage === 0}
onCheckedChange={(value) => setImageEditData({...image, showOnMainpage: value ? 0 : 1})}
/>
</div>
<Button
Expand Down
2 changes: 1 addition & 1 deletion components/admin/list/image-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export default function ImageView() {
</div>
</div>
<Switch
checked={imageViewData?.show_on_mainpage === 0}
checked={imageViewData?.showOnMainpage === 0}
disabled
aria-readonly
/>
Expand Down
4 changes: 2 additions & 2 deletions components/admin/list/list-props.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
<SelectLabel>{t('Words.album')}</SelectLabel>
<SelectItem className="cursor-pointer" value="all">{t('Words.all')}</SelectItem>
{albums?.map((album: AlbumType) => (
<SelectItem className="cursor-pointer" key={album.album_value} value={album.album_value}>
<SelectItem className="cursor-pointer" key={album.albumValue} value={album.albumValue}>
{album.name}
</SelectItem>
))}
Expand Down Expand Up @@ -464,7 +464,7 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
className="cursor-pointer"
onClick={() => {
setImage(image)
setImageAlbum(image.album_value)
setImageAlbum(image.albumValue)
}}
aria-label={t('List.bindAlbum')}
>
Expand Down
6 changes: 3 additions & 3 deletions components/admin/tasks/tasks-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
import { ADMIN_TASK_KEY_REFRESH_IMAGE_METADATA } from '~/types/admin-tasks'

type ApiEnvelope<T> = { code: number; message: string; data: T }
type AlbumOption = Pick<AlbumType, 'id' | 'name' | 'album_value'>
type AlbumOption = Pick<AlbumType, 'id' | 'name' | 'albumValue'>

const DEFAULT_SCOPE: AdminTaskScope = { albumValue: 'all', showStatus: -1 }
const HISTORY_RECENT_LIMIT = 10
Expand Down Expand Up @@ -303,7 +303,7 @@ export default function TasksPage() {
const selectedRun = selectedRunDetail ?? selectedRunSummary

const albumName = (albumValue: string) =>
albumValue === 'all' ? tx('Words.all') : albums?.find((album) => album.album_value === albumValue)?.name || albumValue
albumValue === 'all' ? tx('Words.all') : albums?.find((album) => album.albumValue === albumValue)?.name || albumValue
const showLabel = (showStatus: AdminTaskScope['showStatus']) =>
showStatus === 0 ? tx('Words.public') : showStatus === 1 ? tx('Words.private') : tx('Words.all')
const statusLabel = (status: AdminTaskStatus) =>
Expand Down Expand Up @@ -501,7 +501,7 @@ export default function TasksPage() {
<SelectContent>
<SelectItem value='all'>{tx('Words.all')}</SelectItem>
{albums?.map((album) => (
<SelectItem key={album.id} value={album.album_value}>{album.name}</SelectItem>
<SelectItem key={album.id} value={album.albumValue}>{album.name}</SelectItem>
))}
</SelectContent>
</Select>
Expand Down
2 changes: 1 addition & 1 deletion components/admin/upload/livephoto-file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ export default function LivephotoFileUpload() {
<SelectGroup>
<SelectLabel>{t('Words.album')}</SelectLabel>
{albums?.map((album: AlbumType) => (
<SelectItem key={album.album_value} value={album.album_value}>
<SelectItem key={album.albumValue} value={album.albumValue}>
{album.name}
</SelectItem>
))}
Expand Down
2 changes: 1 addition & 1 deletion components/admin/upload/multiple-file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default function MultipleFileUpload() {
<SelectGroup>
<SelectLabel>{t('Words.album')}</SelectLabel>
{albums?.map((album: AlbumType) => (
<SelectItem key={album.album_value} value={album.album_value}>
<SelectItem key={album.albumValue} value={album.albumValue}>
{album.name}
</SelectItem>
))}
Expand Down
4 changes: 2 additions & 2 deletions components/admin/upload/simple-file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export default function SimpleFileUpload() {
id: imageId,
album: album,
url: url,
image_name: imageName,
imageName: imageName,
title: title,
preview_url: previewUrl,
video_url: videoUrl,
Expand Down Expand Up @@ -284,7 +284,7 @@ export default function SimpleFileUpload() {
<SelectGroup>
<SelectLabel>{t('Words.album')}</SelectLabel>
{albums?.map((album: AlbumType) => (
<SelectItem key={album.album_value} value={album.album_value}>
<SelectItem key={album.albumValue} value={album.albumValue}>
{album.name}
</SelectItem>
))}
Expand Down
2 changes: 1 addition & 1 deletion components/album/preview-image-exif.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default function PreviewImageExif(props: Readonly<ImageDataProps>) {
<div>
<SectionTitle>{t('Exif.basicInfo')}</SectionTitle>
<div className="space-y-1">
<Row label={t('Exif.filename')} value={props.data?.title || props.data?.image_name} />
<Row label={t('Exif.filename')} value={props.data?.title || props.data?.imageName} />
{dimensions && <Row label={t('Exif.dimensions')} value={dimensions} />}
{megaPixels && <Row label={t('Exif.pixels')} value={megaPixels} />}
<Row label={t('Exif.captureTime')} value={formattedDateTime} />
Expand Down
4 changes: 2 additions & 2 deletions components/album/preview-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ export default function PreviewImage(props: Readonly<PreviewImageHandleProps>) {
return
}
}
if (props.data?.album_value) {
router.push(`${props.data.album_value}`)
if (props.data?.albumValue) {
router.push(`${props.data.albumValue}`)
} else {
router.push('/')
}
Expand Down
4 changes: 2 additions & 2 deletions components/layout/top-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ export default function TopNav(props: Readonly<AlbumDataProps>) {
props.data.map((album: AlbumType) => (
<Link
key={album.id}
href={album.album_value}
href={album.albumValue}
className={`rounded-full px-3 py-1 text-sm whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
isActiveTab(album.album_value)
isActiveTab(album.albumValue)
? 'bg-primary/10 text-foreground font-medium'
: 'text-muted-foreground hover:text-foreground'
}`}
Expand Down
4 changes: 2 additions & 2 deletions hono/albums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ app.get('/', async (c) => {

app.post('/', async (c) => {
const album = await c.req.json()
if (album.album_value && album.album_value.charAt(0) !== '/') {
if (album.albumValue && album.albumValue.charAt(0) !== '/') {
throw badRequest('The route must start with /')
}
try {
Expand All @@ -31,7 +31,7 @@ app.post('/', async (c) => {

app.put('/', async (c) => {
const album = await c.req.json()
if (album.album_value && album.album_value.charAt(0) !== '/') {
if (album.albumValue && album.albumValue.charAt(0) !== '/') {
throw badRequest('The route must start with /')
}
try {
Expand Down
4 changes: 2 additions & 2 deletions hono/open/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ app.get('/:id/presigned', async (c) => {

try {
const { imageData, imageUrl } = await loadImageOrThrow(id)
const filename = deriveFilename(imageData.image_name, imageUrl)
const filename = deriveFilename(imageData.imageName, imageUrl)
const url = await buildPresignedUrlForStorage(storage, imageUrl)
c.header('Cache-Control', 'private, max-age=60')
return ok(c, { url, filename: encodeURIComponent(filename) })
Expand All @@ -182,7 +182,7 @@ app.get('/:id', async (c) => {

try {
const { imageData, imageUrl } = await loadImageOrThrow(id)
const filename = deriveFilename(imageData.image_name, imageUrl)
const filename = deriveFilename(imageData.imageName, imageUrl)

const response = await fetch(imageUrl)
if (!response.ok) {
Expand Down
32 changes: 7 additions & 25 deletions server/db/operate/albums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
'use server'

import { db } from '~/server/lib/db'
import {
toAlbumPrismaCreate,
toAlbumPrismaUpdate,
} from '~/server/lib/model-transform'
import type { AlbumType } from '~/types'

/**
Expand All @@ -14,18 +18,7 @@ export async function insertAlbums(album: AlbumType) {
album.sort = 0
}
return await db.albums.create({
data: {
name: album.name,
album_value: album.album_value,
detail: album.detail,
sort: album.sort,
theme: album.theme,
show: album.show,
license: album.license,
del: 0,
image_sorting: album.image_sorting,
random_show: album.random_show,
}
data: toAlbumPrismaCreate(album)
})
}

Expand Down Expand Up @@ -66,25 +59,14 @@ export async function updateAlbum(album: AlbumType) {
where: {
id: album.id
},
data: {
name: album.name,
album_value: album.album_value,
detail: album.detail,
sort: album.sort,
theme: album.theme,
show: album.show,
license: album.license,
updatedAt: new Date(),
image_sorting: album.image_sorting,
random_show: album.random_show,
}
data: toAlbumPrismaUpdate(album)
})
await tx.imagesAlbumsRelation.updateMany({
where: {
album_value: tagOld.album_value
},
data: {
album_value: album.album_value
album_value: album.albumValue
}
})
})
Expand Down
Loading
Loading