mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-12 02:20:28 +08:00
feat(auth): add configuration options for user registration and anonymous image hosting
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -73,6 +73,8 @@ public class DatabaseInitializer(
|
||||
// 其他配置
|
||||
["Storage:DefaultStorageModeId"] = "1",
|
||||
["AppSettings:ServerUrl"] = "",
|
||||
["AppSettings:EnableRegistration"] = "true",
|
||||
["AppSettings:EnableAnonymousImageHosting"] = "true",
|
||||
["VectorDb:Type"] = "InMemory"
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user