Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
117a934
feat: Add Vercel KV support & external subscription config
liiider Feb 9, 2026
bd41aae
fix: Add support for Vercel Marketplace Upstash env vars
liiider Feb 9, 2026
6c76892
fix: add force-dynamic to API routes to fix Vercel static rendering e…
liiider Feb 9, 2026
9322673
chore: rename site to CraterTV
liiider Feb 9, 2026
139f8db
chore: update more SiteName references to CraterTV
liiider Feb 9, 2026
44d7beb
merge: sync upstream MoonTechLab changes
Mar 30, 2026
45e1242
fix: point version links to liiider repo
Mar 30, 2026
bf589f2
merge: sync upstream MoonTechLab changes
Mar 30, 2026
06761f9
fix: enforce source permissions by user group
May 4, 2026
087870f
fix: narrow group source permissions type
May 4, 2026
b49dc2d
fix: prevent stale source permissions
May 4, 2026
782ce91
fix: tighten role permission boundaries
May 4, 2026
75aa7e2
fix: remove anime daily broadcast category
May 4, 2026
de3f4c3
docs: rewrite readme for current deployment
May 4, 2026
3ecbbff
fix: restrict search to canonical titles
May 4, 2026
cb470c5
feat: configure group safe presearch source
May 4, 2026
88f62e4
Refine user permissions and simplify anime categories
liiider May 4, 2026
ab50ee6
fix: use douban anime recommendations on home
May 5, 2026
81dcd78
docs: merge readme updates
May 5, 2026
ec095ba
fix: pass empty douban anime filters
May 5, 2026
af3d375
feat: add catalog-backed safe search
Aug 1, 2026
44726a0
fix: persist user group changes on first save
Aug 1, 2026
0bce3b2
fix: prevent stale config read writeback
Aug 1, 2026
91e762e
feat: add jade mint and cinema burgundy themes
Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copy this file to .env.local and fill in your own values.
USERNAME=admin
PASSWORD=replace-with-a-strong-password

# Server-only TMDB API Read Access Token. Never use a NEXT_PUBLIC_ prefix.
TMDB_API_READ_TOKEN=your-token-here
492 changes: 159 additions & 333 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "moontv",
"name": "creatertv",
"version": "0.1.0",
"private": true,
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const publicDir = path.join(projectRoot, 'public');
const manifestPath = path.join(publicDir, 'manifest.json');

// 从环境变量获取站点名称
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'CraterTV';

