Compare commits

...

4 commits

Author SHA1 Message Date
bo.yu
5ba6d4c5c3 Merge branch 'main' of https://feicode.com/Windfonts/fonts-vault into main
Some checks failed
gitleaks 密钥泄露扫描 / gitleaks (push) Waiting to run
Trivy 依赖漏洞扫描 / trivy-scan (push) Has been cancelled
2026-02-24 09:56:55 +08:00
bo.yu
c3e641ed88 chore: 移除 husky 和 lint-staged 的预提交钩子 2026-02-24 09:56:53 +08:00
bo.yu
30f7c7bd53 chore: 更新项目配置和依赖,优化Docker部署
- 更新Next.js到16.x,启用Turbopack并移除webpack配置
- 重构Dockerfile以使用standalone输出,简化部署流程
- 移除全局middleware,改为在admin layout中检查会话
- 更新环境变量和配置文件,移除生产环境数据库忽略
- 将多个页面从ISR改为动态渲染,避免构建时数据库查询
- 更新字体API和CSS服务逻辑,修复类型问题
- 更新依赖版本和TypeScript配置
2026-02-24 09:56:45 +08:00
bo.yu
ab00d7d1a0 chore: add Docker support and update environment configuration
- Add Dockerfile with multi-stage build for optimized production image
- Add .dockerignore to exclude unnecessary files from Docker builds
- Add Docker deployment scripts (docker-all.sh, docker-build.sh, docker-deploy.sh, docker-push.sh)
- Update .env.example with simplified and reorganized configuration variables
- Update .gitignore to exclude docker-compose.override.yml
- Fix Next.js 15 compatibility by updating dynamic route params to use Promise type
- Update API routes (brands, categories, fonts, styles) to handle async params
- Add health check endpoint at /api/health/route.ts
- Update admin font pages to properly await params
- Update next.config.ts configuration
- Remove next.svg from public assets
- Update package.json and package-lock.json dependencies
- Fix VSCode settings.json formatting
2025-12-07 16:10:11 +08:00
38 changed files with 2479 additions and 1230 deletions

53
.dockerignore Normal file
View file

@ -0,0 +1,53 @@
# 依赖
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# Next.js
.next
out
build

# LICENSE 文件(避免构建错误)
**/LICENSE
**/LICENSE.txt
**/LICENSE.md

# 测试
coverage

# 环境变量
.env*.local
.env.development

# 数据库(保留 prod.db
data/dev.db
data/*.db-shm
data/*.db-wal

# 日志
logs
*.log

# Git
.git
.gitignore

# IDE
.vscode
.idea
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# 其他
.trae
*.pem
.vercel
README.md

View file

@ -1,23 +1,17 @@
# 环境标识
NODE_ENV=development

# 数据库配置
DATABASE_URL=file:./data/dev.db

# NextAuth.js 认证配置
AUTH_SECRET=your-super-secret-key-change-this-in-production
NEXTAUTH_URL=http://localhost:3000

# 管理员账号配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123
ADMIN_EMAIL=admin@example.com

# OSS 配置
OSS_ENDPOINT=https://your-oss-endpoint.aliyuncs.com
OSS_REGION=your-region
OSS_BUCKET=your-bucket
OSS_FONT_PREFIX=fonts
OSS_FOLDER=font-packages
OSS_ACCESS_KEY_ID=your-access-key
OSS_ACCESS_KEY_SECRET=your-secret-key
OSS_ENDPOINT=your-endpoint

# 字体静态 URL (CDN)
NEXT_PUBLIC_FONT_STATIC_URL=https://your-cdn.com
# NextAuth 配置
NEXTAUTH_URL=http://localhost:4000
NEXTAUTH_SECRET=your-secret-key-here

# 管理员配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-password-here

3
.gitignore vendored
View file

@ -51,3 +51,6 @@ next-env.d.ts
# logs
/logs
*.log

# Docker
docker-compose.override.yml

View file

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

View file

@ -22,4 +22,4 @@
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}

62
Dockerfile Normal file
View file

@ -0,0 +1,62 @@
# 多阶段构建 - 基础镜像
FROM node:20-alpine AS base

# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci

# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production

# 构建应用
RUN npm run build

# 运行阶段 - 使用 standalone 输出
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV AUTH_TRUST_HOST=true
ENV DATABASE_URL=file:./data/prod.db

# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 只复制 standalone 输出
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# 创建数据和日志目录
RUN mkdir -p /app/data /app/logs && chown -R nextjs:nodejs /app/data /app/logs

# 复制本地初始化好的数据库
COPY --from=builder --chown=nextjs:nodejs /app/data/prod.db /app/data/prod.db

# 复制环境变量文件和启动脚本
COPY --from=builder --chown=nextjs:nodejs /app/.env.production ./.env.production
COPY --from=builder --chown=nextjs:nodejs /app/scripts/start.sh ./start.sh
RUN chmod +x ./start.sh

USER nextjs

EXPOSE 4000

ENV PORT=4000
ENV HOSTNAME="0.0.0.0"

CMD ["./start.sh"]

View file

@ -10,16 +10,18 @@ import { notFound } from 'next/navigation';
import { FontForm } from '../../font-form';

interface PageProps {
params: {
params: Promise<{
id: string;
};
}>;
}

export default async function EditFontPage({ params }: PageProps) {
await requireAuth();

const { id } = await params;

// 获取字体详情 - 使用 normalizedName
const font = await fontService.findByNormalizedName(params.id);
const font = await fontService.findByNormalizedName(id);
if (!font) {
notFound();
}

View file

@ -10,16 +10,18 @@ import Link from 'next/link';
import { notFound } from 'next/navigation';

interface PageProps {
params: {
params: Promise<{
id: string;
};
}>;
}

export default async function FontDetailPage({ params }: PageProps) {
await requireAuth();

const { id } = await params;

// 获取字体详情(带关联数据)- 使用 normalizedName
const font = await fontService.findByNormalizedNameWithRelations(params.id);
const font = await fontService.findByNormalizedNameWithRelations(id);
if (!font) {
notFound();
}

View file

@ -45,8 +45,7 @@ export default async function FontManagementPage({ searchParams }: PageProps) {
const categoryId = params.categoryId || undefined;
const brandId = params.brandId || undefined;
const sort =
(params.sort as 'name' | 'viewCount' | 'downloadCount' | 'createdAt' | 'updatedAt') ||
'createdAt';
(params.sort as 'name' | 'viewCount' | 'downloadCount' | 'createdAt') || 'createdAt';
const order = (params.order as 'asc' | 'desc') || 'desc';

// 获取字体列表

17
app/admin/layout.tsx Normal file
View file

@ -0,0 +1,17 @@
import { getSession } from '@/lib/auth/session';
import { redirect } from 'next/navigation';

export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
const disableAuth = process.env.DISABLE_AUTH === 'true';

if (!disableAuth && !session?.user) {
redirect('/login');
}

return <>{children}</>;
}

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { handleApiError } from '@/lib/auth/api-guard';
import { brandService } from '@/lib/services/brand.service';
import { brandUpdateSchema } from '@/lib/services/validation';
import { handleApiError } from '@/lib/auth/api-guard';
import { NextRequest, NextResponse } from 'next/server';
import { ZodError } from 'zod';

/**
@ -9,9 +9,10 @@ import { ZodError } from 'zod';
* 获取品牌详情
* 公开访问
*/
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const brand = await brandService.findById(params.id);
const { id } = await params;
const brand = await brandService.findById(id);

