feat(auth): add configuration options for user registration and anonymous image hosting

This commit is contained in:
shiyu
2025-06-10 16:42:39 +08:00
parent b5931de344
commit 853efaa2fe
6 changed files with 167 additions and 92 deletions
+8 -1
View File
@@ -9,7 +9,7 @@ using Foxel.Services.Configuration;
namespace Foxel.Controllers; namespace Foxel.Controllers;
[Route("api/auth")] [Route("api/auth")]
public class AuthController(IAuthService authService) : BaseApiController public class AuthController(IAuthService authService, IConfigService configuration) : BaseApiController
{ {
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<BaseResult<AuthResponse>>> Register([FromBody] RegisterRequest request) public async Task<ActionResult<BaseResult<AuthResponse>>> Register([FromBody] RegisterRequest request)
@@ -19,6 +19,13 @@ public class AuthController(IAuthService authService) : BaseApiController
return Error<AuthResponse>("请求数据无效"); return Error<AuthResponse>("请求数据无效");
} }
// 检查是否允许新用户注册
var enableRegistration = configuration["AppSettings:EnableRegistration"];
if (string.Equals(enableRegistration, "false", StringComparison.OrdinalIgnoreCase))
{
return Error<AuthResponse>("新用户注册功能已关闭");
}
var (success, message, user) = await authService.RegisterUserAsync(request); var (success, message, user) = await authService.RegisterUserAsync(request);
if (!success) if (!success)
{ {
+12 -1
View File
@@ -9,12 +9,13 @@ using Foxel.Services.Storage;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Text.Json; // Added for JsonSerializer in GetTelegramFile if it were kept using System.Text.Json; // Added for JsonSerializer in GetTelegramFile if it were kept
using Foxel.Services.Configuration; // Added for IConfigService
namespace Foxel.Api; namespace Foxel.Api;
[Authorize] [Authorize]
[Route("api/picture")] [Route("api/picture")]
public class PictureController(IPictureService pictureService, IStorageService storageService, ILogger<PictureController> logger) : BaseApiController public class PictureController(IPictureService pictureService, IStorageService storageService, ILogger<PictureController> logger, IConfigService configuration) : BaseApiController
{ {
private readonly ILogger<PictureController> _logger = logger; private readonly ILogger<PictureController> _logger = logger;
@@ -74,6 +75,16 @@ public class PictureController(IPictureService pictureService, IStorageService s
try try
{ {
var userId = GetCurrentUserId(); var userId = GetCurrentUserId();
if (userId == null)
{
var enableAnonymousUpload = configuration["AppSettings:EnableAnonymousImageHosting"];
if (string.Equals(enableAnonymousUpload, "false", StringComparison.OrdinalIgnoreCase))
{
return Error<PictureResponse>("匿名上传功能已关闭,请登录后操作", 403);
}
}
await using var stream = request.File.OpenReadStream(); await using var stream = request.File.OpenReadStream();
var result = await pictureService.UploadPictureAsync( var result = await pictureService.UploadPictureAsync(
request.File.FileName, request.File.FileName,
@@ -73,6 +73,8 @@ public class DatabaseInitializer(
// 其他配置 // 其他配置
["Storage:DefaultStorageModeId"] = "1", ["Storage:DefaultStorageModeId"] = "1",
["AppSettings:ServerUrl"] = "", ["AppSettings:ServerUrl"] = "",
["AppSettings:EnableRegistration"] = "true",
["AppSettings:EnableAnonymousImageHosting"] = "true",
["VectorDb:Type"] = "InMemory" ["VectorDb:Type"] = "InMemory"
}; };
+26 -8
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Row, Col, Form, Input, Button, Space, Tooltip } from 'antd'; import { Row, Col, Form, Input, Button, Space, Tooltip, Switch } from 'antd';
import { LockOutlined, QuestionCircleOutlined, SaveOutlined } from '@ant-design/icons'; import { LockOutlined, QuestionCircleOutlined, SaveOutlined } from '@ant-design/icons';
interface ConfigFormItemProps { interface ConfigFormItemProps {
@@ -23,6 +23,9 @@ const ConfigFormItem: React.FC<ConfigFormItemProps> = ({
isMobile, isMobile,
onSave, onSave,
}) => { }) => {
const booleanAppSettings = ['EnableRegistration', 'EnableAnonymousImageHosting'];
const isBooleanAppSetting = groupName === 'AppSettings' && booleanAppSettings.includes(itemKey);
return ( return (
<Row key={itemKey} gutter={isMobile ? [8, 8] : [16, 16]} align="top" style={{ marginBottom: isMobile ? 8 : 16 }}> <Row key={itemKey} gutter={isMobile ? [8, 8] : [16, 16]} align="top" style={{ marginBottom: isMobile ? 8 : 16 }}>
<Col xs={24} sm={isMobile ? 24 : 16} md={isMobile ? 24 : 17} lg={isMobile ? 24 : 18}> <Col xs={24} sm={isMobile ? 24 : 16} md={isMobile ? 24 : 17} lg={isMobile ? 24 : 18}>
@@ -31,7 +34,7 @@ const ConfigFormItem: React.FC<ConfigFormItemProps> = ({
label={ label={
<Space align="center"> <Space align="center">
<span style={{ fontWeight: 500 }}>{itemKey}</span> <span style={{ fontWeight: 500 }}>{itemKey}</span>
{isSecret && <LockOutlined style={{ color: '#faad14' }} />} {isSecret && !isBooleanAppSetting && <LockOutlined style={{ color: '#faad14' }} />}
{description && ( {description && (
<Tooltip title={description}> <Tooltip title={description}>
<QuestionCircleOutlined style={{ cursor: 'help', color: '#aaa' }} /> <QuestionCircleOutlined style={{ cursor: 'help', color: '#aaa' }} />
@@ -39,14 +42,29 @@ const ConfigFormItem: React.FC<ConfigFormItemProps> = ({
)} )}
</Space> </Space>
} }
initialValue={isSecret ? '' : currentValue} initialValue={
rules={isSecret ? [] : []} isBooleanAppSetting
? currentValue === 'true'
: isSecret
? ''
: currentValue
}
valuePropName={isBooleanAppSetting ? 'checked' : undefined}
rules={isSecret || isBooleanAppSetting ? [] : []}
style={{ marginBottom: isMobile ? 8 : 16 }} style={{ marginBottom: isMobile ? 8 : 16 }}
help={isSecret && currentValue ? help={
<span style={{ fontSize: '12px', color: '#999' }}></span> : isBooleanAppSetting
(isSecret ? <span style={{ fontSize: '12px', color: '#999' }}></span> : null)} ? null
: isSecret && currentValue
? <span style={{ fontSize: '12px', color: '#999' }}></span>
: isSecret
? <span style={{ fontSize: '12px', color: '#999' }}></span>
: null
}
> >
{isSecret ? ( {isBooleanAppSetting ? (
<Switch />
) : isSecret ? (
<Input.Password <Input.Password
placeholder={currentValue ? '******(输入新值以更新)' : '请输入新值'} placeholder={currentValue ? '******(输入新值以更新)' : '请输入新值'}
style={{ maxWidth: 400 }} style={{ maxWidth: 400 }}
+45 -30
View File
@@ -281,13 +281,13 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
isMobile={isMobile} isMobile={isMobile}
> >
<Form form={formsMap.AppSettings} layout="vertical" size={isMobile ? "middle" : "large"}> <Form form={formsMap.AppSettings} layout="vertical" size={isMobile ? "middle" : "large"}>
{renderConfigFormItems(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks'])} {renderConfigFormItems(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks', 'EnableRegistration', 'EnableAnonymousImageHosting'])}
<Divider style={{ margin: '12px 0 20px' }} /> <Divider style={{ margin: '12px 0 20px' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}> <Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button <Button
type="primary" type="primary"
icon={<SaveOutlined />} icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks'])} onClick={() => onSaveAllForGroup(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks', 'EnableRegistration', 'EnableAnonymousImageHosting'])}
style={{ width: isMobile ? '100%' : '240px' }} style={{ width: isMobile ? '100%' : '240px' }}
> >
@@ -309,43 +309,47 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
description="配置文件上传处理方式和图片转换参数" description="配置文件上传处理方式和图片转换参数"
isMobile={isMobile} isMobile={isMobile}
> >
<Form form={formsMap.Upload} layout="vertical" size={isMobile ? "middle" : "large"}>
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '1fr', gridTemplateColumns: '1fr',
gap: isMobile ? 20 : 24, gap: isMobile ? 20 : 24,
marginBottom: 0 marginBottom: 0
}}> }}>
<div> <Form.Item
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}> name="ThumbnailMaxWidth"
label={
<div style={{ fontSize: 14, fontWeight: 500, color: '#666' }}>
(px) (px)
</div> </div>
}
help={<div style={{ fontSize: 12, color: '#999', marginTop: 0 }}>{allDescriptions.Upload?.ThumbnailMaxWidth}</div>}
initialValue={parseInt(configs.Upload?.ThumbnailMaxWidth || '400', 10)}
style={{ marginBottom: 0 }}
>
<InputNumber <InputNumber
min={100} min={100}
max={1000} max={1000}
step={50} step={50}
value={parseInt(configs.Upload?.ThumbnailMaxWidth || '400', 10)}
onChange={(value) => {
if (value !== null) {
onBaseSaveConfig('Upload', 'ThumbnailMaxWidth', value.toString())
}
}}
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}> </Form.Item>
{allDescriptions.Upload?.ThumbnailMaxWidth}
</div>
</div>
<div> <Form.Item
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}> name="ThumbnailCompressionQuality"
label={
<div style={{ fontSize: 14, fontWeight: 500, color: '#666' }}>
</div> </div>
}
help={<div style={{ fontSize: 12, color: '#999', marginTop: isMobile ? 8 : 16, textAlign: 'center' }}>{allDescriptions.Upload?.ThumbnailCompressionQuality}</div>}
initialValue={parseInt(configs.Upload?.ThumbnailCompressionQuality || '75', 10)}
style={{ marginBottom: 0 }}
>
<Slider <Slider
min={30} min={30}
max={90} max={90}
step={5} step={5}
value={parseInt(configs.Upload?.ThumbnailCompressionQuality || '75', 10)}
onChange={(value) => onBaseSaveConfig('Upload', 'ThumbnailCompressionQuality', value.toString())}
style={{ margin: isMobile ? '0 5px' : '0 10px' }} style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{ tooltip={{
formatter: value => `${value}%` formatter: value => `${value}%`
@@ -356,21 +360,23 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
90: '90%' 90: '90%'
}} }}
/> />
<div style={{ fontSize: 12, color: '#999', marginTop: 16, textAlign: 'center' }}> </Form.Item>
{allDescriptions.Upload?.ThumbnailCompressionQuality}
</div>
</div>
<div> <Form.Item
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}> name="HighQualityImageCompressionQuality"
label={
<div style={{ fontSize: 14, fontWeight: 500, color: '#666' }}>
</div> </div>
}
help={<div style={{ fontSize: 12, color: '#999', marginTop: isMobile ? 8 : 16, textAlign: 'center' }}>{allDescriptions.Upload?.HighQualityImageCompressionQuality}</div>}
initialValue={parseInt(configs.Upload?.HighQualityImageCompressionQuality || '95', 10)}
style={{ marginBottom: 0 }}
>
<Slider <Slider
min={50} min={50}
max={100} max={100}
step={5} step={5}
value={parseInt(configs.Upload?.HighQualityImageCompressionQuality || '95', 10)}
onChange={(value) => onBaseSaveConfig('Upload', 'HighQualityImageCompressionQuality', value.toString())}
style={{ margin: isMobile ? '0 5px' : '0 10px' }} style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{ tooltip={{
formatter: value => `${value}%` formatter: value => `${value}%`
@@ -381,11 +387,20 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
100: '100%' 100: '100%'
}} }}
/> />
<div style={{ fontSize: 12, color: '#999', marginTop: 16, textAlign: 'center' }}> </Form.Item>
{allDescriptions.Upload?.HighQualityImageCompressionQuality}
</div>
</div>
</div> </div>
<Divider style={{ margin: '24px 0 20px' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.Upload, "Upload", ['ThumbnailMaxWidth', 'ThumbnailCompressionQuality', 'HighQualityImageCompressionQuality'])}
style={{ width: isMobile ? '100%' : '240px' }}
>
</Button>
</Form.Item>
</Form>
</ConfigSection> </ConfigSection>
</> </>
) )
+28 -6
View File
@@ -43,7 +43,9 @@ const allDescriptions: Record<string, Record<string, string>> = {
}, },
AppSettings: { AppSettings: {
ServerUrl: '服务器URL', ServerUrl: '服务器URL',
MaxConcurrentTasks: '后台任务最大并发处理数量 (例如: 图像分析、标签生成等)' MaxConcurrentTasks: '后台任务最大并发处理数量 (例如: 图像分析、标签生成等)',
EnableRegistration: '是否允许新用户注册 (true/false)',
EnableAnonymousImageHosting: '是否允许匿名用户上传图片 (true/false)'
}, },
Upload: { Upload: {
HighQualityImageCompressionQuality: '高清图片的压缩质量,越高图片质量越好但文件越大。范围 50-100。', HighQualityImageCompressionQuality: '高清图片的压缩质量,越高图片质量越好但文件越大。范围 50-100。',
@@ -52,6 +54,7 @@ const allDescriptions: Record<string, Record<string, string>> = {
} }
}; };
const booleanAppSettings = ['EnableRegistration', 'EnableAnonymousImageHosting'];
const System: React.FC = () => { const System: React.FC = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -115,9 +118,13 @@ const System: React.FC = () => {
const formInstance = formsMap[formInstanceKey]; const formInstance = formsMap[formInstanceKey];
if (formInstance) { if (formInstance) {
const initialGroupValues: Record<string, string> = {}; const initialGroupValues: Record<string, any> = {}; // Changed to any for boolean values
Object.keys(configGroups[group]).forEach(key => { Object.keys(configGroups[group]).forEach(key => {
if (!secretFieldsMap[group]?.includes(key)) { if (group === 'AppSettings' && booleanAppSettings.includes(key)) {
initialGroupValues[key] = configGroups[group][key] === 'true';
} else if (group === 'Upload' && ['ThumbnailMaxWidth', 'ThumbnailCompressionQuality', 'HighQualityImageCompressionQuality'].includes(key)) {
initialGroupValues[key] = parseInt(configGroups[group][key] || (key === 'ThumbnailMaxWidth' ? '400' : (key === 'ThumbnailCompressionQuality' ? '75' : '95')), 10);
} else if (!secretFieldsMap[group]?.includes(key)) {
initialGroupValues[key] = configGroups[group][key]; initialGroupValues[key] = configGroups[group][key];
} else { } else {
initialGroupValues[key] = ''; initialGroupValues[key] = '';
@@ -211,9 +218,13 @@ const System: React.FC = () => {
const handleSaveSingleConfig = async (formInstance: any, groupName: string, key: string) => { const handleSaveSingleConfig = async (formInstance: any, groupName: string, key: string) => {
try { try {
await formInstance.validateFields([key]); await formInstance.validateFields([key]);
const value = formInstance.getFieldValue(key); let value = formInstance.getFieldValue(key);
const isSecret = secretFields[groupName]?.includes(key); const isSecret = secretFields[groupName]?.includes(key);
if (groupName === 'AppSettings' && booleanAppSettings.includes(key) && typeof value === 'boolean') {
value = String(value);
}
if (isSecret && (value === '' || value === undefined)) { if (isSecret && (value === '' || value === undefined)) {
message.info(`未输入 ${key} 的新值,不作更改。`); message.info(`未输入 ${key} 的新值,不作更改。`);
return; return;
@@ -242,7 +253,12 @@ const System: React.FC = () => {
// 计算需要保存的总数 // 计算需要保存的总数
for (const key of itemKeys) { for (const key of itemKeys) {
const value = values[key]; let value = values[key];
if (groupName === 'AppSettings' && booleanAppSettings.includes(key) && typeof value === 'boolean') {
value = String(value);
} else if (groupName === 'Upload' && typeof value === 'number') { // Ensure numbers are converted to strings for saving
value = String(value);
}
const isSecret = secretFields[groupName]?.includes(key); const isSecret = secretFields[groupName]?.includes(key);
if (!(isSecret && (value === '' || value === undefined)) && if (!(isSecret && (value === '' || value === undefined)) &&
(isSecret || configs[groupName]?.[key] !== value)) { (isSecret || configs[groupName]?.[key] !== value)) {
@@ -265,9 +281,15 @@ const System: React.FC = () => {
}); });
for (const key of itemKeys) { for (const key of itemKeys) {
const value = values[key]; let value = values[key];
const isSecret = secretFields[groupName]?.includes(key); const isSecret = secretFields[groupName]?.includes(key);
if (groupName === 'AppSettings' && booleanAppSettings.includes(key) && typeof value === 'boolean') {
value = String(value);
} else if (groupName === 'Upload' && typeof value === 'number') { // Ensure numbers are converted to strings for saving
value = String(value);
}
if (isSecret && (value === '' || value === undefined)) { if (isSecret && (value === '' || value === undefined)) {
continue; continue;
} }