支持邮件登录注册

This commit is contained in:
root 2024-12-01 17:37:06 +08:00
parent 9f300df47b
commit 871e7d47a3
8 changed files with 106 additions and 54 deletions

View file

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using Serilog; using Serilog;
using System.Text.Json; using System.Text.Json;
using iFileProxy.Attributes; using iFileProxy.Attributes;
namespace iFileProxy.Controllers namespace iFileProxy.Controllers
{ {
[Authorize(UserMask.Admin,UserMask.SuperAdmin)] [Authorize(UserMask.Admin,UserMask.SuperAdmin)]

View file

@ -1,8 +1,8 @@
using iFileProxy.Attributes; using iFileProxy.Attributes;
using iFileProxy.Helpers;
using iFileProxy.Models; using iFileProxy.Models;
using iFileProxy.Services; using iFileProxy.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace iFileProxy.Controllers namespace iFileProxy.Controllers
{ {
@ -27,8 +27,8 @@ namespace iFileProxy.Controllers
{ {
try try
{ {
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var ip = MasterHelper.GetClientIPAddr(HttpContext);
var (success, message) = await _authService.RegisterAsync(request.Username, request.Password, ip, request.NickName); var (success, message) = await _authService.RegisterAsync(request,ip);
return Ok(new CommonRsp return Ok(new CommonRsp
{ {
@ -106,9 +106,6 @@ namespace iFileProxy.Controllers
}); });
} }
_logger.LogInformation(JsonConvert.SerializeObject(user));
// 不返回敏感信息 // 不返回敏感信息
return Ok(new CommonRsp return Ok(new CommonRsp
{ {
@ -592,16 +589,4 @@ namespace iFileProxy.Controllers
} }
} }
public class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string NickName { get; set; } = string.Empty;
}
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
} }

View file

@ -4,9 +4,10 @@ namespace iFileProxy.Models
{ {
public enum UserMask public enum UserMask
{ {
Guest = -1, // 访客
User = 0, User = 0,
Admin = 1, Admin = 1,
SuperAdmin = 2 // 超级管理员 SuperAdmin = 2, // 超级管理员
} }
public enum UserState public enum UserState
{ {
@ -77,6 +78,12 @@ namespace iFileProxy.Models
/// </summary> /// </summary>
[JsonProperty("last_login_ip")] [JsonProperty("last_login_ip")]
public string LastLoginIP { get; set; } = string.Empty; public string LastLoginIP { get; set; } = string.Empty;
/// <summary>
/// 电子邮箱
/// </summary>
[JsonProperty("email")]
public string Email { get; set; } = string.Empty;
} }
public class UserEvent public class UserEvent
@ -132,4 +139,29 @@ namespace iFileProxy.Models
[JsonProperty("nickname")] [JsonProperty("nickname")]
public string Nickname { get; set; } = string.Empty; public string Nickname { get; set; } = string.Empty;
} }
/// <summary>
/// 注册请求数据结构
/// </summary>
public class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string NickName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
/// <summary>
/// 登录请求数据结构
/// </summary>
public class LoginRequest
{
/// <summary>
/// 用户名或邮箱
/// </summary>
public string Username { get; set; } = string.Empty;
public string Account { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
} }

View file

@ -16,18 +16,18 @@ namespace iFileProxy
SerilogConfig.CreateLogger(); SerilogConfig.CreateLogger();
Serilog.ILogger logger = Log.Logger.ForContext<Program>(); Serilog.ILogger logger = Log.Logger.ForContext<Program>();
Console.Write(" "); // 强迫症,看着开始的那条日志不对齐不得劲 Console.Write(" "); // ǿ<EFBFBD><EFBFBD>֢<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ſ<EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>þ<EFBFBD>
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// 添加 CORS 服务 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD> CORS <20><><EFBFBD><EFBFBD>
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("AllowFrontend", options.AddPolicy("AllowFrontend",
builder => builder =>
{ {
builder builder
.WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050") // 前端地址 .WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050") // ǰ<EFBFBD>˵<EFBFBD>ַ
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
.AllowCredentials(); .AllowCredentials();
@ -55,10 +55,10 @@ namespace iFileProxy
builder.Services.AddSingleton<TaskManager>(); builder.Services.AddSingleton<TaskManager>();
// 添加认证服务 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddScoped<AuthService>(); builder.Services.AddScoped<AuthService>();
// 添加JWT认证 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD>JWT<EFBFBD><EFBFBD>֤
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
@ -98,17 +98,17 @@ namespace iFileProxy
app.UseSwaggerUI(); app.UseSwaggerUI();
} }
// 错误处理中间件 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD><EFBFBD>
app.UseMiddleware<ErrorHandlerMiddleware>(); app.UseMiddleware<ErrorHandlerMiddleware>();
app.UseCors("AllowFrontend"); app.UseCors("AllowFrontend");
app.UseHttpsRedirection(); app.UseHttpsRedirection();
// 配置默认文件选项 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ѡ<EFBFBD><EFBFBD>
var defaultFilesOptions = new DefaultFilesOptions(); var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear(); // 清除默认列表 defaultFilesOptions.DefaultFileNames.Clear(); // <EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>
defaultFilesOptions.DefaultFileNames.Add("index.html"); // 添加自定义默认文件 defaultFilesOptions.DefaultFileNames.Add("index.html"); // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
app.UseDefaultFiles(defaultFilesOptions); app.UseDefaultFiles(defaultFilesOptions);
@ -120,7 +120,7 @@ namespace iFileProxy
app.Services.GetRequiredService<Dictionary<string, Dictionary<string, uint>>>(), app.Services.GetRequiredService<Dictionary<string, Dictionary<string, uint>>>(),
AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP); AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP);
// 添加中间件 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD><EFBFBD>
app.UseMiddleware<JwtMiddleware>(); app.UseMiddleware<JwtMiddleware>();
app.MapControllers(); app.MapControllers();

View file

@ -20,14 +20,14 @@ namespace iFileProxy.Services
_logger = logger; _logger = logger;
} }
public async Task<(bool success, string message)> RegisterAsync(string username, string password, string ip, string nickname) public async Task<(bool success, string message)> RegisterAsync(RegisterRequest request, string ipAddr)
{ {
try try
{ {
// 检查用户名是否已存在 // 检查用户名是否已存在
if (await _dbGateService.UserExistsAsync(username)) if (await _dbGateService.UserExistsAsync(request.Username, request.Email))
{ {
return (false, "用户名已存在"); return (false, "用户名或电子邮件已存在");
} }
// 检查是否是第一个用户 // 检查是否是第一个用户
@ -36,12 +36,13 @@ namespace iFileProxy.Services
// 创建新用户 // 创建新用户
var user = new User var user = new User
{ {
Username = username, Username = request.Username,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password), PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
LastLoginIP = ip, LastLoginIP = ipAddr,
// 如果是第一个用户,设置为超级管理员 // 如果是第一个用户,设置为超级管理员
Mask = isFirstUser ? UserMask.SuperAdmin : UserMask.User, Mask = isFirstUser ? UserMask.SuperAdmin : UserMask.User,
Nickname = nickname Nickname = request.NickName,
Email = request.Email,
}; };
// 保存用户 // 保存用户
@ -52,12 +53,12 @@ namespace iFileProxy.Services
{ {
UserId = user.UserId, UserId = user.UserId,
EventType = UserEventType.Registry, EventType = UserEventType.Registry,
EventIP = ip, EventIP = ipAddr,
EventDetail = isFirstUser ? "管理员注册" : "用户注册" EventDetail = isFirstUser ? "超级管理员注册" : "用户注册"
}; };
await _dbGateService.CreateUserEventAsync(userEvent); await _dbGateService.CreateUserEventAsync(userEvent);
return (true, isFirstUser ? "管理员注册成功" : "注册成功"); return (true, isFirstUser ? "超级管理员注册成功" : "注册成功");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -66,11 +67,11 @@ namespace iFileProxy.Services
} }
} }
public async Task<(bool success, string token, string message)> LoginAsync(string username, string password, string ip) public async Task<(bool success, string token, string message)> LoginAsync(string account, string password, string ip)
{ {
try try
{ {
var user = await _dbGateService.GetUserByUsernameAsync(username); var user = await _dbGateService.GetUserByAccountAsync(account);
if (user == null) if (user == null)
{ {
return (false, string.Empty, "用户不存在"); return (false, string.Empty, "用户不存在");
@ -97,7 +98,7 @@ namespace iFileProxy.Services
UserId = user.UserId, UserId = user.UserId,
EventType = UserEventType.Login, EventType = UserEventType.Login,
EventIP = ip, EventIP = ip,
EventDetail = "用户登录" EventDetail = $"用户通过{(account.Contains('@') ? "" : "")}登录"
}; };
await _dbGateService.CreateUserEventAsync(userEvent); await _dbGateService.CreateUserEventAsync(userEvent);

View file

@ -39,6 +39,7 @@ namespace iFileProxy.Services
`user_id` varchar(36) NOT NULL, `user_id` varchar(36) NOT NULL,
`username` varchar(50) NOT NULL, `username` varchar(50) NOT NULL,
`nickname` varchar(50) NOT NULL, `nickname` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password_hash` varchar(255) NOT NULL, `password_hash` varchar(255) NOT NULL,
`mask` int NOT NULL DEFAULT 0, `mask` int NOT NULL DEFAULT 0,
`state` int NOT NULL DEFAULT 0, `state` int NOT NULL DEFAULT 0,
@ -46,7 +47,8 @@ namespace iFileProxy.Services
`last_login_time` datetime NULL, `last_login_time` datetime NULL,
`last_login_ip` varchar(45) NULL, `last_login_ip` varchar(45) NULL,
PRIMARY KEY (`user_id`), PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`) UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""; """;
@ -196,6 +198,20 @@ namespace iFileProxy.Services
return n; return n;
} }
/// <summary>
/// 内部查询数据专用 当此方法暴露给C端可能造成sql注入等安全问题
/// </summary>
/// <param name="sql">SQL语句</param>
/// <param name="dbConfName">配置文件中的Description字段</param>
/// <returns>影响的行数</returns>
public int Query(string sql, MySqlConnection connection)
{
using MySqlCommand sqlCmd = new(sql, connection);
int n = sqlCmd.ExecuteNonQuery();
_logger.Debug($"查询完成, 受影响的行数: {n}");
return n;
}
public List<TaskInfo> CheckCacheDependencies(string taskId,string ipAddr) public List<TaskInfo> CheckCacheDependencies(string taskId,string ipAddr)
{ {
string sql = $"SELECT * FROM t_tasks_info WHERE `status` = @status AND `tag` = @tag AND `client_ip` <> @ip_addr"; string sql = $"SELECT * FROM t_tasks_info WHERE `status` = @status AND `tag` = @tag AND `client_ip` <> @ip_addr";
@ -362,10 +378,11 @@ namespace iFileProxy.Services
} }
public bool UpdateTaskStatus(TaskInfo taskInfo) public bool UpdateTaskStatus(TaskInfo taskInfo, MySqlConnection? connection = null)
{ {
string sql = @"UPDATE t_tasks_info set `status` = @status , update_time = Now() WHERE `tid` = @tid"; string sql = @"UPDATE t_tasks_info set `status` = @status , update_time = Now() WHERE `tid` = @tid";
MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db"); MySqlConnection conn = connection ?? GetAndOpenDBConn("iFileProxy_Db");
try try
{ {
using MySqlCommand sqlCmd = new (sql, conn); using MySqlCommand sqlCmd = new (sql, conn);
@ -623,12 +640,13 @@ namespace iFileProxy.Services
return users.FirstOrDefault(); return users.FirstOrDefault();
} }
public async Task<bool> UserExistsAsync(string username) public async Task<bool> UserExistsAsync(string username, string email)
{ {
var sql = "SELECT COUNT(*) FROM t_users WHERE username = @username"; var sql = "SELECT COUNT(*) FROM t_users WHERE username = @username OR email = @email";
var parameters = new Dictionary<string, object> var parameters = new Dictionary<string, object>
{ {
{ "@username", username } { "@username", username },
{ "@email", email }
}; };
var count = await ExecuteScalarAsync<long>(sql, parameters); var count = await ExecuteScalarAsync<long>(sql, parameters);
return count > 0; return count > 0;
@ -636,8 +654,8 @@ namespace iFileProxy.Services
public async Task<bool> CreateUserAsync(User user) public async Task<bool> CreateUserAsync(User user)
{ {
var sql = @"INSERT INTO t_users (user_id, username, password_hash, mask, state, create_time, last_login_time, last_login_ip, nickname) var sql = @"INSERT INTO t_users (user_id, username, password_hash, mask, state, create_time, last_login_time, last_login_ip, nickname, email)
VALUES (@userId, @username, @passwordHash, @mask, @state, @createTime, @lastLoginTime, @lastLoginIp, @nickname)"; VALUES (@userId, @username, @passwordHash, @mask, @state, @createTime, @lastLoginTime, @lastLoginIp, @nickname, @email)";
var parameters = new Dictionary<string, object> var parameters = new Dictionary<string, object>
{ {
@ -650,7 +668,7 @@ namespace iFileProxy.Services
{ "@lastLoginTime", user.LastLoginTime }, { "@lastLoginTime", user.LastLoginTime },
{ "@lastLoginIp", user.LastLoginIP }, { "@lastLoginIp", user.LastLoginIP },
{ "@nickname", user.Nickname }, { "@nickname", user.Nickname },
{ "@email", user.Email }
}; };
var result = await ExecuteNonQueryAsync(sql, parameters); var result = await ExecuteNonQueryAsync(sql, parameters);
@ -870,5 +888,17 @@ namespace iFileProxy.Services
Data = events Data = events
}; };
} }
public async Task<User?> GetUserByAccountAsync(string account)
{
var sql = "SELECT * FROM t_users WHERE username = @account OR email = @account";
var parameters = new Dictionary<string, object>
{
{ "@account", account }
};
var users = await ExecuteQueryAsync<User>(sql, parameters);
return users.FirstOrDefault();
}
} }
} }

View file

@ -38,6 +38,9 @@ namespace iFileProxy.Services
/// </summary> /// </summary>
public void CheckAndCleanCache(object state) public void CheckAndCleanCache(object state)
{ {
// 初始化并打开一个MySQL连接 防止后续数据过多导致程序crush
var dbConn = _dbGateService.GetAndOpenDBConn(DbConfigName.iFileProxy);
// 获取数据库中超出生命周期的缓存数据 // 获取数据库中超出生命周期的缓存数据
string result = _dbGateService.QueryTableData($"SELECT * FROM t_tasks_info WHERE UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(update_time) > {CACHE_LIFETIME} AND (tag <> 'CLEANED' OR tag IS NULL)", DbConfigName.iFileProxy); string result = _dbGateService.QueryTableData($"SELECT * FROM t_tasks_info WHERE UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(update_time) > {CACHE_LIFETIME} AND (tag <> 'CLEANED' OR tag IS NULL)", DbConfigName.iFileProxy);
List<TaskInfo>? taskInfos = JsonConvert.DeserializeObject<List<TaskInfo>>(result); List<TaskInfo>? taskInfos = JsonConvert.DeserializeObject<List<TaskInfo>>(result);
@ -59,9 +62,9 @@ namespace iFileProxy.Services
throw; throw;
} }
} }
_dbGateService.Query($"UPDATE t_tasks_info SET `tag` = \"CLEANED\" WHERE `tid` = '{taskInfo.TaskId}'", DbConfigName.iFileProxy); _dbGateService.Query($"UPDATE t_tasks_info SET `tag` = \"CLEANED\" WHERE `tid` = '{taskInfo.TaskId}'", dbConn);
taskInfo.Status = TaskState.Cleaned; taskInfo.Status = TaskState.Cleaned;
_dbGateService.UpdateTaskStatus(taskInfo); _dbGateService.UpdateTaskStatus(taskInfo,dbConn);
} }
} }
} }

View file

@ -16,7 +16,7 @@
"Download": { "Download": {
"SavePath": "./download/", // "SavePath": "./download/", //
"ThreadNum": 4, // 线 "ThreadNum": 4, // 线
"MaxAllowedFileSize": 65536, // "MaxAllowedFileSize": 1000000000, //
"MaxParallelTasks": 4, // "MaxParallelTasks": 4, //
"MaxQueueLength": 60, // "MaxQueueLength": 60, //
"Aria2cPath": "./lib/aria2c", "Aria2cPath": "./lib/aria2c",