if (!brand) {
return NextResponse.json(
@ -39,19 +40,20 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
* 更新品牌
* 需要管理员认证
*/
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 验证管理员权限
const { verifyAdmin } = await import('@/lib/auth/api-guard');
await verifyAdmin();

const { id } = await params;
const body = await req.json();

// 验证数据
const validated = brandUpdateSchema.parse(body);

// 更新品牌
const brand = await brandService.update(params.id, validated);
const brand = await brandService.update(id, validated);

return NextResponse.json({
code: 200,
@ -107,13 +109,14 @@ export async function PUT(req: NextRequest, { params }: { params: { id: string }
* 删除品牌
* 需要管理员认证
*/
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 验证管理员权限
const { verifyAdmin } = await import('@/lib/auth/api-guard');
await verifyAdmin();

await brandService.delete(params.id);
const { id } = await params;
await brandService.delete(id);

return NextResponse.json({
code: 200,

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { handleApiError } from '@/lib/auth/api-guard';
import { categoryService } from '@/lib/services/category.service';
import { categoryUpdateSchema } from '@/lib/services/validation';
import { handleApiError } from '@/lib/auth/api-guard';
import { NextRequest, NextResponse } from 'next/server';
import { ZodError } from 'zod';

/**
@ -9,9 +9,10 @@ import { ZodError } from 'zod';
* 获取分类详情
* 公开访问
*/
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const category = await categoryService.findById(params.id);
const { id } = await params;
const category = await categoryService.findById(id);

if (!category) {
return NextResponse.json(
@ -39,19 +40,20 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
* 更新分类
* 需要管理员认证
*/
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 验证管理员权限
const { verifyAdmin } = await import('@/lib/auth/api-guard');
await verifyAdmin();

const { id } = await params;
const body = await req.json();

// 验证数据
const validated = categoryUpdateSchema.parse(body);

// 更新分类
const category = await categoryService.update(params.id, validated);
const category = await categoryService.update(id, validated);

return NextResponse.json({
code: 200,
@ -107,13 +109,14 @@ export async function PUT(req: NextRequest, { params }: { params: { id: string }
* 删除分类
* 需要管理员认证
*/
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 验证管理员权限
const { verifyAdmin } = await import('@/lib/auth/api-guard');
await verifyAdmin();

await categoryService.delete(params.id);
const { id } = await params;
await categoryService.delete(id);

return NextResponse.json({
code: 200,

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { cssService } from '@/lib/services/css.service';
import { logger } from '@/lib/logger';
import { cssService } from '@/lib/services/css.service';
import { NextRequest, NextResponse } from 'next/server';
import { ZodError } from 'zod';

/**
@ -41,10 +41,13 @@ export async function GET(request: NextRequest) {
}

// 生成CSS
// 将 subset/lang 参数映射到 version
const version = (subset || lang || 'full') as 'en' | 'zh' | 'zh-common' | 'full';
const weight = searchParams.get('weight') || 'Regular';
const { css, etag } = await cssService.generateCSS({
family,
subset,
lang,
version,
weight,
});

// 检查条件请求If-None-Match

View file

@ -10,9 +10,9 @@ import { ZodError } from 'zod';
* 公开访问
* 支持通过 ID 或 normalizedName 查询
*/
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = params;
const { id } = await params;

// 尝试通过 normalizedName 查询,如果失败则通过 ID 查询
let font = await fontService.findByNormalizedName(id);
@ -50,13 +50,13 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
* 需要管理员认证
* 支持通过 ID 或 normalizedName 查询
*/
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 验证管理员权限
const { verifyAdmin } = await import('@/lib/auth/api-guard');
await verifyAdmin();

const { id } = params;
const { id } = await params;
const body = await req.json();

// 检查字体是否存在 - 支持 normalizedName 或 ID
@ -124,13 +124,13 @@ export async function PUT(req: NextRequest, { params }: { params: { id: string }
* 需要管理员认证
* 支持通过 ID 或 normalizedName 查询
*/
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 验证管理员权限
const { verifyAdmin } = await import('@/lib/auth/api-guard');
await verifyAdmin();

const { id } = params;
const { id } = await params;

// 检查字体是否存在 - 支持 normalizedName 或 ID
let existing = await fontService.findByNormalizedName(id);

12
app/api/health/route.ts Normal file
View file

@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.json(
{
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
},
{ status: 200 }
);
}

View file

@ -11,10 +11,7 @@ import { NextRequest, NextResponse } from 'next/server';
* - 统一缓存策略
* - 记录访问日志
*/
export async function GET(
request: NextRequest,
ctx: { params: { path: string[] } | Promise<{ path: string[] }> }
) {
export async function GET(request: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
try {
const { path } = await ctx.params;
const normalizedPath = path.join('/');

View file

@ -15,9 +15,10 @@ const styleUpdateSchema = z.object({
* 获取单个风格
* 公开访问
*/
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const style = await styleService.findById(params.id);
const { id } = await params;
const style = await styleService.findById(id);

if (!style) {
return NextResponse.json(
@ -45,62 +46,65 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
* 更新风格
* 需要管理员认证
*/
export const PUT = withAdmin(async (req: NextRequest, { params }: { params: { id: string } }) => {
try {
const body = await req.json();
export const PUT = withAdmin(
async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
try {
const { id } = await params;
const body = await req.json();

// 验证数据
const validated = styleUpdateSchema.parse(body);
// 验证数据
const validated = styleUpdateSchema.parse(body);

// 更新风格
const style = await styleService.update(params.id, validated);
// 更新风格
const style = await styleService.update(id, validated);

return NextResponse.json({
code: 200,
data: style,
message: '更新风格成功',
});
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json(
{
code: 400,
message: '数据验证失败',
status: 'fail',
errors: error.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
})),
},
{ status: 400 }
);
return NextResponse.json({
code: 200,
data: style,
message: '更新风格成功',
});
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json(
{
code: 400,
message: '数据验证失败',
status: 'fail',
errors: error.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
})),
},
{ status: 400 }
);
}

if (error instanceof Error && error.message.includes('不存在')) {
return NextResponse.json(
{
code: 404,
message: error.message,
status: 'fail',
},
{ status: 404 }
);
}

if (error instanceof Error && error.message.includes('已存在')) {
return NextResponse.json(
{
code: 422,
message: error.message,
status: 'fail',
},
{ status: 422 }
);
}

return handleApiError(error);
}

if (error instanceof Error && error.message.includes('不存在')) {
return NextResponse.json(
{
code: 404,
message: error.message,
status: 'fail',
},
{ status: 404 }
);
}

if (error instanceof Error && error.message.includes('已存在')) {
return NextResponse.json(
{
code: 422,
message: error.message,
status: 'fail',
},
{ status: 422 }
);
}

return handleApiError(error);
}
});
);

/**
* DELETE /api/styles/[id]
@ -108,9 +112,10 @@ export const PUT = withAdmin(async (req: NextRequest, { params }: { params: { id
* 需要管理员认证
*/
export const DELETE = withAdmin(
async (req: NextRequest, { params }: { params: { id: string } }) => {
async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
try {
await styleService.delete(params.id);
const { id } = await params;
await styleService.delete(id);

return NextResponse.json({
code: 200,

View file

@ -11,6 +11,48 @@ interface FaqItem {
}

const faqData: FaqItem[] = [
{
category: '费用与授权',
question: '使用文风字体需要付费吗?',
answer:
'不需要,现阶段所有文风在线字体均可免费使用。未来我们可能会尝试与字体厂商合作推出更多优质字体系列,以保证平台的正常运营和长期发展。',
},
{
category: '费用与授权',
question: '我可以在商业产品中使用文风字体吗?',
answer:
'可以但请注意部分字体存在设计师及公司的个性主观要求限制使用时您应仔细查看具体的使用协议和场景。字体授权查询https://wenfeng.org/license',
},
{
category: '费用与授权',
question: '如何知道一个字体是否可以商用?',
answer:
'在字体详情页面会明确标注授权类型。标记为"免费商用"的字体可以在商业项目中使用。如果标记为"个人免费"或"付费授权",则需要购买商业授权。使用前请务必查看详细的授权信息。',
},
{
category: '费用与授权',
question: '免费商用字体有什么限制吗?',
answer:
'大多数免费商用字体可以自由使用但通常有以下限制1) 不得单独出售字体文件2) 部分字体要求保留版权声明3) 不得修改字体后重新分发。具体限制请查看每个字体的授权协议。',
},
{
category: '平台使用',
question: '文风字体只支持中文字体包吗?',
answer:
'Windfonts 作为国内首个类 Google Fonts 的字体服务平台,我们的愿景是希望可以更好的为中文用户服务。目前收集的字体包大部分都支持中文,其中也包含支持英文字符的字体包。后续的改进和完善工作将会明显标记出支持中英文的标识、缺失字符等,并完善字体语言的分类。',
},
{
category: '平台使用',
question: '我可以在哪些平台使用?',
answer:
'Web网页 Web 端可直接引入当前字体的 CSS 文件使用。WordPress我们针对 WordPress 用户专门开发了插件,请下载文派叶子🍃(WP-China-Yes) 插件安装即用。CMS任何建站系统均可自行对接 Windfonts API 来实现自定义功能集成。APP 及小程序请访问文风开源字体Wenfeng.org下载字体切片包或源文件以满足您的嵌入要求。',
},
{
category: '平台使用',
question: '如何实现使用某款字体包时仅对中文字样起作用?',
answer:
'当您引入字体代码的 CSS 样式文件时,请参照"开始使用"的示例代码,只需要引入仅包含中文字符的 CSS 样式文件即可。',
},
{
category: '基础使用',
question: '如何在我的网站中使用字体?',
@ -27,25 +69,13 @@ const faqData: FaqItem[] = [
category: '基础使用',
question: '可以同时使用多个字体吗?',
answer:
'可以。使用 | 符号分隔多个字体名称,例如:/api/css?family=思源黑体|思源宋体。但建议不要同时加载过多字体,以免影响页面加载性能。',
'可以。使用 | 符号分隔多个字体名称,例如:/api/css?family=WF-Qtxtt|WF-Hxbsb。但建议不要同时加载过多字体以免影响页面加载性能。',
},
{
category: '授权相关',
question: '如何知道一个字体是否可以商用?',
category: '技术问题',
question: '什么是可变字体?',
answer:
'在字体详情页面会明确标注授权类型。标记为"免费商用"的字体可以在商业项目中使用。如果标记为"个人免费"或"付费授权",则需要购买商业授权。使用前请务必查看详细的授权信息。',
},
{
category: '授权相关',
question: '免费商用字体有什么限制吗?',
answer:
'大多数免费商用字体可以自由使用但通常有以下限制1) 不得单独出售字体文件2) 部分字体要求保留版权声明3) 不得修改字体后重新分发。具体限制请查看每个字体的授权协议。',
},
{
category: '授权相关',
question: '如何购买付费字体的授权?',
answer:
'在字体详情页面会显示购买链接或联系方式。您可以直接访问字体厂商的官方网站购买授权,或联系版权方获取报价。购买后请保留授权凭证。',
'可变字体是排版领域的新近发展。所有样式都仅存储在一个或两个字体文件中,而不是每个样式都存储在单独的文件中。文风字体将会在未来版本中提供可变字体支持。',
},
{
category: '技术问题',
@ -58,12 +88,6 @@ const faqData: FaqItem[] = [
question: 'CSS API 支持 HTTPS 吗?',
answer: '是的CSS API 完全支持 HTTPS。建议在生产环境中使用 HTTPS 以确保安全性。',
},
{
category: '技术问题',
question: '可以下载字体文件到本地使用吗?',
answer:
'为了保护字体版权和确保授权合规,我们不提供字体文件的直接下载。请使用 CSS API 在线加载字体。如需离线使用,请联系字体版权方获取授权。',
},
{
category: '技术问题',
question: 'API 有请求频率限制吗?',
@ -71,34 +95,52 @@ const faqData: FaqItem[] = [
'目前 CSS API 没有严格的频率限制,但我们建议合理使用。如果您的项目有大量请求需求,建议利用浏览器缓存机制,避免重复请求相同的资源。',
},
{
category: '账号管理',
question: '需要注册账号才能使用字体吗?',
category: '字体管理',
question: '为什么没有 XXX 免费商用字体?',
answer:
'不需要。浏览和使用字体不需要注册账号。只有管理员需要登录才能管理字体、品牌和分类等后台功能。',
'大部分原因可能在于字体厂商的授权方式不允许提供类似 Web 字体服务,所以文风字体不提供这类字体的引用。免费字体并非开源字体,使用时仍需注意区分。',
},
{
category: '账号管理',
question: '如何成为管理员?',
answer: '管理员账号由系统管理员通过环境变量配置。如果您需要管理权限,请联系系统管理员。',
category: '字体管理',
question: '我想要使用的字体没有,如何提交?',
answer:
'如果您确认字体的授权没有问题Windfonts.com 乐意收录该中文字体提供服务,请通过支持论坛或联系我们发送相关的文件下载及授权信息地址。审核无误后将会上传至字库系统进行切片处理。',
},
{
category: '其他问题',
category: '字体管理',
question: '我是字体作者/厂商,发现某款字体有问题?',
answer:
'请通过文风支持论坛、邮箱、联系表单、任何网站公开的方式联系我们,此问题将会在第一时间得到跟进处理。如果您确信需要对某款字体进行下架,根据《民法典》要求您可通过提交侵权投诉或邮件等书面形式发送您(作者信息)及您作品的权利证明(包括但不限于可访问授权网址、版权证书、授权协议等),工作人员确认无误后我们将会下线此字体。虽然对此表示遗憾,但我们尊重作者本身的权利。注意:您的邮件或权利证明信息将在脱敏后面向用户公示,以做记录存证方便未来公众查询、避免再次因字体授权方式不明确导致的侵权行为。',
},
{
category: '字体管理',
question: '我可以在 Windfonts 上传自己的商用授权字体吗?',
answer: '现阶段不可以,但您仍然值得期待此功能,我们将会在未来开发计划中考虑这一可能性。',
},
{
category: '字体管理',
question: '字体列表多久更新一次?',
answer:
'字体列表通过 OSS 同步功能更新。管理员可以手动触发同步,或设置定期自动同步。新增的字体会在同步后立即显示在列表中。',
},
{
category: '字体管理',
question: '发现字体信息错误怎么办?',
answer:
'如果发现字体信息有误,请联系管理员。管理员可以在后台编辑字体信息,或重新同步 OSS 数据以更新信息。',
},
{
category: '开源与开发',
question: 'Windfonts 字体服务器是开源的吗?',
answer:
'Windfonts Webfonts Server 的诞生离不开各种开源项目的支持,所以这也将会是一款可以自托管的字体服务器软件。我们计划不仅限于支持中英文字体,对 CJK 中日汉简繁及特殊字符字库均有考量。由于现在还处于开发早期,有很多问题需要处理和完善,具体的源代码版本稳定后将全部开源。具体时间待定,您可以关注文风字体官方新闻通告。',
},
{
category: '其他问题',
question: '如何搜索字体?',
answer:
'在字体列表页面使用搜索框输入关键词,系统会搜索字体名称、品牌名称和标签。您也可以使用筛选器按分类、品牌等条件筛选字体。',
},
{
category: '其他问题',
question: '发现字体信息错误怎么办?',
answer:
'如果发现字体信息有误,请联系管理员。管理员可以在后台编辑字体信息,或重新同步 OSS 数据以更新信息。',
},
];

export function FaqSection() {
@ -163,17 +205,41 @@ export function FaqSection() {
</CardHeader>
<CardContent>
<p className="text-muted-foreground mb-4 text-sm">
如果您的问题没有在上面列出,或需要更多帮助,请联系我们的技术支持团队。
如果您的问题没有在上面列出,或需要更多帮助,请通过以下方式联系我们。
</p>
<div className="space-y-2 text-sm">
<p>
<strong>邮箱:</strong>{' '}
<a href="mailto:support@example.com" className="text-primary hover:underline">
support@example.com
<strong>官方网站:</strong>{' '}
<a
href="https://wenfeng.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://wenfeng.org
</a>
</p>
<p>
<strong>工作时间:</strong> 周一至周五 9:00 - 18:00
<strong>授权查询:</strong>{' '}
<a
href="https://wenfeng.org/license"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://wenfeng.org/license
</a>
</p>
<p>
<strong>GitHub</strong>{' '}
<a
href="https://feicode.com/Windfonts/fonts-vault"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://feicode.com/Windfonts/fonts-vault
</a>
</p>
</div>
</CardContent>

View file

@ -30,9 +30,9 @@ export function FontDetailContent({ font, relatedFonts, analysis }: FontDetailCo
// Get available weights from font data
const availableWeights = font.weights
? Object.values(font.weights).map((w) => ({
name: w.weight_name,
value: w.font_weight,
}))
name: w.weight_name,
value: w.font_weight,
}))
: [];

const firstWeightName = font.weights ? Object.keys(font.weights)[0] : 'Regular';
@ -279,11 +279,10 @@ export function FontDetailContent({ font, relatedFonts, analysis }: FontDetailCo
<button
key={weight.value}
onClick={() => setSelectedWeight(weight.name)}
className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${
selectedWeight === weight.name
className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${selectedWeight === weight.name
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent'
}`}
}`}
>
{weight.name}
</button>
@ -811,9 +810,10 @@ const MyComponent = () => (
{
((localAnalysis || analysis)?.total_char_count ??
(localAnalysis || analysis)?.char_count ??
(localAnalysis || analysis)?.chars?.length ??
(localAnalysis || analysis)?.characters?.length ??
'-') as any
((localAnalysis || analysis)?.chars as string[] | undefined)?.length ??
((localAnalysis || analysis)?.characters as string[] | undefined)
?.length ??
'-') as string | number
}
</div>
</div>

View file

@ -6,31 +6,8 @@ import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import { FontDetailContent } from './font-detail-content';

// ISR: 每小时重新生成页面
export const revalidate = 3600;

// 动态路由参数
export const dynamicParams = true;

// 生成静态参数 - 预渲染热门字体
export async function generateStaticParams() {
try {
// 获取前20个最受欢迎的字体进行预渲染
const fonts = await fontService.findAll({
page: 1,
size: 20,
sort: 'viewCount',
order: 'desc',
});

return fonts.dataList.map((font) => ({
id: font.normalizedName,
}));
} catch (error) {
console.error('Error generating static params:', error);
return [];
}
}
// 动态渲染,避免构建时查询数据库
export const dynamic = 'force-dynamic';

export default async function FontDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
@ -53,14 +30,24 @@ export default async function FontDetailPage({ params }: { params: Promise<{ id:
});

// Filter out current font from related fonts
const filteredRelatedFonts = relatedFonts.dataList.filter((f) => f.id !== font.id);
const filteredRelatedFonts = relatedFonts.dataList
.filter((f) => f.id !== font.id)
.map((f) => ({
...f,
brand: f.brand ?? null,
category: f.category ?? null,
}));

const analysis = await syncService.fetchFontAnalysis(font.normalizedName);

return (
<PublicLayout>
<Suspense fallback={<FontDetailSkeleton />}>
<FontDetailContent font={font} relatedFonts={filteredRelatedFonts} analysis={analysis} />
<FontDetailContent
font={font}
relatedFonts={filteredRelatedFonts as Parameters<typeof FontDetailContent>[0]['relatedFonts']}
analysis={analysis}
/>
</Suspense>
</PublicLayout>
);

View file

@ -5,19 +5,10 @@ import { categoryService } from '@/lib/services/category.service';
import { Suspense } from 'react';
import { FontListContent } from './font-list-content';

// ISR: 每小时重新生成页面
export const revalidate = 3600;
// 动态渲染,避免构建时查询数据库
export const dynamic = 'force-dynamic';

// 生成静态参数
export async function generateStaticParams() {
return [{ searchParams: {} }];
}

export default async function FontsPage({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
export default async function FontsPage() {
// 获取分类和品牌数据用于筛选器
const [categories, brands] = await Promise.all([
categoryService.findAll(),

View file

@ -41,8 +41,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);

View file

@ -2,20 +2,11 @@ import { Toaster } from '@/components/ui/sonner';
import { loadFont } from '@windfonts/chinese-fonts';
import type { Metadata, Viewport } from 'next';
import { SessionProvider } from 'next-auth/react';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
loadFont('Hclcks-Regular', { subset: 'zh-common' });
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});

const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});

export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || 'https://fonts.wptea.com'),
title: {
default: '文风字库',
template: '%s | 文风字库',
@ -72,9 +63,7 @@ export default function RootLayout({
}>) {
return (
<html lang="zh-CN" className="overscroll-none">
<body
className={`${geistSans.variable} ${geistMono.variable} touch-pan-y overscroll-none antialiased`}
>
<body className="touch-pan-y overscroll-none antialiased">
<SessionProvider>{children}</SessionProvider>
<Toaster />
</body>

View file

@ -7,8 +7,8 @@ import { fontService } from '@/lib/services/font.service';
import { ArrowRight, Palette, Search, Shield, Zap } from 'lucide-react';
import Link from 'next/link';

// ISR: 每小时重新生成首页
export const revalidate = 3600;
// 动态渲染,避免构建时查询数据库
export const dynamic = 'force-dynamic';

export default async function Home() {
// 获取推荐字体(热门字体,带关联数据)
@ -19,7 +19,7 @@ export default async function Home() {

return (
<PublicLayout>
<div className="container py-8 md:py-16">
<div className="container mx-auto py-8 md:py-16">
{/* Hero Section - 平台介绍区域 */}
<section className="mb-16 text-center">
<div className="mx-auto max-w-4xl">

View file

@ -1,123 +0,0 @@
import { auth } from '@/lib/auth/config';
import { NextResponse } from 'next/server';

/**
* Next.js Middleware for authentication and authorization
*
* Protected routes:
* - /admin/* - Requires authentication
* - API routes with POST, PUT, DELETE methods - Requires authentication
*
* Public routes:
* - / - Home page
* - /fonts/* - Font browsing and details
* - /docs/* - Documentation
* - /login - Login page
* - GET /api/fonts - Font listing
* - GET /api/css - CSS generation
* - GET /api/brands - Brand listing
* - GET /api/categories - Category listing
*/

// Define public paths that don't require authentication
const publicPaths = ['/', '/login', '/fonts', '/docs'];

// Define public API paths (GET only)
const publicApiPaths = ['/api/auth', '/api/fonts', '/api/css', '/api/brands', '/api/categories'];

// Check if a path matches any pattern in the list
function matchesPath(pathname: string, patterns: string[]): boolean {
return patterns.some((pattern) => {
if (pattern.endsWith('*')) {
return pathname.startsWith(pattern.slice(0, -1));
}
return pathname === pattern || pathname.startsWith(`${pattern}/`);
});
}

export default auth((req) => {
const { pathname } = req.nextUrl;
const isLoggedIn = !!req.auth;
const disableAuth = process.env.DISABLE_AUTH === 'true';

// Allow access to static files and Next.js internals
if (pathname.startsWith('/_next') || pathname.startsWith('/static') || pathname.includes('.')) {
return NextResponse.next();
}

// Check if this is an admin route
const isAdminRoute = pathname.startsWith('/admin');

// Check if this is an API route
const isApiRoute = pathname.startsWith('/api');

// Handle admin routes - require authentication
if (isAdminRoute) {
if (disableAuth) {
return NextResponse.next();
}
if (!isLoggedIn) {
const loginUrl = new URL('/login', req.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}

// Handle API routes
if (isApiRoute) {
const method = req.method;

// Allow all auth-related API calls
if (pathname.startsWith('/api/auth')) {
return NextResponse.next();
}

// For public API paths, allow GET requests without authentication
if (method === 'GET' && matchesPath(pathname, publicApiPaths)) {
return NextResponse.next();
}

// All other API routes (POST, PUT, DELETE, or non-public GET) require authentication
if (!isLoggedIn && !disableAuth) {
return NextResponse.json(
{
code: 401,
message: '未授权访问,请先登录',
status: 'error',
},
{ status: 401 }
);
}

return NextResponse.next();
}

// Handle public routes - allow access without authentication
if (matchesPath(pathname, publicPaths)) {
return NextResponse.next();
}

// Redirect to login if trying to access protected route without authentication
if (!isLoggedIn) {
const loginUrl = new URL('/login', req.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}

return NextResponse.next();
});

// Configure which routes the middleware should run on
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public files (public folder)
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};

View file

@ -1,15 +1,8 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
// Webpack 配置
webpack: (config) => {
// 排除 LICENSE 文件
config.module.rules.push({
test: /LICENSE$/,
type: 'asset/source',
});
return config;
},
// Turbopack 配置 (Next.js 16 默认使用 Turbopack)
turbopack: {},

// 图片优化配置
images: {
@ -53,6 +46,9 @@ const nextConfig: NextConfig = {
poweredByHeader: false,
compress: true,

// Docker 部署配置
output: 'standalone',

// 静态资源缓存
async headers() {
return [

2660
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
@ -35,11 +35,11 @@
"drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.36.4",
"lucide-react": "^0.554.0",
"next": "15.5.6",
"next": "^16.0.7",
"next-auth": "^5.0.0-beta.29",
"next-themes": "^0.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.66.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
@ -54,9 +54,10 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.6",
"eslint-config-next": "^16.0.7",
"eslint-config-prettier": "^9.1.2",
"husky": "^9.1.7",
"ignore-loader": "^0.1.2",
"lint-staged": "^15.5.2",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

76
scripts/docker-all.sh Executable file
View file

@ -0,0 +1,76 @@
#!/bin/bash

# 一键构建、推送、部署脚本
set -e

# 颜色输出
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 文风字库 Docker 一键部署${NC}"
echo -e "${BLUE}========================================${NC}"

# 检查 REGISTRY 环境变量
if [ -z "$DOCKER_REGISTRY" ]; then
echo -e "${YELLOW}警告: 未设置 DOCKER_REGISTRY 环境变量${NC}"
echo -e "${YELLOW}将只构建本地镜像,不推送到远程仓库${NC}"
read -p "是否继续? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi

# 步骤 1: 构建镜像
echo -e "\n${BLUE}[1/3] 构建 Docker 镜像...${NC}"
./scripts/docker-build.sh

# 步骤 2: 推送镜像(如果设置了 REGISTRY
if [ -n "$DOCKER_REGISTRY" ]; then
echo -e "\n${BLUE}[2/3] 推送镜像到仓库...${NC}"
./scripts/docker-push.sh
else
echo -e "\n${YELLOW}[2/3] 跳过推送步骤${NC}"
fi

# 步骤 3: 询问是否部署
echo -e "\n${BLUE}[3/3] 部署选项${NC}"
echo -e "${YELLOW}选择部署方式:${NC}"
echo " 1) 本地部署 (docker-compose)"
echo " 2) 远程部署 (需要 SSH 配置)"
echo " 3) 跳过部署"
read -p "请选择 (1-3): " -n 1 -r
echo

case $REPLY in
1)
echo -e "${GREEN}启动本地部署...${NC}"
docker-compose up -d
echo -e "${GREEN}✓ 本地部署完成!${NC}"
echo -e "${BLUE}访问地址: http://localhost:3000${NC}"
;;
2)
read -p "请输入服务器地址 (user@host): " SERVER
if [ -n "$SERVER" ]; then
echo -e "${GREEN}部署到远程服务器: ${SERVER}${NC}"
ssh $SERVER "cd ~/windfonts-vault && ./docker-deploy.sh"
echo -e "${GREEN}✓ 远程部署完成!${NC}"
else
echo -e "${RED}未输入服务器地址,跳过部署${NC}"
fi
;;
3)
echo -e "${YELLOW}跳过部署步骤${NC}"
;;
*)
echo -e "${RED}无效选择,跳过部署${NC}"
;;
esac

echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN} 部署流程完成!${NC}"
echo -e "${GREEN}========================================${NC}"

28
scripts/docker-build.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash

# Docker 构建脚本
set -e

# 颜色输出
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color

echo -e "${BLUE}开始构建 Docker 镜像...${NC}"

# 获取版本号(从 package.json
VERSION=$(node -p "require('./package.json').version")
IMAGE_NAME="windfonts-vault"
REGISTRY="${DOCKER_REGISTRY:-xianyu12580}"

# 构建镜像
echo -e "${GREEN}构建镜像: ${REGISTRY}/${IMAGE_NAME}:${VERSION}${NC}"
docker build -t ${REGISTRY}/${IMAGE_NAME}:${VERSION} -t ${REGISTRY}/${IMAGE_NAME}:latest .

# 同时打本地标签(方便本地测试)
docker tag ${REGISTRY}/${IMAGE_NAME}:latest ${IMAGE_NAME}:latest

echo -e "${GREEN}✓ 构建完成!${NC}"
echo -e "${BLUE}镜像列表:${NC}"
docker images | grep ${IMAGE_NAME}

61
scripts/docker-deploy.sh Executable file
View file

@ -0,0 +1,61 @@
#!/bin/bash

# Docker 部署脚本(用于云端服务器)
set -e

# 颜色输出
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# 配置
IMAGE_NAME="windfonts-vault"
CONTAINER_NAME="windfonts-vault"
REGISTRY="${DOCKER_REGISTRY:-}"
VERSION="${VERSION:-latest}"

echo -e "${BLUE}开始部署 ${IMAGE_NAME}...${NC}"

# 停止并删除旧容器
if [ "$(docker ps -aq -f name=${CONTAINER_NAME})" ]; then
echo -e "${GREEN}停止旧容器...${NC}"
docker stop ${CONTAINER_NAME} || true
docker rm ${CONTAINER_NAME} || true
fi

# 拉取最新镜像
if [ -n "$REGISTRY" ]; then
echo -e "${GREEN}拉取镜像: ${REGISTRY}/${IMAGE_NAME}:${VERSION}${NC}"
docker pull ${REGISTRY}/${IMAGE_NAME}:${VERSION}
IMAGE_TAG="${REGISTRY}/${IMAGE_NAME}:${VERSION}"
else
IMAGE_TAG="${IMAGE_NAME}:${VERSION}"
fi

# 启动新容器
echo -e "${GREEN}启动新容器...${NC}"
docker run -d \
--name ${CONTAINER_NAME} \
--restart unless-stopped \
-p 3000:3000 \
-v $(pwd)/data:/app/data \
-v $(pwd)/logs:/app/logs \
--env-file .env.production \
${IMAGE_TAG}

# 等待容器启动
echo -e "${BLUE}等待容器启动...${NC}"
sleep 5

# 检查容器状态
if [ "$(docker ps -q -f name=${CONTAINER_NAME})" ]; then
echo -e "${GREEN}✓ 部署成功!${NC}"
echo -e "${BLUE}容器状态:${NC}"
docker ps -f name=${CONTAINER_NAME}
echo -e "${BLUE}查看日志: docker logs -f ${CONTAINER_NAME}${NC}"
else
echo -e "${RED}✗ 部署失败!${NC}"
echo -e "${BLUE}查看日志: docker logs ${CONTAINER_NAME}${NC}"
exit 1
fi

30
scripts/docker-push.sh Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash

# Docker 推送脚本
set -e

# 颜色输出
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# 获取版本号
VERSION=$(node -p "require('./package.json').version")
IMAGE_NAME="windfonts-vault"
REGISTRY="${DOCKER_REGISTRY:-xianyu12580}"

echo -e "${BLUE}开始推送镜像到 ${REGISTRY}...${NC}"

# 推送版本标签
echo -e "${GREEN}推送: ${REGISTRY}/${IMAGE_NAME}:${VERSION}${NC}"
docker push ${REGISTRY}/${IMAGE_NAME}:${VERSION}

# 推送 latest 标签
echo -e "${GREEN}推送: ${REGISTRY}/${IMAGE_NAME}:latest${NC}"
docker push ${REGISTRY}/${IMAGE_NAME}:latest

echo -e "${GREEN}✓ 推送完成!${NC}"
echo -e "${BLUE}镜像地址:${NC}"
echo -e " ${REGISTRY}/${IMAGE_NAME}:${VERSION}"
echo -e " ${REGISTRY}/${IMAGE_NAME}:latest"

15
scripts/start.sh Normal file
View file

@ -0,0 +1,15 @@
#!/bin/sh
set -e

# 加载环境变量
if [ -f .env.production ]; then
echo "Loading .env.production..."
export $(grep -v '^#' .env.production | xargs)
fi

echo "Starting server with environment:"
echo " NEXTAUTH_URL=$NEXTAUTH_URL"
echo " AUTH_TRUST_HOST=$AUTH_TRUST_HOST"
echo " DATABASE_URL=$DATABASE_URL"

exec node server.js

View file

@ -12,8 +12,8 @@ export const EXTRA_LINKS = [
path: 'https://admincdn.com',
},
{
label: '方块字库',
path: 'https://fontsquare.com',
label: '赛博字库',
path: 'https://cyberfonts.com',
},
{
label: '文派开源',

View file

@ -1,4 +1,5 @@
import { auth } from '@/lib/auth/config';
import { redirect } from 'next/navigation';

export async function getSession() {
const disableAuth = process.env.DISABLE_AUTH === 'true';
@ -16,11 +17,15 @@ export async function getSession() {
return await auth();
}

/**
* Require authentication for server components
* Redirects to login if not authenticated
*/
export async function requireAuth() {
const session = await getSession();

if (!session?.user) {
throw new Error('未授权访问');
redirect('/login');
}

return session;

View file

@ -11,14 +11,15 @@ interface CacheEntry {
export class CSSService {
private cache: Map<string, CacheEntry> = new Map();
private cacheTTL: number = 30 * 60 * 1000; // 30分钟
private proxyBaseUrl: string;
private cdnBaseUrl: string;

constructor() {
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
this.proxyBaseUrl = `${baseUrl}/api/proxy`;
private get proxyBaseUrl(): string {
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:4000';
return `${baseUrl}/api/proxy`;
}

private get cdnBaseUrl(): string {
const cdnEnv = process.env.NEXT_PUBLIC_FONT_STATIC_URL;
this.cdnBaseUrl = (cdnEnv && cdnEnv.replace(/\/$/, '')) || '';
return (cdnEnv && cdnEnv.replace(/\/$/, '')) || '';
}

/**

View file

@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@ -19,13 +23,31 @@
}
],
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/types/*": ["./src/types/*"],
"@/hooks/*": ["./src/hooks/*"]
"@/*": [
"./src/*"
],
"@/components/*": [
"./src/components/*"
],
"@/lib/*": [
"./src/lib/*"
],
"@/types/*": [
"./src/types/*"
],
"@/hooks/*": [
"./src/hooks/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}