// manifest.json 模板
const manifestTemplate = {
Expand Down
3,268 changes: 2,093 additions & 1,175 deletions src/app/admin/page.tsx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/app/api/admin/category/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';


// 支持的操作类型
type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort';
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/admin/config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
Expand All @@ -26,7 +27,9 @@ export async function GET(request: NextRequest) {
const username = authInfo.username;

try {
const config = await getConfig();
// 管理页保存后的读取必须绕过进程内缓存。Vercel 的读写请求可能
// 落在不同实例上,否则刷新会读到另一个实例中的旧配置。
const config = await getConfig({ forceRefresh: true });
const result: AdminConfigResult = {
Role: 'owner',
Config: config,
Expand Down
7 changes: 2 additions & 5 deletions src/app/api/admin/site/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export async function POST(request: NextRequest) {
DoubanProxy,
DoubanImageProxyType,
DoubanImageProxy,
DisableYellowFilter,
FluidSearch,
EnableWebLive,
} = body as {
Expand All @@ -49,7 +48,6 @@ export async function POST(request: NextRequest) {
DoubanProxy: string;
DoubanImageProxyType: string;
DoubanImageProxy: string;
DisableYellowFilter: boolean;
FluidSearch: boolean;
EnableWebLive: boolean;
};
Expand All @@ -64,8 +62,8 @@ export async function POST(request: NextRequest) {
typeof DoubanProxy !== 'string' ||
typeof DoubanImageProxyType !== 'string' ||
typeof DoubanImageProxy !== 'string' ||
typeof DisableYellowFilter !== 'boolean' ||
typeof FluidSearch !== 'boolean'
typeof FluidSearch !== 'boolean' ||
typeof EnableWebLive !== 'boolean'
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
Expand Down Expand Up @@ -93,7 +91,6 @@ export async function POST(request: NextRequest) {
DoubanProxy,
DoubanImageProxyType,
DoubanImageProxy,
DisableYellowFilter,
FluidSearch,
EnableWebLive: EnableWebLive ?? false,
};
Expand Down
73 changes: 56 additions & 17 deletions src/app/api/admin/source/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ import { db } from '@/lib/db';
export const runtime = 'nodejs';

// 支持的操作类型
type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort' | 'batch_disable' | 'batch_enable' | 'batch_delete';
type Action =
| 'add'
| 'disable'
| 'enable'
| 'delete'
| 'sort'
| 'batch_disable'
| 'batch_enable'
| 'batch_delete';

interface BaseBody {
action?: Action;
Expand Down Expand Up @@ -37,7 +45,16 @@ export async function POST(request: NextRequest) {
const username = authInfo.username;

// 基础校验
const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort', 'batch_disable', 'batch_enable', 'batch_delete'];
const ACTIONS: Action[] = [
'add',
'disable',
'enable',
'delete',
'sort',
'batch_disable',
'batch_enable',
'batch_delete',
];
if (!username || !action || !ACTIONS.includes(action)) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
Expand Down Expand Up @@ -115,27 +132,33 @@ export async function POST(request: NextRequest) {
// 检查并清理用户组和用户的权限数组
// 清理用户组权限
if (adminConfig.UserConfig.Tags) {
adminConfig.UserConfig.Tags.forEach(tag => {
adminConfig.UserConfig.Tags.forEach((tag) => {
if (tag.enabledApis) {
tag.enabledApis = tag.enabledApis.filter(api => api !== key);
tag.enabledApis = tag.enabledApis.filter((api) => api !== key);
}
if (tag.safeSearchApi === key) {
delete tag.safeSearchApi;
}
});
}

// 清理用户权限
adminConfig.UserConfig.Users.forEach(user => {
adminConfig.UserConfig.Users.forEach((user) => {
if (user.enabledApis) {
user.enabledApis = user.enabledApis.filter(api => api !== key);
user.enabledApis = user.enabledApis.filter((api) => api !== key);
}
});
break;
}
case 'batch_disable': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys) || keys.length === 0) {
return NextResponse.json({ error: '缺少 keys 参数或为空' }, { status: 400 });
return NextResponse.json(
{ error: '缺少 keys 参数或为空' },
{ status: 400 }
);
}
keys.forEach(key => {
keys.forEach((key) => {
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
if (entry) {
entry.disabled = true;
Expand All @@ -146,9 +169,12 @@ export async function POST(request: NextRequest) {
case 'batch_enable': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys) || keys.length === 0) {
return NextResponse.json({ error: '缺少 keys 参数或为空' }, { status: 400 });
return NextResponse.json(
{ error: '缺少 keys 参数或为空' },
{ status: 400 }
);
}
keys.forEach(key => {
keys.forEach((key) => {
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
if (entry) {
entry.disabled = false;
Expand All @@ -159,16 +185,19 @@ export async function POST(request: NextRequest) {
case 'batch_delete': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys) || keys.length === 0) {
return NextResponse.json({ error: '缺少 keys 参数或为空' }, { status: 400 });
return NextResponse.json(
{ error: '缺少 keys 参数或为空' },
{ status: 400 }
);
}
// 过滤掉 from=config 的源,但不报错
const keysToDelete = keys.filter(key => {
const keysToDelete = keys.filter((key) => {
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
return entry && entry.from !== 'config';
});

// 批量删除
keysToDelete.forEach(key => {
keysToDelete.forEach((key) => {
const idx = adminConfig.SourceConfig.findIndex((s) => s.key === key);
if (idx !== -1) {
adminConfig.SourceConfig.splice(idx, 1);
Expand All @@ -179,17 +208,27 @@ export async function POST(request: NextRequest) {
if (keysToDelete.length > 0) {
// 清理用户组权限
if (adminConfig.UserConfig.Tags) {
adminConfig.UserConfig.Tags.forEach(tag => {
adminConfig.UserConfig.Tags.forEach((tag) => {
if (tag.enabledApis) {
tag.enabledApis = tag.enabledApis.filter(api => !keysToDelete.includes(api));
tag.enabledApis = tag.enabledApis.filter(
(api) => !keysToDelete.includes(api)
);
}
if (
tag.safeSearchApi &&
keysToDelete.includes(tag.safeSearchApi)
) {
delete tag.safeSearchApi;
}
});
}

// 清理用户权限
adminConfig.UserConfig.Users.forEach(user => {
adminConfig.UserConfig.Users.forEach((user) => {
if (user.enabledApis) {
user.enabledApis = user.enabledApis.filter(api => !keysToDelete.includes(api));
user.enabledApis = user.enabledApis.filter(
(api) => !keysToDelete.includes(api)
);
}
});
}
Expand Down
11 changes: 10 additions & 1 deletion src/app/api/admin/source/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export async function GET(request: NextRequest) {
}

const config = await getConfig();
if (authInfo.username !== process.env.USERNAME) {
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (!user || user.role !== 'admin' || user.banned) {
return NextResponse.json({ error: '权限不足' }, { status: 401 });
}
}

const apiSites = config.SourceConfig;

// 共享状态
Expand Down Expand Up @@ -189,7 +198,7 @@ export async function GET(request: NextRequest) {
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Cache-Control': 'no-store',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
Expand Down
Loading