Suwayomi认证方式重构

This commit is contained in:
mtvpls
2026-04-17 22:50:58 +08:00
parent 92a7a0d8a7
commit 7fb11f0ff7
6 changed files with 378 additions and 27 deletions
+94 -11
View File
@@ -10127,7 +10127,9 @@ const SuwayomiConfigComponent = ({
const { isLoading, withLoading } = useLoadingState();
const [enabled, setEnabled] = useState(false);
const [serverURL, setServerURL] = useState('');
const [authToken, setAuthToken] = useState('');
const [authMode, setAuthMode] = useState<'none' | 'basic_auth' | 'simple_login'>('none');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [defaultLang, setDefaultLang] = useState('zh');
const [sourceIds, setSourceIds] = useState('');
const [maxSources, setMaxSources] = useState(10);
@@ -10136,7 +10138,9 @@ const SuwayomiConfigComponent = ({
if (config?.SuwayomiConfig) {
setEnabled(config.SuwayomiConfig.Enabled || false);
setServerURL(config.SuwayomiConfig.ServerURL || '');
setAuthToken(config.SuwayomiConfig.AuthToken || '');
setAuthMode(config.SuwayomiConfig.AuthMode || 'none');
setUsername(config.SuwayomiConfig.Username || '');
setPassword(config.SuwayomiConfig.Password || '');
setDefaultLang(config.SuwayomiConfig.DefaultLang || 'zh');
setSourceIds((config.SuwayomiConfig.SourceIds || []).join(','));
setMaxSources(config.SuwayomiConfig.MaxSources || 10);
@@ -10146,7 +10150,9 @@ const SuwayomiConfigComponent = ({
const buildConfig = () => ({
Enabled: enabled,
ServerURL: serverURL,
AuthToken: authToken,
AuthMode: authMode,
Username: authMode === 'none' ? '' : username,
Password: authMode === 'none' ? '' : password,
DefaultLang: defaultLang || 'zh',
SourceIds: sourceIds.split(',').map((item) => item.trim()).filter(Boolean),
MaxSources: Math.max(1, maxSources || 10),
@@ -10180,6 +10186,34 @@ const SuwayomiConfigComponent = ({
});
};
const handleTest = async () => {
await withLoading('testSuwayomi', async () => {
try {
const response = await fetch('/api/admin/suwayomi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ServerURL: serverURL,
AuthMode: authMode,
Username: username,
Password: password,
DefaultLang: defaultLang,
}),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || data.error || '测试连接失败');
}
showSuccess(data.message || '连接成功', showAlert);
} catch (error) {
showError(error instanceof Error ? error.message : '测试连接失败', showAlert);
throw error;
}
});
};
return (
<div className='space-y-6'>
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4'>
@@ -10188,6 +10222,7 @@ const SuwayomiConfigComponent = ({
</h3>
<div className='text-sm text-blue-800 dark:text-blue-200 space-y-1'>
<p> Suwayomi Server GraphQL </p>
<p> basic_auth simple_login</p>
<p> </p>
<p> 使</p>
</div>
@@ -10222,16 +10257,57 @@ const SuwayomiConfigComponent = ({
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> Token</label>
<input
type='password'
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder='可选,若 Suwayomi 开启认证请填写'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'></label>
<div className='grid grid-cols-1 gap-2 md:grid-cols-3'>
{[
{ value: 'none', label: '无认证' },
{ value: 'basic_auth', label: 'basic_auth' },
{ value: 'simple_login', label: 'simple_login' },
].map((item) => (
<button
key={item.value}
type='button'
onClick={() => setAuthMode(item.value as 'none' | 'basic_auth' | 'simple_login')}
className={`rounded-lg border px-3 py-2 text-sm transition-colors ${
authMode === item.value
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/30 dark:text-blue-200'
: 'border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800'
}`}
>
{item.label}
</button>
))}
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
basic_auth 使 Basic Authorization simple_login /login.html Cookie
</p>
</div>
{authMode !== 'none' && (
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'></label>
<input
type='text'
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder='登录用户名'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'></label>
<input
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='登录密码'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/>
</div>
</div>
)}
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'></label>
@@ -10267,6 +10343,13 @@ const SuwayomiConfigComponent = ({
</div>
<div className='flex gap-3'>
<button
onClick={handleTest}
disabled={!serverURL || isLoading('testSuwayomi')}
className={buttonStyles.primary}
>
{isLoading('testSuwayomi') ? '测试中...' : '测试连接'}
</button>
<button
onClick={handleSave}
disabled={isLoading('saveSuwayomi')}
+69
View File
@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { db } from '@/lib/db';
import { SuwayomiClient } from '@/lib/suwayomi.client';
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
try {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
if (username !== process.env.USERNAME) {
const userInfo = await db.getUserInfoV2(username);
if (!userInfo || userInfo.role !== 'admin' || userInfo.banned) {
return NextResponse.json({ error: '权限不足' }, { status: 401 });
}
}
const body = await request.json();
const {
ServerURL,
AuthMode,
Username,
Password,
DefaultLang,
} = body as {
ServerURL?: string;
AuthMode?: 'none' | 'basic_auth' | 'simple_login';
Username?: string;
Password?: string;
DefaultLang?: string;
};
if (!ServerURL?.trim()) {
return NextResponse.json({ success: false, message: '请先填写 Suwayomi 服务地址' }, { status: 400 });
}
if ((AuthMode === 'basic_auth' || AuthMode === 'simple_login') && (!Username?.trim() || !Password)) {
return NextResponse.json({ success: false, message: '当前认证方式需要填写用户名和密码' }, { status: 400 });
}
const client = new SuwayomiClient({
serverUrl: ServerURL.trim(),
authMode: AuthMode || 'none',
username: Username?.trim(),
password: Password,
});
const sources = await client.getSources((DefaultLang || 'zh').trim() || 'zh');
return NextResponse.json({
success: true,
message: `连接成功,当前语言下检测到 ${sources.length} 个源`,
});
} catch (error) {
return NextResponse.json(
{
success: false,
message: error instanceof Error ? error.message : '测试连接失败',
},
{ status: 400 }
);
}
}
+30 -3
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthorizedUsername } from '../_utils';
import { getSuwayomiConfig } from '@/lib/suwayomi.client';
import { getSuwayomiConfig, loginWithSimpleAuth } from '@/lib/suwayomi.client';
export const runtime = 'nodejs';
@@ -34,11 +34,38 @@ export async function GET(request: NextRequest) {
const config = await getSuwayomiConfig();
const upstreamUrl = resolveUpstreamUrl(config.serverBaseUrl, pathOrUrl);
const response = await fetch(upstreamUrl, {
headers: config.token ? { Authorization: `Bearer ${config.token}` } : undefined,
const buildHeaders = async (forceRelogin: boolean) => {
if (config.authMode === 'basic_auth') {
if (!config.username || !config.password) {
throw new Error('Suwayomi basic_auth 缺少用户名或密码');
}
return {
Authorization: `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`,
};
}
if (config.authMode === 'simple_login') {
return {
Cookie: await loginWithSimpleAuth(config, forceRelogin),
};
}
return undefined;
};
let response = await fetch(upstreamUrl, {
headers: await buildHeaders(false),
cache: 'no-store',
});
if (response.status === 401 && config.authMode === 'simple_login') {
response = await fetch(upstreamUrl, {
headers: await buildHeaders(true),
cache: 'no-store',
});
}
if (!response.ok) {
return NextResponse.json(
{ error: `Suwayomi 图片请求失败: ${response.status}` },