diff --git a/.env b/.env deleted file mode 100644 index dadb0b7..0000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -# 数据库 url -DATABASE_URL="postgres://postgres:666666@localhost:5432/postgres" -# AUTH_SECRET npx auth secret -AUTH_SECRET=your-secret-key diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..50eb5cb --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# 数据库 url +DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres" +# AUTH_SECRET npx auth secret +AUTH_SECRET=your-secret-key +# 禁用 Vercel node.js 帮助程序 +NODEJS_HELPERS=0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b37bdea..04cd403 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ dist /build # local env files -.env*.local \ No newline at end of file +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7980c9f..01471d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs ENV AUTH_TRUST_HOST true +ENV NODEJS_HELPERS 0 EXPOSE 3000 diff --git a/app/(default)/[...tag]/page.tsx b/app/(default)/[...tag]/page.tsx index 190292d..17c3f31 100644 --- a/app/(default)/[...tag]/page.tsx +++ b/app/(default)/[...tag]/page.tsx @@ -1,5 +1,5 @@ import Masonry from '~/components/Masonry' -import { fetchClientImagesListByTag, fetchClientImagesPageTotalByTag } from '~/server/lib/query' +import { fetchClientImagesListByTag, fetchClientImagesPageTotalByTag } from '~/server/db/query' import { ImageHandleProps } from '~/types' export default function Page({ params }: { params: { tag: string } }) { diff --git a/app/(default)/label/[...tag]/page.tsx b/app/(default)/label/[...tag]/page.tsx index 4deda5f..e903b56 100644 --- a/app/(default)/label/[...tag]/page.tsx +++ b/app/(default)/label/[...tag]/page.tsx @@ -1,5 +1,5 @@ import Masonry from '~/components/Masonry' -import { fetchClientImagesListByLabel, fetchClientImagesPageTotalByLabel } from '~/server/lib/query' +import { fetchClientImagesListByLabel, fetchClientImagesPageTotalByLabel } from '~/server/db/query' import { ImageHandleProps } from '~/types' export default function Label({ params }: { params: { tag: string } }) { diff --git a/app/(default)/page.tsx b/app/(default)/page.tsx index 28fb9b6..b382952 100644 --- a/app/(default)/page.tsx +++ b/app/(default)/page.tsx @@ -1,5 +1,5 @@ import Masonry from '~/components/Masonry' -import { fetchClientImagesListByTag, fetchClientImagesPageTotalByTag } from '~/server/lib/query' +import { fetchClientImagesListByTag, fetchClientImagesPageTotalByTag } from '~/server/db/query' import { ImageHandleProps } from '~/types' export default async function Home() { diff --git a/app/admin/about/page.tsx b/app/admin/about/page.tsx index e0058ef..c7f4e38 100644 --- a/app/admin/about/page.tsx +++ b/app/admin/about/page.tsx @@ -33,7 +33,7 @@ export default function About() { width={64} height={64} /> - v1.0.0 + v1.1.0 PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js 开发。
diff --git a/app/admin/copyright/page.tsx b/app/admin/copyright/page.tsx index e745b02..2ab4714 100644 --- a/app/admin/copyright/page.tsx +++ b/app/admin/copyright/page.tsx @@ -1,4 +1,4 @@ -import { fetchCopyrightList } from '~/server/lib/query' +import { fetchCopyrightList } from '~/server/db/query' import { HandleProps } from '~/types' import { Card, CardHeader } from '@nextui-org/react' import React from 'react' diff --git a/app/admin/list/page.tsx b/app/admin/list/page.tsx index d274bef..e9ed1b5 100644 --- a/app/admin/list/page.tsx +++ b/app/admin/list/page.tsx @@ -1,7 +1,7 @@ import { fetchServerImagesListByTag, fetchServerImagesPageTotalByTag -} from '~/server/lib/query' +} from '~/server/db/query' import { ImageServerHandleProps } from '~/types' import ListProps from '~/components/admin/list/ListProps' diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 1b4f12e..1c099ab 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,4 +1,4 @@ -import { fetchImagesAnalysis } from '~/server/lib/query' +import { fetchImagesAnalysis } from '~/server/db/query' import CardList from '~/components/admin/dashboard/CardList' import { DataProps } from '~/types' diff --git a/app/admin/settings/authenticator/page.tsx b/app/admin/settings/authenticator/page.tsx index e401522..a9a11d3 100644 --- a/app/admin/settings/authenticator/page.tsx +++ b/app/admin/settings/authenticator/page.tsx @@ -41,7 +41,7 @@ export default function Authenticator() { async function getQRCode() { try { - const res = await fetch('/api/v1/get-seed-secret', { + const res = await fetch('/api/v1/auth/get-seed-secret', { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -60,7 +60,7 @@ export default function Authenticator() { async function saveAuthTemplateToken() { try { - const res = await fetch('/api/v1/auth-validate', { + const res = await fetch('/api/v1/auth/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -82,7 +82,7 @@ export default function Authenticator() { async function removeAuth() { try { setDeleteLoading(true) - const res = await fetch('/api/v1/remove-auth', { + const res = await fetch('/api/v1/auth/remove', { method: 'DELETE', headers: { 'Content-Type': 'application/json' diff --git a/app/admin/settings/password/page.tsx b/app/admin/settings/password/page.tsx index dfd93a2..b98fa83 100644 --- a/app/admin/settings/password/page.tsx +++ b/app/admin/settings/password/page.tsx @@ -37,7 +37,7 @@ export default function PassWord() { } try { setLoading(true) - await fetch('/api/v1/update-password', { + await fetch('/api/v1/settings/update-password', { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/app/admin/settings/preferences/page.tsx b/app/admin/settings/preferences/page.tsx index 73f6ad0..ced9b56 100644 --- a/app/admin/settings/preferences/page.tsx +++ b/app/admin/settings/preferences/page.tsx @@ -1,7 +1,7 @@ 'use client' import { Button, Card, CardBody, Input } from '@nextui-org/react' -import React, {useEffect, useState} from 'react' +import React, { useEffect, useState } from 'react' import useSWR from 'swr' import { fetcher } from '~/utils/fetcher' import { toast } from 'sonner' @@ -10,12 +10,12 @@ export default function Preferences() { const [title, setTitle] = useState('') const [loading, setLoading] = useState(false) - const { data, isValidating, isLoading } = useSWR('/api/v1/get-custom-title', fetcher) + const { data, isValidating, isLoading } = useSWR('/api/v1/settings/get-custom-title', fetcher) async function updateTitle() { try { setLoading(true) - await fetch('/api/v1/update-custom-title', { + await fetch('/api/v1/settings/update-custom-title', { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/app/admin/tag/page.tsx b/app/admin/tag/page.tsx index ac06398..e174001 100644 --- a/app/admin/tag/page.tsx +++ b/app/admin/tag/page.tsx @@ -1,4 +1,4 @@ -import { fetchTagsList } from '~/server/lib/query' +import { fetchTagsList } from '~/server/db/query' import TagList from '~/components/admin/tag/TagList' import { Card, CardHeader } from '@nextui-org/react' import RefreshButton from '~/components/RefreshButton' diff --git a/app/api/[[...route]]/route.ts b/app/api/[[...route]]/route.ts new file mode 100644 index 0000000..52dc9c0 --- /dev/null +++ b/app/api/[[...route]]/route.ts @@ -0,0 +1,22 @@ +import 'server-only' +import { handle } from 'hono/vercel' +import { Hono } from 'hono' +import route from '~/hono' +import open from '~/hono/open/open' + +const app = new Hono().basePath('/api') + +app.route('/v1', route) +app.route('/open', open) +app.notFound((c) => { + return c.text('not found', 404) +}) + +export const GET = handle(app) +export const POST = handle(app) +export const PUT = handle(app) +export const DELETE = handle(app) +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' + +export default app diff --git a/app/api/open/get-auth-status/route.ts b/app/api/open/get-auth-status/route.ts deleted file mode 100644 index 8c76351..0000000 --- a/app/api/open/get-auth-status/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import 'server-only' -import { queryAuthStatus } from '~/server/lib/query' - -export async function GET() { - try { - const data = await queryAuthStatus(); - - return Response.json({ - code: 200, - message: '获取双因素状态成功!', - data: { - auth_enable: data?.config_value - } - }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '获取双因素状态失败!' }) - } -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/open/get-image-blob/route.ts b/app/api/open/get-image-blob/route.ts deleted file mode 100644 index f96c768..0000000 --- a/app/api/open/get-image-blob/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import 'server-only' -import { NextRequest } from 'next/server' - -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url) - const imageUrl = searchParams.get('imageUrl') - // @ts-ignore - const blob = await fetch(imageUrl).then(res => res.blob()) - return new Response(blob) -} - -export const revalidate = 0 \ No newline at end of file diff --git a/app/api/open/get-image-by-id/route.ts b/app/api/open/get-image-by-id/route.ts deleted file mode 100644 index b789093..0000000 --- a/app/api/open/get-image-by-id/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import 'server-only' -import { fetchImageByIdAndAuth } from '~/server/lib/query' -import { NextRequest } from 'next/server' - -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url) - const id = searchParams.get('id') - const data = await fetchImageByIdAndAuth(Number(id)); - if (data && data?.length > 0) { - return Response.json({ code: 200, message: '图片数据获取成功!', data: data }) - } else { - return Response.json({ code: 500, message: '图片不存在或未公开展示!' }) - } -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/route.ts b/app/api/route.ts deleted file mode 100644 index 18357e1..0000000 --- a/app/api/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import 'server-only' - -export async function GET() { - return Response.json('hello') -} \ No newline at end of file diff --git a/app/api/v1/alist-info/route.ts b/app/api/v1/alist-info/route.ts deleted file mode 100644 index 4c8fad8..0000000 --- a/app/api/v1/alist-info/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { fetchAListInfo } from '~/server/lib/query' - -export async function GET() { - const data = await fetchAListInfo(); - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/auth-login-validate/route.ts b/app/api/v1/auth-login-validate/route.ts deleted file mode 100644 index 7536632..0000000 --- a/app/api/v1/auth-login-validate/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import 'server-only' -import { queryAuthSecret } from '~/server/lib/query' -import { NextRequest } from 'next/server' -import * as OTPAuth from 'otpauth' - -export async function POST(req: NextRequest) { - const data = await req.json() - try { - const secret = await queryAuthSecret(); - let totp = new OTPAuth.TOTP({ - issuer: "PicImpact", - label: "admin", - algorithm: "SHA512", - digits: 6, - period: 30, - // @ts-ignore - secret: OTPAuth.Secret.fromBase32(secret?.config_value), - }); - let delta = totp.validate({ token: data.token, window: 1 }) - if (delta === 0) { - return Response.json({ code: 200, message: '双因素口令验证成功!' }) - } - return Response.json({ code: 500, message: '双因素口令验证失败!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '双因素口令验证失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/auth-validate/route.ts b/app/api/v1/auth-validate/route.ts deleted file mode 100644 index 8cc9179..0000000 --- a/app/api/v1/auth-validate/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import 'server-only' -import { queryAuthTemplateSecret } from '~/server/lib/query' -import { NextRequest } from 'next/server' -import * as OTPAuth from 'otpauth' -import { saveAuthSecret } from '~/server/lib/operate' - -export async function POST(req: NextRequest) { - const data = await req.json() - try { - const secret = await queryAuthTemplateSecret(); - let totp = new OTPAuth.TOTP({ - issuer: "PicImpact", - label: "admin", - algorithm: "SHA512", - digits: 6, - period: 30, - // @ts-ignore - secret: OTPAuth.Secret.fromBase32(secret?.config_value), - }); - let delta = totp.validate({ token: data.token, window: 1 }) - if (delta === 0) { - // @ts-ignore - await saveAuthSecret('true', secret?.config_value) - return Response.json({ code: 200, message: '设置成功!' }) - } - return Response.json({ code: 500, message: '设置失败!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '设置失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/copyright-add/route.ts b/app/api/v1/copyright-add/route.ts deleted file mode 100644 index cbd3101..0000000 --- a/app/api/v1/copyright-add/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import 'server-only' -import { insertCopyright } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function POST(req: NextRequest) { - const copyright = await req.json() - try { - await insertCopyright(copyright); - return Response.json({ code: 200, message: '新增成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '新增失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/copyright-delete/[id]/route.ts b/app/api/v1/copyright-delete/[id]/route.ts deleted file mode 100644 index 40005bb..0000000 --- a/app/api/v1/copyright-delete/[id]/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'server-only' -import { deleteCopyright } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function DELETE( - req: NextRequest, - { params }: { params: { id: number } }, -) { - const data = await deleteCopyright(params.id); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/copyright-update/route.ts b/app/api/v1/copyright-update/route.ts deleted file mode 100644 index f106564..0000000 --- a/app/api/v1/copyright-update/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import 'server-only' -import { updateCopyright } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const copyright = await req.json() - try { - await updateCopyright(copyright); - return Response.json({ code: 200, message: '更新成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '更新失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/get-copyrights/route.ts b/app/api/v1/get-copyrights/route.ts deleted file mode 100644 index f70a30c..0000000 --- a/app/api/v1/get-copyrights/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { fetchCopyrightList } from '~/server/lib/query' - -export async function GET() { - const data = await fetchCopyrightList() - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/get-custom-title/route.ts b/app/api/v1/get-custom-title/route.ts deleted file mode 100644 index d0cf3f2..0000000 --- a/app/api/v1/get-custom-title/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { fetchCustomTitle } from '~/server/lib/query' - -export async function GET() { - const data = await fetchCustomTitle(); - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/get-seed-secret/route.ts b/app/api/v1/get-seed-secret/route.ts deleted file mode 100644 index 6f11429..0000000 --- a/app/api/v1/get-seed-secret/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import 'server-only' -import * as OTPAuth from 'otpauth' -import { saveAuthTemplateSecret } from '~/server/lib/operate' - -export async function GET() { - try { - let secret = new OTPAuth.Secret({ size: 12 }); - - let totp = new OTPAuth.TOTP({ - issuer: "PicImpact", - label: "admin", - algorithm: "SHA512", - digits: 6, - period: 30, - secret: secret, - }); - - await saveAuthTemplateSecret(secret.base32); - - return Response.json({ - code: 200, - message: '令牌颁发成功!', - data: { - uri: totp.toString(), - secret: secret.base32 - } - }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '令牌颁发失败!' }) - } -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/get-tags/route.ts b/app/api/v1/get-tags/route.ts deleted file mode 100644 index 0bd94aa..0000000 --- a/app/api/v1/get-tags/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { fetchTagsListAndNotDefault } from '~/server/lib/query' - -export async function GET() { - const data = await fetchTagsListAndNotDefault(); - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/image-add/route.ts b/app/api/v1/image-add/route.ts deleted file mode 100644 index c74dca3..0000000 --- a/app/api/v1/image-add/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import 'server-only' -import { insertImage } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function POST(req: NextRequest) { - const image = await req.json() - if (!image.url) { - return Response.json({ - code: 500, - message: '图片链接不能为空!' - }) - } - if (!image.height || image.height <= 0) { - return Response.json({ - code: 500, - message: '图片高度不能为空且必须大于 0!' - }) - } - if (!image.width || image.width <= 0) { - return Response.json({ - code: 500, - message: '图片宽度不能为空且必须大于 0!' - }) - } - try { - await insertImage(image); - return Response.json({ code: 200, message: '保存成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '保存失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/image-batch-delete/route.ts b/app/api/v1/image-batch-delete/route.ts deleted file mode 100644 index 5e9f63e..0000000 --- a/app/api/v1/image-batch-delete/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import 'server-only' -import { deleteBatchImage } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function DELETE(req: NextRequest) { - try { - const data = await req.json() - await deleteBatchImage(data); - return Response.json({ code: 200, message: '删除成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '删除失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/image-delete/[id]/route.ts b/app/api/v1/image-delete/[id]/route.ts deleted file mode 100644 index b88f56d..0000000 --- a/app/api/v1/image-delete/[id]/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import 'server-only' -import { deleteImage } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function DELETE( - req: NextRequest, - { params }: { params: { id: number } }, -) { - try { - await deleteImage(params.id); - return Response.json({ code: 200, message: '删除成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '删除失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/image-update/route.ts b/app/api/v1/image-update/route.ts deleted file mode 100644 index 6f61497..0000000 --- a/app/api/v1/image-update/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import 'server-only' -import { updateImage } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const image = await req.json() - if (!image.url) { - return Response.json({ - code: 500, - message: '图片链接不能为空!' - }) - } - if (!image.height || image.height <= 0) { - return Response.json({ - code: 500, - message: '图片高度不能为空且必须大于 0!' - }) - } - if (!image.width || image.width <= 0) { - return Response.json({ - code: 500, - message: '图片宽度不能为空且必须大于 0!' - }) - } - try { - await updateImage(image); - return Response.json({ code: 200, message: '更新成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '更新失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/r2-info/route.ts b/app/api/v1/r2-info/route.ts deleted file mode 100644 index 3ce7c92..0000000 --- a/app/api/v1/r2-info/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { fetchR2Info } from '~/server/lib/query' - -export async function GET() { - const data = await fetchR2Info(); - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/remove-auth/route.ts b/app/api/v1/remove-auth/route.ts deleted file mode 100644 index 8932be7..0000000 --- a/app/api/v1/remove-auth/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import 'server-only' -import { deleteAuthSecret } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function DELETE(req: NextRequest) { - try { - await deleteAuthSecret(); - return Response.json({ code: 200, message: '移除成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '移除失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/s3-info/route.ts b/app/api/v1/s3-info/route.ts deleted file mode 100644 index 5ecf107..0000000 --- a/app/api/v1/s3-info/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { fetchS3Info } from '~/server/lib/query' - -export async function GET() { - const data = await fetchS3Info(); - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/app/api/v1/tag-add/route.ts b/app/api/v1/tag-add/route.ts deleted file mode 100644 index 9b124cd..0000000 --- a/app/api/v1/tag-add/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import 'server-only' -import { insertTag } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function POST(req: NextRequest) { - const tag = await req.json() - if (tag.tag_value && tag.tag_value.charAt(0) !== '/') { - return Response.json({ - code: 500, - message: '路由必须以 / 开头!' - }) - } - try { - await insertTag(tag); - return Response.json({ code: 200, message: '新增成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '新增失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/tag-delete/[id]/route.ts b/app/api/v1/tag-delete/[id]/route.ts deleted file mode 100644 index 776faf2..0000000 --- a/app/api/v1/tag-delete/[id]/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'server-only' -import { deleteTag } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function DELETE( - req: NextRequest, - { params }: { params: { id: number } }, -) { - const data = await deleteTag(params.id); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/tag-update/route.ts b/app/api/v1/tag-update/route.ts deleted file mode 100644 index 24b12f4..0000000 --- a/app/api/v1/tag-update/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import 'server-only' -import { updateTag } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const tag = await req.json() - if (tag.tag_value && tag.tag_value.charAt(0) !== '/') { - return Response.json({ - code: 500, - message: '路由必须以 / 开头!' - }) - } - try { - await updateTag(tag); - return Response.json({ code: 200, message: '更新成功!' }) - } catch (e) { - console.log(e) - return Response.json({ code: 500, message: '更新失败!' }) - } -} \ No newline at end of file diff --git a/app/api/v1/update-alist-info/route.ts b/app/api/v1/update-alist-info/route.ts deleted file mode 100644 index dd62047..0000000 --- a/app/api/v1/update-alist-info/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import 'server-only' -import { updateAListConfig } from '~/server/lib/operate' -import { NextRequest } from 'next/server' -import { Config } from '~/types' - -export async function PUT(req: NextRequest) { - const query = await req.json() - - const alistUrl = query?.find((item: Config) => item.config_key === 'alist_url').config_value - const alistToken = query?.find((item: Config) => item.config_key === 'alist_token').config_value - - const data = await updateAListConfig({ alistUrl, alistToken }); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-copyright-default/route.ts b/app/api/v1/update-copyright-default/route.ts deleted file mode 100644 index 17cc489..0000000 --- a/app/api/v1/update-copyright-default/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { updateCopyrightDefault } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const copyright = await req.json() - const data = await updateCopyrightDefault(copyright.id, copyright.default); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-copyright-show/route.ts b/app/api/v1/update-copyright-show/route.ts deleted file mode 100644 index 4bc89c4..0000000 --- a/app/api/v1/update-copyright-show/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { updateCopyrightShow } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const copyright = await req.json() - const data = await updateCopyrightShow(copyright.id, copyright.show); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-custom-title/route.ts b/app/api/v1/update-custom-title/route.ts deleted file mode 100644 index 97925a5..0000000 --- a/app/api/v1/update-custom-title/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { updateCustomTitle } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const query = await req.json() - const data = await updateCustomTitle(query.title); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-image-show/route.ts b/app/api/v1/update-image-show/route.ts deleted file mode 100644 index ecd14d8..0000000 --- a/app/api/v1/update-image-show/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { updateImageShow } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const image = await req.json() - const data = await updateImageShow(image.id, image.show); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-image-tag/route.ts b/app/api/v1/update-image-tag/route.ts deleted file mode 100644 index 7f78bd3..0000000 --- a/app/api/v1/update-image-tag/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import 'server-only' -import { updateImageTag } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const image = await req.json() - try { - await updateImageTag(image.imageId, image.tagId); - return Response.json({ - code: 200, - message: '更新成功!' - }) - } catch (e) { - console.log(e) - return Response.json({ - code: 500, - message: '更新失败!' - }) - } -} \ No newline at end of file diff --git a/app/api/v1/update-password/route.ts b/app/api/v1/update-password/route.ts deleted file mode 100644 index 9c76d63..0000000 --- a/app/api/v1/update-password/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import 'server-only' -import { updatePassword } from '~/server/lib/operate' -import { NextRequest } from 'next/server' -import { auth } from '~/server/auth' -import CryptoJS from 'crypto-js' -import { fetchSecretKey, fetchUserById } from '~/server/lib/query' - -export async function PUT(req: NextRequest) { - const { user } = await auth() - const pwd = await req.json() - const daUser = await fetchUserById(user?.id) - const secretKey = await fetchSecretKey() - if (!secretKey || !secretKey.config_value) { - return Response.json({ - code: 500, - message: '更新失败!' - }) - } - const hashedOldPassword = CryptoJS.HmacSHA512(pwd.oldPassword, secretKey?.config_value).toString() - - try { - if (daUser && hashedOldPassword === daUser.password) { - const hashedNewPassword = CryptoJS.HmacSHA512(pwd.newPassword, secretKey?.config_value).toString() - const data = await updatePassword(user?.id, hashedNewPassword); - return Response.json({ - code: 200, - message: '更新成功!' - }) - } else { - return Response.json({ - code: 500, - message: '旧密码不匹配!' - }) - } - } catch (e) { - console.log(e) - return Response.json({ - code: 500, - message: '更新失败!' - }) - } -} \ No newline at end of file diff --git a/app/api/v1/update-r2-info/route.ts b/app/api/v1/update-r2-info/route.ts deleted file mode 100644 index 4a09e3b..0000000 --- a/app/api/v1/update-r2-info/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import 'server-only' -import { updateR2Config } from '~/server/lib/operate' -import { NextRequest } from 'next/server' -import { Config } from '~/types' - -export async function PUT(req: NextRequest) { - const query = await req.json() - - const r2AccesskeyId = query?.find((item: Config) => item.config_key === 'r2_accesskey_id').config_value - const r2AccesskeySecret = query?.find((item: Config) => item.config_key === 'r2_accesskey_secret').config_value - const r2Endpoint = query?.find((item: Config) => item.config_key === 'r2_endpoint').config_value - const r2Bucket = query?.find((item: Config) => item.config_key === 'r2_bucket').config_value - const r2StorageFolder = query?.find((item: Config) => item.config_key === 'r2_storage_folder').config_value - const r2PublicDomain = query?.find((item: Config) => item.config_key === 'r2_public_domain').config_value - - const data = await updateR2Config({ r2AccesskeyId, r2AccesskeySecret, r2Endpoint, r2Bucket, r2StorageFolder, r2PublicDomain }); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-s3-info/route.ts b/app/api/v1/update-s3-info/route.ts deleted file mode 100644 index 3b95315..0000000 --- a/app/api/v1/update-s3-info/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import 'server-only' -import { updateS3Config } from '~/server/lib/operate' -import { NextRequest } from 'next/server' -import { Config } from '~/types' - -export async function PUT(req: NextRequest) { - const query = await req.json() - - const accesskeyId = query?.find((item: Config) => item.config_key === 'accesskey_id').config_value - const accesskeySecret = query?.find((item: Config) => item.config_key === 'accesskey_secret').config_value - const region = query?.find((item: Config) => item.config_key === 'region').config_value - const endpoint = query?.find((item: Config) => item.config_key === 'endpoint').config_value - const bucket = query?.find((item: Config) => item.config_key === 'bucket').config_value - const storageFolder = query?.find((item: Config) => item.config_key === 'storage_folder').config_value - const forcePathStyle = query?.find((item: Config) => item.config_key === 'force_path_style').config_value - const s3Cdn = query?.find((item: Config) => item.config_key === 's3_cdn').config_value - const s3CdnUrl = query?.find((item: Config) => item.config_key === 's3_cdn_url').config_value - - const data = await updateS3Config({ accesskeyId, accesskeySecret, region, endpoint, bucket, storageFolder, forcePathStyle, s3Cdn, s3CdnUrl }); - return Response.json(data) -} \ No newline at end of file diff --git a/app/api/v1/update-tag-show/route.ts b/app/api/v1/update-tag-show/route.ts deleted file mode 100644 index 23e0dfb..0000000 --- a/app/api/v1/update-tag-show/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'server-only' -import { updateTagShow } from '~/server/lib/operate' -import { NextRequest } from 'next/server' - -export async function PUT(req: NextRequest) { - const tag = await req.json() - const data = await updateTagShow(tag.id, tag.show); - return Response.json(data) -} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index e99ab88..1648417 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,7 +7,7 @@ import { ProgressBarProviders } from '~/app/providers/progress-bar-providers' import { ButtonStoreProvider } from '~/app/providers/button-store-Providers' import '~/style/globals.css' -import { fetchCustomTitle } from '~/server/lib/query' +import { fetchCustomTitle } from '~/server/db/query' type Props = { params: { id: string } @@ -33,7 +33,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/app/providers/next-ui-providers.tsx b/app/providers/next-ui-providers.tsx index d8d388b..3f4154d 100644 --- a/app/providers/next-ui-providers.tsx +++ b/app/providers/next-ui-providers.tsx @@ -6,7 +6,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes' export function NextUIProviders({children}: { children: React.ReactNode }) { return ( - + {children} diff --git a/components.json b/components.json index f2fb0af..ce140ea 100644 --- a/components.json +++ b/components.json @@ -1,6 +1,6 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "new-york", "rsc": true, "tsx": true, "tailwind": { @@ -12,6 +12,9 @@ }, "aliases": { "components": "~/components", - "utils": "~/lib/utils" + "utils": "~/utils", + "ui": "~/components/ui", + "lib": "~/utils", + "hooks": "~/hooks" } } \ No newline at end of file diff --git a/components/MasonryItem.tsx b/components/MasonryItem.tsx index 6b9d967..029ae35 100644 --- a/components/MasonryItem.tsx +++ b/components/MasonryItem.tsx @@ -133,7 +133,7 @@ export default function MasonryItem() {
{MasonryViewData?.exif?.model && MasonryViewData?.exif?.f_number diff --git a/components/admin/copyright/CopyrightAddSheet.tsx b/components/admin/copyright/CopyrightAddSheet.tsx index ef0a61f..2e60fea 100644 --- a/components/admin/copyright/CopyrightAddSheet.tsx +++ b/components/admin/copyright/CopyrightAddSheet.tsx @@ -28,7 +28,7 @@ export default function CopyrightAddSheet(props : Readonly) { } try { setLoading(true) - const res = await fetch('/api/v1/copyright-add', { + const res = await fetch('/api/v1/copyright/add', { headers: { 'Content-Type': 'application/json', }, diff --git a/components/admin/copyright/CopyrightEditSheet.tsx b/components/admin/copyright/CopyrightEditSheet.tsx index fb71444..5421037 100644 --- a/components/admin/copyright/CopyrightEditSheet.tsx +++ b/components/admin/copyright/CopyrightEditSheet.tsx @@ -27,7 +27,7 @@ export default function CopyrightEditSheet(props : Readonly) { } try { setLoading(true) - const res = await fetch('/api/v1/copyright-update', { + const res = await fetch('/api/v1/copyright/update', { headers: { 'Content-Type': 'application/json', }, diff --git a/components/admin/copyright/CopyrightList.tsx b/components/admin/copyright/CopyrightList.tsx index efd0b2c..e489d85 100644 --- a/components/admin/copyright/CopyrightList.tsx +++ b/components/admin/copyright/CopyrightList.tsx @@ -43,7 +43,7 @@ export default function CopyrightList(props : Readonly) { setDeleteLoading(true) if (!copyright.id) return try { - const res = await fetch(`/api/v1/copyright-delete/${copyright.id}`, { + const res = await fetch(`/api/v1/copyright/delete/${copyright.id}`, { method: 'DELETE', }) if (res.status === 200) { @@ -64,7 +64,7 @@ export default function CopyrightList(props : Readonly) { try { setUpdateCopyrightId(id) setUpdateCopyrightLoading(true) - const res = await fetch(`/api/v1/update-copyright-show`, { + const res = await fetch(`/api/v1/copyright/update-show`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -92,7 +92,7 @@ export default function CopyrightList(props : Readonly) { try { setUpdateCopyrightDefaultId(id) setUpdateCopyrightDefaultLoading(true) - const res = await fetch(`/api/v1/update-copyright-default`, { + const res = await fetch(`/api/v1/copyright/update-default`, { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/components/admin/list/ImageBatchDeleteSheet.tsx b/components/admin/list/ImageBatchDeleteSheet.tsx index 8f7e9d5..0c0aee0 100644 --- a/components/admin/list/ImageBatchDeleteSheet.tsx +++ b/components/admin/list/ImageBatchDeleteSheet.tsx @@ -29,7 +29,7 @@ export default function ImageBatchDeleteSheet(props : Readonly state, ) const [loading, setLoading] = useState(false) - const { data, isLoading } = useSWR('/api/v1/get-copyrights', fetcher) + const { data, isLoading } = useSWR('/api/v1/copyright/get', fetcher) async function submit() { if (!image.url) { @@ -35,7 +35,7 @@ export default function ImageEditSheet(props : Readonly state, ) - const { data, isLoading } = useSWR('/api/v1/get-copyrights', fetcher) + const { data, isLoading } = useSWR('/api/v1/copyright/get', fetcher) const fieldNames = { label: 'name', value: 'id' } diff --git a/components/admin/list/ListProps.tsx b/components/admin/list/ListProps.tsx index 6813851..c996c90 100644 --- a/components/admin/list/ListProps.tsx +++ b/components/admin/list/ListProps.tsx @@ -61,7 +61,7 @@ export default function ListProps(props : Readonly) { const { setImageEdit, setImageEditData, setImageView, setImageViewData, setImageHelp, setImageBatchDelete } = useButtonStore( (state) => state, ) - const { data: tags, isLoading: tagsLoading } = useSWR('/api/v1/get-tags', fetcher) + const { data: tags, isLoading: tagsLoading } = useSWR('/api/v1/tags/get', fetcher) const dataProps: DataProps = { data: data, @@ -71,7 +71,7 @@ export default function ListProps(props : Readonly) { setDeleteLoading(true) if (!image.id) return try { - const res = await fetch(`/api/v1/image-delete/${image.id}`, { + const res = await fetch(`/api/v1/image/delete/${image.id}`, { method: 'DELETE', }).then(res => res.json()) if (res?.code === 200) { @@ -92,7 +92,7 @@ export default function ListProps(props : Readonly) { try { setUpdateShowLoading(true) setUpdateShowId(id) - const res = await fetch(`/api/v1/update-image-show`, { + const res = await fetch(`/api/v1/image/update-show`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -123,7 +123,7 @@ export default function ListProps(props : Readonly) { } try { setUpdateImageTagLoading(true) - const res = await fetch(`/api/v1/update-image-tag`, { + const res = await fetch(`/api/v1/image/update-tag`, { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/components/admin/settings/storages/AListEditSheet.tsx b/components/admin/settings/storages/AListEditSheet.tsx index 52c9970..5ce45c1 100644 --- a/components/admin/settings/storages/AListEditSheet.tsx +++ b/components/admin/settings/storages/AListEditSheet.tsx @@ -18,7 +18,7 @@ export default function AListEditSheet() { async function submit() { setLoading(true) try { - const res = await fetch('/api/v1/update-alist-info', { + const res = await fetch('/api/v1/settings/update-alist-info', { headers: { 'Content-Type': 'application/json', }, @@ -26,7 +26,7 @@ export default function AListEditSheet() { body: JSON.stringify(aListData), }).then(res => res.json()) toast.success('更新成功!') - mutate('/api/v1/alist-info') + mutate('/api/v1/storage/alist/info') } catch (e) { toast.error('更新失败!') } finally { diff --git a/components/admin/settings/storages/AListTabs.tsx b/components/admin/settings/storages/AListTabs.tsx index 76a0707..269461a 100644 --- a/components/admin/settings/storages/AListTabs.tsx +++ b/components/admin/settings/storages/AListTabs.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { useButtonStore } from '~/app/providers/button-store-Providers' export default function AListTabs() { - const { data, error, isValidating, mutate } = useSWR('/api/v1/alist-info', fetcher + const { data, error, isValidating, mutate } = useSWR('/api/v1/storage/alist/info', fetcher , { revalidateOnFocus: false }) const { setAListEdit, setAListEditData } = useButtonStore( (state) => state, diff --git a/components/admin/settings/storages/R2EditSheet.tsx b/components/admin/settings/storages/R2EditSheet.tsx index b4bfcf9..b6a8e3e 100644 --- a/components/admin/settings/storages/R2EditSheet.tsx +++ b/components/admin/settings/storages/R2EditSheet.tsx @@ -18,7 +18,7 @@ export default function S3EditSheet() { async function submit() { setLoading(true) try { - await fetch('/api/v1/update-r2-info', { + await fetch('/api/v1/settings/update-r2-info', { headers: { 'Content-Type': 'application/json', }, @@ -26,7 +26,7 @@ export default function S3EditSheet() { body: JSON.stringify(r2Data), }).then(res => res.json()) toast.success('更新成功!') - mutate('/api/v1/r2-info') + mutate('/api/v1/settings/r2-info') } catch (e) { toast.error('更新失败!') } finally { diff --git a/components/admin/settings/storages/R2Tabs.tsx b/components/admin/settings/storages/R2Tabs.tsx index 4e7e53a..006ae7b 100644 --- a/components/admin/settings/storages/R2Tabs.tsx +++ b/components/admin/settings/storages/R2Tabs.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { useButtonStore } from '~/app/providers/button-store-Providers' export default function R2Tabs() { - const { data, error, isValidating, mutate } = useSWR('/api/v1/r2-info', fetcher + const { data, error, isValidating, mutate } = useSWR('/api/v1/settings/r2-info', fetcher , { revalidateOnFocus: false }) const { setR2Edit, setR2EditData } = useButtonStore( (state) => state, diff --git a/components/admin/settings/storages/S3EditSheet.tsx b/components/admin/settings/storages/S3EditSheet.tsx index fd7a8ce..a0c4384 100644 --- a/components/admin/settings/storages/S3EditSheet.tsx +++ b/components/admin/settings/storages/S3EditSheet.tsx @@ -18,7 +18,7 @@ export default function S3EditSheet() { async function submit() { setLoading(true) try { - const res = await fetch('/api/v1/update-s3-info', { + const res = await fetch('/api/v1/settings/update-s3-info', { headers: { 'Content-Type': 'application/json', }, @@ -26,7 +26,7 @@ export default function S3EditSheet() { body: JSON.stringify(s3Data), }).then(res => res.json()) toast.success('更新成功!') - mutate('/api/v1/s3-info') + mutate('/api/v1/settings/s3-info') } catch (e) { toast.error('更新失败!') } finally { diff --git a/components/admin/settings/storages/S3Tabs.tsx b/components/admin/settings/storages/S3Tabs.tsx index 87dd835..6861bd4 100644 --- a/components/admin/settings/storages/S3Tabs.tsx +++ b/components/admin/settings/storages/S3Tabs.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { useButtonStore } from '~/app/providers/button-store-Providers' export default function S3Tabs() { - const { data, error, isValidating, mutate } = useSWR('/api/v1/s3-info', fetcher + const { data, error, isValidating, mutate } = useSWR('/api/v1/settings/s3-info', fetcher , { revalidateOnFocus: false }) const { setS3Edit, setS3EditData } = useButtonStore( (state) => state, diff --git a/components/admin/tag/TagAddSheet.tsx b/components/admin/tag/TagAddSheet.tsx index 42b3fa0..a492139 100644 --- a/components/admin/tag/TagAddSheet.tsx +++ b/components/admin/tag/TagAddSheet.tsx @@ -27,7 +27,7 @@ export default function TagAddSheet(props : Readonly) { } try { setLoading(true) - const res = await fetch('/api/v1/tag-add', { + const res = await fetch('/api/v1/tags/add', { headers: { 'Content-Type': 'application/json', }, diff --git a/components/admin/tag/TagEditSheet.tsx b/components/admin/tag/TagEditSheet.tsx index 29ef60e..d8f064b 100644 --- a/components/admin/tag/TagEditSheet.tsx +++ b/components/admin/tag/TagEditSheet.tsx @@ -26,7 +26,7 @@ export default function TagEditSheet(props : Readonly) { } try { setLoading(true) - const res = await fetch('/api/v1/tag-update', { + const res = await fetch('/api/v1/tags/update', { headers: { 'Content-Type': 'application/json', }, diff --git a/components/admin/tag/TagList.tsx b/components/admin/tag/TagList.tsx index c55f96f..b0d6c87 100644 --- a/components/admin/tag/TagList.tsx +++ b/components/admin/tag/TagList.tsx @@ -43,7 +43,7 @@ export default function TagList(props : Readonly) { setDeleteLoading(true) if (!tag.id) return try { - const res = await fetch(`/api/v1/tag-delete/${tag.id}`, { + const res = await fetch(`/api/v1/tags/delete/${tag.id}`, { method: 'DELETE', }) if (res.status === 200) { @@ -64,7 +64,7 @@ export default function TagList(props : Readonly) { try { setUpdateTagId(id) setUpdateTagLoading(true) - const res = await fetch(`/api/v1/update-tag-show`, { + const res = await fetch(`/api/v1/tags/update-show`, { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/components/admin/upload/FileUpload.tsx b/components/admin/upload/FileUpload.tsx index f85bbdc..d7123f2 100644 --- a/components/admin/upload/FileUpload.tsx +++ b/components/admin/upload/FileUpload.tsx @@ -44,7 +44,7 @@ export default function FileUpload() { (state) => state, ) - const { data, isLoading } = useSWR('/api/v1/get-tags', fetcher) + const { data, isLoading } = useSWR('/api/v1/tags/get', fetcher) async function loadExif(file: any) { try { @@ -137,7 +137,7 @@ export default function FileUpload() { lat: lat, lon: lon, } as ImageType - const res = await fetch('/api/v1/image-add', { + const res = await fetch('/api/v1/image/add', { headers: { 'Content-Type': 'application/json', }, @@ -185,7 +185,7 @@ export default function FileUpload() { } try { toast.info('正在获取 AList 挂载目录!') - const res = await fetch('/api/v1/alist-storages', { + const res = await fetch('/api/v1/storage/alist/storages', { method: 'GET', }).then(res => res.json()) if (res?.code === 200) { @@ -224,7 +224,7 @@ export default function FileUpload() { formData.append('storage', storageArray[0]) formData.append('type', type) formData.append('mountPath', alistMountPathArray[0]) - return await fetch('/api/v1/file-upload', { + return await fetch('/api/v1/file/upload', { method: 'POST', body: formData }).then((res) => res.json()) @@ -299,7 +299,7 @@ export default function FileUpload() { lat: '', lon: '', } as ImageType - const res = await fetch('/api/v1/image-add', { + const res = await fetch('/api/v1/image/add', { headers: { 'Content-Type': 'application/json', }, diff --git a/components/layout/DropMenu.tsx b/components/layout/DropMenu.tsx index bb8f638..f4d9990 100644 --- a/components/layout/DropMenu.tsx +++ b/components/layout/DropMenu.tsx @@ -1,13 +1,18 @@ 'use client' -import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Avatar } from '@nextui-org/react' +import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react' import { usePathname } from 'next/navigation' import { useRouter } from 'next-nprogress-bar' -import { loginOut } from '~/server/lib/actions' +import { loginOut } from '~/server/actions' import { useSession } from 'next-auth/react' import { useEffect, useState } from 'react' import { useTheme } from 'next-themes' -import { Home, MonitorDot, SunMedium, MoonStar, Github, LogOut, LogIn } from 'lucide-react' +import { Home, MonitorDot, SunMedium, MoonStar, Github, LogOut, LogIn, Orbit } from 'lucide-react' +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "~/components/ui/avatar" export const DropMenu = () => { const router = useRouter() @@ -27,13 +32,13 @@ export const DropMenu = () => { return ( - + + + CN + { session ? @@ -76,7 +81,7 @@ export const DropMenu = () => { : } + startContent={theme === 'light' ? : } onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} > { theme === 'light' ? '切换至⌈常夜⌋' : '切换至⌈白夜⌋' } @@ -101,7 +106,7 @@ export const DropMenu = () => { : } + startContent={theme === 'light' ? : } onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} > { theme === 'light' ? '切换至⌈常夜⌋' : '切换至⌈白夜⌋' } diff --git a/components/layout/DynamicNavbar.tsx b/components/layout/DynamicNavbar.tsx index 4e096bc..529bbf9 100644 --- a/components/layout/DynamicNavbar.tsx +++ b/components/layout/DynamicNavbar.tsx @@ -1,7 +1,7 @@ import VaulDrawer from '~/components/layout/VaulDrawer' import { DropMenu } from '~/components/layout/DropMenu' import DynamicDropMenu from '~/components/layout/DynamicDropMenu' -import { fetchTagsShow } from '~/server/lib/query' +import { fetchTagsShow } from '~/server/db/query' import { DataProps } from '~/types' import SearchButton from '~/components/admin/SearchButton' diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 9fdd48f..e262699 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -2,7 +2,7 @@ import { Navbar, NavbarBrand, NavbarContent, NavbarItem } from '@nextui-org/reac import Logo from '~/components/layout/Logo' import DynamicNavbar from '~/components/layout/DynamicNavbar' import HeaderLink from '~/components/layout/HeaderLink' -import { fetchTagsShow } from '~/server/lib/query' +import { fetchTagsShow } from '~/server/db/query' import { DataProps } from '~/types' export default async function Header() { diff --git a/components/layout/VaulDrawer.tsx b/components/layout/VaulDrawer.tsx index b7714e5..609d33f 100644 --- a/components/layout/VaulDrawer.tsx +++ b/components/layout/VaulDrawer.tsx @@ -19,9 +19,10 @@ import { Settings, LogOut, Copyright, - Info + Info, + Orbit, } from 'lucide-react' -import { loginOut } from '~/server/lib/actions' +import { loginOut } from '~/server/actions' export default function VaulDrawer() { const { data: session, status } = useSession() @@ -150,7 +151,7 @@ export default function VaulDrawer() { : } + startContent={theme === 'light' ? : } onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} > { theme === 'light' ? '切换至⌈常夜⌋' : '切换至⌈白夜⌋' } @@ -179,7 +180,7 @@ export default function VaulDrawer() { : } + startContent={theme === 'light' ? : } onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} > { theme === 'light' ? '切换至⌈常夜⌋' : '切换至⌈白夜⌋' } diff --git a/components/login/UserFrom.tsx b/components/login/UserFrom.tsx index 9d11773..0e86001 100644 --- a/components/login/UserFrom.tsx +++ b/components/login/UserFrom.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import { Button, Input } from '@nextui-org/react' import { useRouter } from 'next-nprogress-bar' import { toast } from 'sonner' -import { authenticate } from '~/server/lib/actions' +import { authenticate } from '~/server/actions' import { SafeParseReturnType, z } from 'zod' import confetti from 'canvas-confetti' import { Eye, EyeOff } from 'lucide-react' diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..04c5795 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '~/utils' + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/hono/auth.ts b/hono/auth.ts new file mode 100644 index 0000000..d4105e3 --- /dev/null +++ b/hono/auth.ts @@ -0,0 +1,74 @@ +import 'server-only' +import { queryAuthTemplateSecret } from '~/server/db/query' +import * as OTPAuth from 'otpauth' +import { deleteAuthSecret, saveAuthSecret, saveAuthTemplateSecret } from '~/server/db/operate' +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/get-seed-secret', async (c) => { + try { + let secret = new OTPAuth.Secret({ size: 12 }); + + let totp = new OTPAuth.TOTP({ + issuer: "PicImpact", + label: "admin", + algorithm: "SHA512", + digits: 6, + period: 30, + secret: secret, + }); + + await saveAuthTemplateSecret(secret.base32); + + return c.json({ + code: 200, + message: '令牌颁发成功!', + data: { + uri: totp.toString(), + secret: secret.base32 + } + }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '令牌颁发失败!' }) + } +}) + +app.post('/validate', async (c) => { + const data = await c.req.json() + try { + const secret = await queryAuthTemplateSecret(); + let totp = new OTPAuth.TOTP({ + issuer: "PicImpact", + label: "admin", + algorithm: "SHA512", + digits: 6, + period: 30, + // @ts-ignore + secret: OTPAuth.Secret.fromBase32(secret?.config_value), + }); + let delta = totp.validate({ token: data.token, window: 1 }) + if (delta === 0) { + // @ts-ignore + await saveAuthSecret('true', secret?.config_value) + return c.json({ code: 200, message: '设置成功!' }) + } + return c.json({ code: 500, message: '设置失败!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '设置失败!' }) + } +}) + +app.delete('/remove', async (c) => { + try { + await deleteAuthSecret(); + return c.json({ code: 200, message: '移除成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '移除失败!' }) + } +}) + +export default app \ No newline at end of file diff --git a/hono/copyright.ts b/hono/copyright.ts new file mode 100644 index 0000000..458dafc --- /dev/null +++ b/hono/copyright.ts @@ -0,0 +1,59 @@ +import 'server-only' +import { + deleteCopyright, + insertCopyright, + updateCopyright, + updateCopyrightDefault, + updateCopyrightShow +} from '~/server/db/operate' +import { fetchCopyrightList } from '~/server/db/query' +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/get', async (c) => { + const data = await fetchCopyrightList() + return c.json(data) +}) + +app.post('/add', async (c) => { + const copyright = await c.req.json() + try { + await insertCopyright(copyright); + return c.json({ code: 200, message: '新增成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '新增失败!' }) + } +}) + +app.delete('/delete/:id', async (c) => { + const { id } = c.req.param() + const data = await deleteCopyright(Number(id)); + return c.json(data) +}) + +app.put('/update', async (c) => { + const copyright = await c.req.json() + try { + await updateCopyright(copyright); + return c.json({ code: 200, message: '更新成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '更新失败!' }) + } +}) + +app.put('/update-default', async (c) => { + const copyright = await c.req.json() + const data = await updateCopyrightDefault(copyright.id, copyright.default); + return c.json(data) +}) + +app.put('/update-show', async (c) => { + const copyright = await c.req.json() + const data = await updateCopyrightShow(copyright.id, copyright.show); + return c.json(data) +}) + +export default app diff --git a/app/api/v1/file-upload/route.ts b/hono/file.ts similarity index 79% rename from app/api/v1/file-upload/route.ts rename to hono/file.ts index 71016c9..2d4d09e 100644 --- a/app/api/v1/file-upload/route.ts +++ b/hono/file.ts @@ -1,10 +1,14 @@ -import { fetchAListInfo, fetchS3Info, fetchR2Info } from '~/server/lib/query' +import 'server-only' +import { fetchAListInfo, fetchS3Info, fetchR2Info } from '~/server/db/query' import { getClient } from '~/server/lib/s3' import { getR2Client } from '~/server/lib/r2' import { PutObjectCommand } from '@aws-sdk/client-s3' +import { Hono } from 'hono' -export async function POST(request: Request) { - const formData = await request.formData() +const app = new Hono() + +app.post('/upload', async (c) => { + const formData = await c.req.formData() const file = formData.get('file') const storage = formData.get('storage') @@ -40,7 +44,8 @@ export async function POST(request: Request) { ) if (s3Cdn && s3Cdn === 'true') { - return Response.json({ code: 200, data: `https://${ + return Response.json({ + code: 200, data: `https://${ s3CdnUrl.includes('https://') ? s3CdnUrl.split('//')[1] : s3CdnUrl }/${ storageFolder && storageFolder !== '/' @@ -50,7 +55,8 @@ export async function POST(request: Request) { }) } else { if (forcePathStyle && forcePathStyle === 'true') { - return Response.json({ code: 200, data: `https://${ + return Response.json({ + code: 200, data: `https://${ endpoint.includes('https://') ? endpoint.split('//')[1] : endpoint }/${bucket}/${ storageFolder && storageFolder !== '/' @@ -60,12 +66,13 @@ export async function POST(request: Request) { }) } } - return Response.json({ code: 200, data: `https://${bucket}.${ - endpoint.includes('https://') ? endpoint.split('//')[1] : endpoint - }/${ - storageFolder && storageFolder !== '/' - ? type && type !== '/' ? `${storageFolder}${type}/${encodeURIComponent(file?.name)}` : `${storageFolder}/${encodeURIComponent(file?.name)}` - : type && type !== '/' ? `${type.slice(1)}/${encodeURIComponent(file?.name)}` : `${encodeURIComponent(file?.name)}` + return Response.json({ + code: 200, data: `https://${bucket}.${ + endpoint.includes('https://') ? endpoint.split('//')[1] : endpoint + }/${ + storageFolder && storageFolder !== '/' + ? type && type !== '/' ? `${storageFolder}${type}/${encodeURIComponent(file?.name)}` : `${storageFolder}/${encodeURIComponent(file?.name)}` + : type && type !== '/' ? `${type.slice(1)}/${encodeURIComponent(file?.name)}` : `${encodeURIComponent(file?.name)}` }` }) } else if (storage && storage.toString() === 'r2') { @@ -95,10 +102,11 @@ export async function POST(request: Request) { await r2.send( new PutObjectCommand(params) ) - return Response.json({ code: 200, data: `${ - r2PublicDomain ? - r2PublicDomain.includes('https://') ? r2PublicDomain : `https://${r2PublicDomain}` - : r2Endpoint.includes('https://') ? r2Endpoint : `https://${r2Endpoint}` + return Response.json({ + code: 200, data: `${ + r2PublicDomain ? + r2PublicDomain.includes('https://') ? r2PublicDomain : `https://${r2PublicDomain}` + : r2Endpoint.includes('https://') ? r2Endpoint : `https://${r2Endpoint}` }/${ r2StorageFolder && r2StorageFolder !== '/' ? type && type !== '/' ? `${r2StorageFolder}${type}/${encodeURIComponent(file?.name)}` : `${r2StorageFolder}/${encodeURIComponent(file?.name)}` @@ -110,7 +118,7 @@ export async function POST(request: Request) { const alistToken = findConfig.find((item: any) => item.config_key === 'alist_token')?.config_value || ''; const alistUrl = findConfig.find((item: any) => item.config_key === 'alist_url')?.config_value || ''; const filePath = encodeURIComponent(`${mountPath && mountPath.toString() === '/' ? '' : mountPath}${ - type && type !== '/' ? `${type}/${file?.name}` : `/${file?.name}` }`) + type && type !== '/' ? `${type}/${file?.name}` : `/${file?.name}`}`) const data = await fetch(`${alistUrl}/api/fs/put`, { method: 'PUT', headers: { @@ -126,14 +134,16 @@ export async function POST(request: Request) { 'Authorization': alistToken.toString(), 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: decodeURIComponent(filePath) }) + body: JSON.stringify({path: decodeURIComponent(filePath)}) }).then((res) => res.json()) if (res?.code === 200) { - return Response.json({ code: 200, message: '文件上传成功!', data: res?.data.raw_url }) + return Response.json({code: 200, message: '文件上传成功!', data: res?.data.raw_url}) } else { - return Response.json({ code: 500, message: '文件路径获取失败!', data: null }) + return Response.json({code: 500, message: '文件路径获取失败!', data: null}) } } } - return Response.json({ code: 500, message: '文件上传失败!', data: null }) -} \ No newline at end of file + return Response.json({code: 500, message: '文件上传失败!', data: null}) +}) + +export default app \ No newline at end of file diff --git a/hono/hello.ts b/hono/hello.ts new file mode 100644 index 0000000..a56b2ae --- /dev/null +++ b/hono/hello.ts @@ -0,0 +1,11 @@ +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/hello', (c) => { + return c.json({ + message: 'Hello from Hono!', + }) +}) + +export default app diff --git a/hono/image.ts b/hono/image.ts new file mode 100644 index 0000000..102723e --- /dev/null +++ b/hono/image.ts @@ -0,0 +1,117 @@ +import 'server-only' +import { + deleteBatchImage, + deleteImage, + insertImage, + updateImage, + updateImageShow, + updateImageTag +} from '~/server/db/operate' +import { Hono } from 'hono' + +const app = new Hono() + +app.post('/add', async (c) => { + const image = await c.req.json() + if (!image.url) { + return c.json({ + code: 500, + message: '图片链接不能为空!' + }) + } + if (!image.height || image.height <= 0) { + return c.json({ + code: 500, + message: '图片高度不能为空且必须大于 0!' + }) + } + if (!image.width || image.width <= 0) { + return c.json({ + code: 500, + message: '图片宽度不能为空且必须大于 0!' + }) + } + try { + await insertImage(image); + return c.json({ code: 200, message: '保存成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '保存失败!' }) + } +}) + +app.delete('/batch-delete', async (c) => { + try { + const data = await c.req.json() + await deleteBatchImage(data); + return c.json({ code: 200, message: '删除成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '删除失败!' }) + } +}) + +app.delete('/delete/:id', async (c) => { + try { + const { id } = c.req.param() + await deleteImage(Number(id)); + return c.json({ code: 200, message: '删除成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '删除失败!' }) + } +}) + +app.put('/update', async (c) => { + const image = await c.req.json() + if (!image.url) { + return c.json({ + code: 500, + message: '图片链接不能为空!' + }) + } + if (!image.height || image.height <= 0) { + return c.json({ + code: 500, + message: '图片高度不能为空且必须大于 0!' + }) + } + if (!image.width || image.width <= 0) { + return c.json({ + code: 500, + message: '图片宽度不能为空且必须大于 0!' + }) + } + try { + await updateImage(image); + return c.json({ code: 200, message: '更新成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '更新失败!' }) + } +}) + +app.put('/update-show', async (c) => { + const image = await c.req.json() + const data = await updateImageShow(image.id, image.show); + return c.json(data) +}) + +app.put('/update-tag', async (c) => { + const image = await c.req.json() + try { + await updateImageTag(image.imageId, image.tagId); + return c.json({ + code: 200, + message: '更新成功!' + }) + } catch (e) { + console.log(e) + return c.json({ + code: 500, + message: '更新失败!' + }) + } +}) + +export default app \ No newline at end of file diff --git a/hono/index.ts b/hono/index.ts new file mode 100644 index 0000000..f063bcf --- /dev/null +++ b/hono/index.ts @@ -0,0 +1,23 @@ +import 'server-only' +import { Hono } from 'hono' +import settings from '~/hono/settings' +import hello from '~/hono/hello' +import auth from '~/hono/auth' +import copyright from '~/hono/copyright' +import file from '~/hono/file' +import image from '~/hono/image' +import tags from '~/hono/tags' +import alist from '~/hono/storage/alist' + +const route = new Hono() + +route.route('/settings', settings) +route.route('/hello', hello) +route.route('/auth', auth) +route.route('/copyright', copyright) +route.route('/file', file) +route.route('/image', image) +route.route('/tags', tags) +route.route('/storage/alist', alist) + +export default route \ No newline at end of file diff --git a/hono/open/open.ts b/hono/open/open.ts new file mode 100644 index 0000000..b4bfeca --- /dev/null +++ b/hono/open/open.ts @@ -0,0 +1,43 @@ +import 'server-only' +import { Hono } from 'hono' +import { fetchImageByIdAndAuth, queryAuthStatus } from '~/server/db/query' + +const app = new Hono() + +app.get('/get-auth-status', async (c) => { + try { + const data = await queryAuthStatus(); + + return c.json({ + code: 200, + message: '获取双因素状态成功!', + data: { + auth_enable: data?.config_value + } + }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '获取双因素状态失败!' }) + } +}) + +app.get('/get-image-blob', async (c) => { + const { searchParams } = new URL(c.req.url) + const imageUrl = searchParams.get('imageUrl') + // @ts-ignore + const blob = await fetch(imageUrl).then(res => res.blob()) + return new Response(blob) +}) + +app.get('/get-image-by-id', async (c) => { + const { searchParams } = new URL(c.req.url) + const id = searchParams.get('id') + const data = await fetchImageByIdAndAuth(Number(id)); + if (data && data?.length > 0) { + return c.json({ code: 200, message: '图片数据获取成功!', data: data }) + } else { + return c.json({ code: 500, message: '图片不存在或未公开展示!' }) + } +}) + +export default app diff --git a/hono/settings.ts b/hono/settings.ts new file mode 100644 index 0000000..2a4f115 --- /dev/null +++ b/hono/settings.ts @@ -0,0 +1,109 @@ +import 'server-only' +import { fetchCustomTitle, fetchR2Info, fetchS3Info, fetchSecretKey, fetchUserById } from '~/server/db/query' +import { Config } from '~/types' +import { updateAListConfig, updateCustomTitle, updatePassword, updateR2Config, updateS3Config } from '~/server/db/operate' +import { auth } from '~/server/auth' +import CryptoJS from 'crypto-js' +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/get-custom-title', async (c) => { + const data = await fetchCustomTitle(); + return c.json(data) +}) + +app.get('/r2-info', async (c) => { + const data = await fetchR2Info(); + return c.json(data) +}) + +app.get('/s3-info', async (c) => { + const data = await fetchS3Info(); + return c.json(data) +}) + +app.put('/update-alist-info', async (c) => { + const query = await c.req.json() + + const alistUrl = query?.find((item: Config) => item.config_key === 'alist_url').config_value + const alistToken = query?.find((item: Config) => item.config_key === 'alist_token').config_value + + const data = await updateAListConfig({ alistUrl, alistToken }); + return c.json(data) +}) + +app.put('/update-r2-info', async (c) => { + const query = await c.req.json() + + const r2AccesskeyId = query?.find((item: Config) => item.config_key === 'r2_accesskey_id').config_value + const r2AccesskeySecret = query?.find((item: Config) => item.config_key === 'r2_accesskey_secret').config_value + const r2Endpoint = query?.find((item: Config) => item.config_key === 'r2_endpoint').config_value + const r2Bucket = query?.find((item: Config) => item.config_key === 'r2_bucket').config_value + const r2StorageFolder = query?.find((item: Config) => item.config_key === 'r2_storage_folder').config_value + const r2PublicDomain = query?.find((item: Config) => item.config_key === 'r2_public_domain').config_value + + const data = await updateR2Config({ r2AccesskeyId, r2AccesskeySecret, r2Endpoint, r2Bucket, r2StorageFolder, r2PublicDomain }); + return c.json(data) +}) + +app.put('/update-s3-info', async (c) => { + const query = await c.req.json() + + const accesskeyId = query?.find((item: Config) => item.config_key === 'accesskey_id').config_value + const accesskeySecret = query?.find((item: Config) => item.config_key === 'accesskey_secret').config_value + const region = query?.find((item: Config) => item.config_key === 'region').config_value + const endpoint = query?.find((item: Config) => item.config_key === 'endpoint').config_value + const bucket = query?.find((item: Config) => item.config_key === 'bucket').config_value + const storageFolder = query?.find((item: Config) => item.config_key === 'storage_folder').config_value + const forcePathStyle = query?.find((item: Config) => item.config_key === 'force_path_style').config_value + const s3Cdn = query?.find((item: Config) => item.config_key === 's3_cdn').config_value + const s3CdnUrl = query?.find((item: Config) => item.config_key === 's3_cdn_url').config_value + + const data = await updateS3Config({ accesskeyId, accesskeySecret, region, endpoint, bucket, storageFolder, forcePathStyle, s3Cdn, s3CdnUrl }); + return c.json(data) +}) + +app.put('/update-custom-title', async (c) => { + const query = await c.req.json() + const data = await updateCustomTitle(query.title); + return c.json(data) +}) + +app.put('/update-password', async (c) => { + const { user } = await auth() + const pwd = await c.req.json() + const daUser = await fetchUserById(user?.id) + const secretKey = await fetchSecretKey() + if (!secretKey || !secretKey.config_value) { + return Response.json({ + code: 500, + message: '更新失败!' + }) + } + const hashedOldPassword = CryptoJS.HmacSHA512(pwd.oldPassword, secretKey?.config_value).toString() + + try { + if (daUser && hashedOldPassword === daUser.password) { + const hashedNewPassword = CryptoJS.HmacSHA512(pwd.newPassword, secretKey?.config_value).toString() + await updatePassword(user?.id, hashedNewPassword); + return c.json({ + code: 200, + message: '更新成功!' + }) + } else { + return c.json({ + code: 500, + message: '旧密码不匹配!' + }) + } + } catch (e) { + console.log(e) + return c.json({ + code: 500, + message: '更新失败!' + }) + } +}) + +export default app \ No newline at end of file diff --git a/app/api/v1/alist-storages/route.ts b/hono/storage/alist.ts similarity index 62% rename from app/api/v1/alist-storages/route.ts rename to hono/storage/alist.ts index e68309a..a206e92 100644 --- a/app/api/v1/alist-storages/route.ts +++ b/hono/storage/alist.ts @@ -1,7 +1,16 @@ import 'server-only' -import { fetchAListInfo } from '~/server/lib/query' +import { fetchAListInfo } from '~/server/db/query' -export async function GET() { +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/info', async (c) => { + const data = await fetchAListInfo(); + return c.json(data) +}) + +app.get('/storages', async (c) => { const findConfig = await fetchAListInfo() const alistToken = findConfig.find((item: any) => item.config_key === 'alist_token')?.config_value || ''; const alistUrl = findConfig.find((item: any) => item.config_key === 'alist_url')?.config_value || ''; @@ -12,8 +21,7 @@ export async function GET() { 'Authorization': alistToken.toString(), }, }).then(res => res.json()) + return c.json(data) +}) - return Response.json(data) -} - -export const dynamic = 'force-dynamic' \ No newline at end of file +export default app \ No newline at end of file diff --git a/hono/tags.ts b/hono/tags.ts new file mode 100644 index 0000000..ec30d63 --- /dev/null +++ b/hono/tags.ts @@ -0,0 +1,59 @@ +import 'server-only' +import { fetchTagsListAndNotDefault } from '~/server/db/query' +import { deleteTag, insertTag, updateTag, updateTagShow } from '~/server/db/operate' +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/get', async (c) => { + const data = await fetchTagsListAndNotDefault(); + return c.json(data) +}) + +app.post('/add', async (c) => { + const tag = await c.req.json() + if (tag.tag_value && tag.tag_value.charAt(0) !== '/') { + return c.json({ + code: 500, + message: '路由必须以 / 开头!' + }) + } + try { + await insertTag(tag); + return c.json({ code: 200, message: '新增成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '新增失败!' }) + } +}) + +app.put('/update', async (c) => { + const tag = await c.req.json() + if (tag.tag_value && tag.tag_value.charAt(0) !== '/') { + return c.json({ + code: 500, + message: '路由必须以 / 开头!' + }) + } + try { + await updateTag(tag); + return c.json({ code: 200, message: '更新成功!' }) + } catch (e) { + console.log(e) + return c.json({ code: 500, message: '更新失败!' }) + } +}) + +app.delete('/delete/:id', async (c) => { + const { id } = c.req.param() + const data = await deleteTag(Number(id)); + return c.json(data) +}) + +app.put('/update-show', async (c) => { + const tag = await c.req.json() + const data = await updateTagShow(tag.id, tag.show); + return c.json(data) +}) + +export default app \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 08e83d3..4cfd0a7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: "standalone", + reactStrictMode: true, compiler: { removeConsole: process.env.NODE_ENV === "production", }, diff --git a/package.json b/package.json index 8ef58c9..4c6d05f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "picimpact", - "version": "1.0.0", + "version": "1.1.0", "private": true, "author": "Bess Croft ", "packageManager": "pnpm@9.7.1", @@ -22,8 +22,10 @@ "@ant-design/nextjs-registry": "^1.0.1", "@auth/prisma-adapter": "^2.5.3", "@aws-sdk/client-s3": "^3.658.1", + "@hono/node-server": "^1.13.1", "@nextui-org/react": "^2.4.8", "@prisma/client": "5.17.0", + "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", @@ -41,6 +43,7 @@ "dayjs": "^1.11.13", "exifreader": "^4.23.5", "framer-motion": "^11.9.0", + "hono": "^4.6.3", "input-otp": "^1.2.4", "lucide-react": "^0.446.0", "next": "^14.2.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db7bf14..a85b3ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,18 @@ importers: '@aws-sdk/client-s3': specifier: ^3.658.1 version: 3.658.1 + '@hono/node-server': + specifier: ^1.13.1 + version: 1.13.1(hono@4.6.3) '@nextui-org/react': specifier: ^2.4.8 version: 2.4.8(@types/react@18.3.10)(framer-motion@11.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13) '@prisma/client': specifier: 5.17.0 version: 5.17.0(prisma@5.17.0) + '@radix-ui/react-avatar': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -74,6 +80,9 @@ importers: framer-motion: specifier: ^11.9.0 version: 11.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + hono: + specifier: ^4.6.3 + version: 4.6.3 input-otp: specifier: ^1.2.4 version: 1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -475,6 +484,12 @@ packages: '@formatjs/intl-localematcher@0.5.4': resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} + '@hono/node-server@1.13.1': + resolution: {integrity: sha512-TSxE6cT5RHnawbjnveexVN7H2Dpn1YaLxQrCOLCUwD+hFbqbFsnJBgdWcYtASqtWVjA+Qgi8uqFug39GsHjo5A==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1254,6 +1269,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-avatar@1.1.1': + resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1303,6 +1331,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.0.5': resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} peerDependencies: @@ -3351,6 +3388,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.6.3: + resolution: {integrity: sha512-0LeEuBNFeSHGqZ9sNVVgZjB1V5fmhkBSB0hZrpqStSMLOWgfLy0dHOvrjbJh0H2khsjet6rbHfWTHY0kpYThKQ==} + engines: {node: '>=16.9.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -5382,6 +5423,10 @@ snapshots: dependencies: tslib: 2.6.3 + '@hono/node-server@1.13.1(hono@4.6.3)': + dependencies: + hono: 4.6.3 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6569,6 +6614,18 @@ snapshots: '@types/react': 18.3.10 '@types/react-dom': 18.3.0 + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.10 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) @@ -6607,6 +6664,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.10 + '@radix-ui/react-context@1.1.1(@types/react@18.3.10)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.10 + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -9463,6 +9526,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.6.3: {} + ignore@5.3.2: {} import-fresh@3.3.0: diff --git a/public/fufu.jpg b/public/fufu.jpg deleted file mode 100644 index a02045c..0000000 Binary files a/public/fufu.jpg and /dev/null differ diff --git a/server/lib/actions.ts b/server/actions.ts similarity index 95% rename from server/lib/actions.ts rename to server/actions.ts index 735e170..30e57be 100644 --- a/server/lib/actions.ts +++ b/server/actions.ts @@ -1,7 +1,7 @@ 'use server' import { signIn, signOut } from '~/server/auth' -import { queryAuthSecret, queryAuthStatus } from '~/server/lib/query' +import { queryAuthSecret, queryAuthStatus } from '~/server/db/query' import * as OTPAuth from "otpauth" export async function authenticate( diff --git a/server/auth.ts b/server/auth.ts index ba0e364..c5815e3 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -5,7 +5,7 @@ import CredentialsProvider from 'next-auth/providers/credentials' import { db } from '~/server/lib/db' import { z } from 'zod' import CryptoJS from 'crypto-js' -import { fetchSecretKey } from '~/server/lib/query' +import { fetchSecretKey } from '~/server/db/query' const prisma = new PrismaClient() diff --git a/server/lib/operate.ts b/server/db/operate.ts similarity index 99% rename from server/lib/operate.ts rename to server/db/operate.ts index be9985d..a2080c9 100644 --- a/server/lib/operate.ts +++ b/server/db/operate.ts @@ -2,7 +2,7 @@ import { db } from '~/server/lib/db' import { TagType, ImageType, CopyrightType } from '~/types' -import {queryAuthTemplateSecret} from "~/server/lib/query"; +import {queryAuthTemplateSecret} from "~/server/db/query"; export async function insertTag(tag: TagType) { if (!tag.sort || tag.sort < 0) { diff --git a/server/lib/query.ts b/server/db/query.ts similarity index 92% rename from server/lib/query.ts rename to server/db/query.ts index 00aeaec..d772846 100644 --- a/server/lib/query.ts +++ b/server/db/query.ts @@ -133,7 +133,21 @@ export async function fetchServerImagesListByTag(pageNum: number, tag: string) { SELECT image.*, STRING_AGG(tags."name", ',') AS tag_names, - STRING_AGG(tags.id::text, ',') AS tag_values + STRING_AGG(tags.id::text, ',') AS tag_values, + ( + SELECT json_agg(row_to_json(t)) + FROM ( + SELECT copyright.id + FROM "public"."Copyright" AS copyright + INNER JOIN "public"."ImageCopyrightRelation" AS icrelation + ON copyright.id = icrelation."copyrightId" + INNER JOIN "public"."Images" AS image_child + ON icrelation."imageId" = image_child."id" + WHERE copyright.del = 0 + AND image_child.del = 0 + AND image.id = image_child.id + ) t + ) AS copyrights FROM "public"."Images" AS image INNER JOIN "public"."ImageTagRelation" AS relation @@ -150,6 +164,16 @@ export async function fetchServerImagesListByTag(pageNum: number, tag: string) { ORDER BY image.sort DESC, image.create_time DESC, image.update_time DESC LIMIT 8 OFFSET ${(pageNum - 1) * 8} ` + + if (findAll) { + // @ts-ignore + findAll?.map((item: any) => { + if (item.copyrights) { + item.copyrights = item?.copyrights.map((item: any) => Number(item.id)) + } + }) + } + return findAll; } const findAll = await db.$queryRaw` @@ -173,14 +197,12 @@ export async function fetchServerImagesListByTag(pageNum: number, tag: string) { ) AS copyrights FROM "public"."Images" AS image - INNER JOIN "public"."ImageTagRelation" AS relation + LEFT JOIN "public"."ImageTagRelation" AS relation ON image.id = relation."imageId" - INNER JOIN "public"."Tags" AS tags + LEFT JOIN "public"."Tags" AS tags ON relation.tag_value = tags.tag_value WHERE image.del = 0 - AND - tags.del = 0 GROUP BY image.id ORDER BY image.sort DESC, image.create_time DESC, image.update_time DESC LIMIT 8 OFFSET ${(pageNum - 1) * 8} @@ -230,14 +252,12 @@ export async function fetchServerImagesPageTotalByTag(tag: string) { image.id FROM "public"."Images" AS image - INNER JOIN "public"."ImageTagRelation" AS relation + LEFT JOIN "public"."ImageTagRelation" AS relation ON image.id = relation."imageId" - INNER JOIN "public"."Tags" AS tags + LEFT JOIN "public"."Tags" AS tags ON relation.tag_value = tags.tag_value WHERE image.del = 0 - AND - tags.del = 0 ) AS unique_images; ` // @ts-ignore diff --git a/server/lib/user.ts b/server/user.ts similarity index 100% rename from server/lib/user.ts rename to server/user.ts diff --git a/style/globals.css b/style/globals.css index 7c4e738..e6f5358 100644 --- a/style/globals.css +++ b/style/globals.css @@ -16,6 +16,11 @@ } } +[data-theme='dark'] { + --background: black; + --foreground: white; +} + .semi-tagInput { background-color: #e5e7eb !important; } @@ -63,6 +68,16 @@ --ring: 240 10% 3.9%; --radius: 0.5rem; + + --chart-1: 12 76% 61%; + + --chart-2: 173 58% 39%; + + --chart-3: 197 37% 24%; + + --chart-4: 43 74% 66%; + + --chart-5: 27 87% 67%; } .dark { @@ -93,6 +108,11 @@ --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } diff --git a/tailwind.config.ts b/tailwind.config.ts index 5365998..b58698b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -10,74 +10,93 @@ const config = { ], prefix: "", theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - "trail": { - "0%": { "--angle": "0deg" }, - "100%": { "--angle": "360deg" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "trail": "trail var(--duration) linear infinite", - }, - }, + container: { + center: 'true', + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + 'trail': { + '0%': { + '--angle': '0deg' + }, + '100%': { + '--angle': '360deg' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'trail': 'trail var(--duration) linear infinite' + } + } }, plugins: [nextui(), require('tailwindcss-animate'), require('tailwind-scrollbar-hide')], } satisfies Config diff --git a/types/http.ts b/types/http.ts new file mode 100644 index 0000000..c9ff752 --- /dev/null +++ b/types/http.ts @@ -0,0 +1,5 @@ +export type Response = { + code: number, + message: string, + data?: any +} \ No newline at end of file