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

View File

@@ -9,7 +9,7 @@ using Foxel.Services.Configuration;
namespace Foxel.Controllers;
[Route("api/auth")]
public class AuthController(IAuthService authService) : BaseApiController
public class AuthController(IAuthService authService, IConfigService configuration) : BaseApiController
{
[HttpPost("register")]
public async Task<ActionResult<BaseResult<AuthResponse>>> Register([FromBody] RegisterRequest request)
@@ -19,6 +19,13 @@ public class AuthController(IAuthService authService) : BaseApiController
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);
if (!success)
{

View File

@@ -9,12 +9,13 @@ using Foxel.Services.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json; // Added for JsonSerializer in GetTelegramFile if it were kept
using Foxel.Services.Configuration; // Added for IConfigService
namespace Foxel.Api;
[Authorize]
[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;
@@ -74,6 +75,16 @@ public class PictureController(IPictureService pictureService, IStorageService s
try
{
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();
var result = await pictureService.UploadPictureAsync(
request.File.FileName,

View File

@@ -73,6 +73,8 @@ public class DatabaseInitializer(
// 其他配置
["Storage:DefaultStorageModeId"] = "1",
["AppSettings:ServerUrl"] = "",
["AppSettings:EnableRegistration"] = "true",
["AppSettings:EnableAnonymousImageHosting"] = "true",
["VectorDb:Type"] = "InMemory"
};

View File

@@ -1,5 +1,5 @@
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';
interface ConfigFormItemProps {
@@ -23,6 +23,9 @@ const ConfigFormItem: React.FC<ConfigFormItemProps> = ({
isMobile,
onSave,
}) => {
const booleanAppSettings = ['EnableRegistration', 'EnableAnonymousImageHosting'];
const isBooleanAppSetting = groupName === 'AppSettings' && booleanAppSettings.includes(itemKey);
return (
<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}>
@@ -31,7 +34,7 @@ const ConfigFormItem: React.FC<ConfigFormItemProps> = ({
label={
<Space align="center">
<span style={{ fontWeight: 500 }}>{itemKey}</span>
{isSecret && <LockOutlined style={{ color: '#faad14' }} />}
{isSecret && !isBooleanAppSetting && <LockOutlined style={{ color: '#faad14' }} />}
{description && (
<Tooltip title={description}>
<QuestionCircleOutlined style={{ cursor: 'help', color: '#aaa' }} />
@@ -39,14 +42,29 @@ const ConfigFormItem: React.FC<ConfigFormItemProps> = ({
)}
</Space>
}
initialValue={isSecret ? '' : currentValue}
rules={isSecret ? [] : []}
initialValue={
isBooleanAppSetting
? currentValue === 'true'
: isSecret
? ''
: currentValue
}
valuePropName={isBooleanAppSetting ? 'checked' : undefined}
rules={isSecret || isBooleanAppSetting ? [] : []}
style={{ marginBottom: isMobile ? 8 : 16 }}
help={isSecret && currentValue ?
<span style={{ fontSize: '12px', color: '#999' }}></span> :
(isSecret ? <span style={{ fontSize: '12px', color: '#999' }}></span> : null)}
help={
isBooleanAppSetting
? 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
placeholder={currentValue ? '******(输入新值以更新)' : '请输入新值'}
style={{ maxWidth: 400 }}

View File

@@ -281,13 +281,13 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
isMobile={isMobile}
>
<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' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks'])}
onClick={() => onSaveAllForGroup(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks', 'EnableRegistration', 'EnableAnonymousImageHosting'])}
style={{ width: isMobile ? '100%' : '240px' }}
>
@@ -309,83 +309,98 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
description="配置文件上传处理方式和图片转换参数"
isMobile={isMobile}
>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr',
gap: isMobile ? 20 : 24,
marginBottom: 0
}}>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
(px)
</div>
<InputNumber
min={100}
max={1000}
step={50}
value={parseInt(configs.Upload?.ThumbnailMaxWidth || '400', 10)}
onChange={(value) => {
if (value !== null) {
onBaseSaveConfig('Upload', 'ThumbnailMaxWidth', value.toString())
}
}}
style={{ width: '100%' }}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{allDescriptions.Upload?.ThumbnailMaxWidth}
</div>
</div>
<Form form={formsMap.Upload} layout="vertical" size={isMobile ? "middle" : "large"}>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr',
gap: isMobile ? 20 : 24,
marginBottom: 0
}}>
<Form.Item
name="ThumbnailMaxWidth"
label={
<div style={{ fontSize: 14, fontWeight: 500, color: '#666' }}>
(px)
</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
min={100}
max={1000}
step={50}
style={{ width: '100%' }}
/>
</Form.Item>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
<Slider
min={30}
max={90}
step={5}
value={parseInt(configs.Upload?.ThumbnailCompressionQuality || '75', 10)}
onChange={(value) => onBaseSaveConfig('Upload', 'ThumbnailCompressionQuality', value.toString())}
style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{
formatter: value => `${value}%`
}}
marks={{
30: '30%',
60: '60%',
90: '90%'
}}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 16, textAlign: 'center' }}>
{allDescriptions.Upload?.ThumbnailCompressionQuality}
</div>
</div>
<Form.Item
name="ThumbnailCompressionQuality"
label={
<div style={{ fontSize: 14, fontWeight: 500, color: '#666' }}>
</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
min={30}
max={90}
step={5}
style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{
formatter: value => `${value}%`
}}
marks={{
30: '30%',
60: '60%',
90: '90%'
}}
/>
</Form.Item>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
<Slider
min={50}
max={100}
step={5}
value={parseInt(configs.Upload?.HighQualityImageCompressionQuality || '95', 10)}
onChange={(value) => onBaseSaveConfig('Upload', 'HighQualityImageCompressionQuality', value.toString())}
style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{
formatter: value => `${value}%`
}}
marks={{
50: '50%',
75: '75%',
100: '100%'
}}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 16, textAlign: 'center' }}>
{allDescriptions.Upload?.HighQualityImageCompressionQuality}
</div>
<Form.Item
name="HighQualityImageCompressionQuality"
label={
<div style={{ fontSize: 14, fontWeight: 500, color: '#666' }}>
</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
min={50}
max={100}
step={5}
style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{
formatter: value => `${value}%`
}}
marks={{
50: '50%',
75: '75%',
100: '100%'
}}
/>
</Form.Item>
</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>
</>
)

View File

@@ -43,7 +43,9 @@ const allDescriptions: Record<string, Record<string, string>> = {
},
AppSettings: {
ServerUrl: '服务器URL',
MaxConcurrentTasks: '后台任务最大并发处理数量 (例如: 图像分析、标签生成等)'
MaxConcurrentTasks: '后台任务最大并发处理数量 (例如: 图像分析、标签生成等)',
EnableRegistration: '是否允许新用户注册 (true/false)',
EnableAnonymousImageHosting: '是否允许匿名用户上传图片 (true/false)'
},
Upload: {
HighQualityImageCompressionQuality: '高清图片的压缩质量,越高图片质量越好但文件越大。范围 50-100。',
@@ -52,6 +54,7 @@ const allDescriptions: Record<string, Record<string, string>> = {
}
};
const booleanAppSettings = ['EnableRegistration', 'EnableAnonymousImageHosting'];
const System: React.FC = () => {
const isMobile = useIsMobile();
@@ -115,9 +118,13 @@ const System: React.FC = () => {
const formInstance = formsMap[formInstanceKey];
if (formInstance) {
const initialGroupValues: Record<string, string> = {};
const initialGroupValues: Record<string, any> = {}; // Changed to any for boolean values
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];
} else {
initialGroupValues[key] = '';
@@ -211,9 +218,13 @@ const System: React.FC = () => {
const handleSaveSingleConfig = async (formInstance: any, groupName: string, key: string) => {
try {
await formInstance.validateFields([key]);
const value = formInstance.getFieldValue(key);
let value = formInstance.getFieldValue(key);
const isSecret = secretFields[groupName]?.includes(key);
if (groupName === 'AppSettings' && booleanAppSettings.includes(key) && typeof value === 'boolean') {
value = String(value);
}
if (isSecret && (value === '' || value === undefined)) {
message.info(`未输入 ${key} 的新值,不作更改。`);
return;
@@ -242,7 +253,12 @@ const System: React.FC = () => {
// 计算需要保存的总数
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);
if (!(isSecret && (value === '' || value === undefined)) &&
(isSecret || configs[groupName]?.[key] !== value)) {
@@ -265,9 +281,15 @@ const System: React.FC = () => {
});
for (const key of itemKeys) {
const value = values[key];
let value = values[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)) {
continue;
}