From 7fe9d05ffa96a0814507e832e29d8ff8aada4636 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Dec 2024 01:27:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B7=AF=E7=94=B1=E7=BB=86?= =?UTF-8?q?=E7=B2=92=E5=BA=A6ACL=E5=92=8C=E7=94=A8=E6=88=B7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Attributes/AuthorizeAttribute.cs | 45 ++ src/Controllers/ManagementController.cs | 2 + src/Controllers/UserController.cs | 607 ++++++++++++++++++ src/Middleware/JwtMiddleware.cs | 58 ++ src/Models/CommonRsp.cs | 2 +- src/Models/User.cs | 135 ++++ src/Program.cs | 28 +- .../PublishProfiles/FolderProfile.pubxml | 2 +- src/Services/AuthService.cs | 141 ++++ src/Services/DatabaseGateService.cs | 352 +++++++++- src/appsettings.json | 7 +- src/document/iFileProxy.sql | 67 ++ src/iFileProxy.csproj | 2 + 13 files changed, 1416 insertions(+), 32 deletions(-) create mode 100644 src/Attributes/AuthorizeAttribute.cs create mode 100644 src/Controllers/UserController.cs create mode 100644 src/Middleware/JwtMiddleware.cs create mode 100644 src/Models/User.cs create mode 100644 src/Services/AuthService.cs create mode 100644 src/document/iFileProxy.sql diff --git a/src/Attributes/AuthorizeAttribute.cs b/src/Attributes/AuthorizeAttribute.cs new file mode 100644 index 0000000..1bf28c4 --- /dev/null +++ b/src/Attributes/AuthorizeAttribute.cs @@ -0,0 +1,45 @@ +using iFileProxy.Models; +using iFileProxy.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace iFileProxy.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class AuthorizeAttribute : Attribute, IAuthorizationFilter + { + private readonly UserMask[] _allowedMasks; + + public AuthorizeAttribute(params UserMask[] allowedMasks) + { + _allowedMasks = allowedMasks; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + var userId = context.HttpContext.Items["User"]?.ToString(); + if (string.IsNullOrEmpty(userId)) + { + context.Result = new JsonResult(new CommonRsp { Message = "未授权", Retcode = -1 }) + { + StatusCode = StatusCodes.Status401Unauthorized + }; + return; + } + + if (_allowedMasks.Length > 0) + { + var dbGateService = context.HttpContext.RequestServices.GetRequiredService(); + var user = dbGateService.GetUserByIdAsync(userId).Result; + + if (user == null || !_allowedMasks.Contains(user.Mask) ) + { + context.Result = new JsonResult(new CommonRsp { Message = "权限不足" ,Retcode = 1}) + { + StatusCode = StatusCodes.Status403Forbidden + }; + } + } + } + } +} \ No newline at end of file diff --git a/src/Controllers/ManagementController.cs b/src/Controllers/ManagementController.cs index ac984d1..bb8003c 100644 --- a/src/Controllers/ManagementController.cs +++ b/src/Controllers/ManagementController.cs @@ -4,8 +4,10 @@ using iFileProxy.Services; using Microsoft.AspNetCore.Mvc; using Serilog; using System.Text.Json; +using iFileProxy.Attributes; namespace iFileProxy.Controllers { + [Authorize(UserMask.Admin,UserMask.SuperAdmin)] [Route("[controller]")] [ApiController] public class ManagementController(TaskManager taskManager, DatabaseGateService dbGateService) : ControllerBase diff --git a/src/Controllers/UserController.cs b/src/Controllers/UserController.cs new file mode 100644 index 0000000..74217f5 --- /dev/null +++ b/src/Controllers/UserController.cs @@ -0,0 +1,607 @@ +using iFileProxy.Attributes; +using iFileProxy.Models; +using iFileProxy.Services; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace iFileProxy.Controllers +{ + [Route("[controller]")] + [ApiController] + public class UserController : ControllerBase + { + private readonly AuthService _authService; + private readonly ILogger _logger; + + public UserController(AuthService authService, ILogger logger) + { + _authService = authService; + _logger = logger; + } + + /// + /// 用户注册 + /// + [HttpPost("register")] + public async Task> Register([FromBody] RegisterRequest request) + { + try + { + var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + var (success, message) = await _authService.RegisterAsync(request.Username, request.Password, ip, request.NickName); + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = message + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "注册失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "注册失败" + }); + } + } + + /// + /// 用户登录 + /// + [HttpPost("login")] + public async Task> Login([FromBody] LoginRequest request) + { + try + { + var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + var (success, token, message) = await _authService.LoginAsync(request.Username, request.Password, ip); + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = message, + Data = success ? new { token } : null + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "登录失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "登录失败" + }); + } + } + + /// + /// 获取当前用户信息 + /// + [Authorize] + [HttpGet("info")] + public async Task> GetUserInfo() + { + try + { + var userId = HttpContext.Items["User"]?.ToString(); + if (string.IsNullOrEmpty(userId)) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "未登录" + }); + } + + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + var user = await dbGateService.GetUserByIdAsync(userId); + if (user == null) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "用户不存在" + }); + } + + + _logger.LogInformation(JsonConvert.SerializeObject(user)); + + // 不返回敏感信息 + return Ok(new CommonRsp + { + Retcode = 0, + Message = "success", + Data = new + { + user.UserId, + user.Username, + user.Mask, + user.State, + user.CreateTime, + user.LastLoginTime, + user.LastLoginIP, + user.Nickname + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户信息失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "获取用户信息失败" + }); + } + } + + /// + /// 修改用户权限 + /// + [Authorize(UserMask.SuperAdmin)] + [HttpPost("updateMask/{userId}")] + public async Task> UpdateUserMask(string userId, [FromBody] UpdateUserMaskRequest request) + { + try + { + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + + // 获取目标用户 + var targetUser = await dbGateService.GetUserByIdAsync(userId); + if (targetUser == null) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "用户不存在" + }); + } + + // 不允许修改超级管理员的权限 + if (targetUser.Mask == UserMask.SuperAdmin) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "不能修改超级管理员的权限" + }); + } + + // 更新权限 + targetUser.Mask = request.NewMask; + var success = await dbGateService.UpdateUserAsync(targetUser); + + // 记录操作事件 + var operatorId = HttpContext.Items["User"]?.ToString(); + if (success && !string.IsNullOrEmpty(operatorId)) + { + var userEvent = new UserEvent + { + UserId = targetUser.UserId, + EventType = UserEventType.UpdateMask, + EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", + EventDetail = $"权限被修改为 {request.NewMask},操作者ID: {operatorId}" + }; + await dbGateService.CreateUserEventAsync(userEvent); + } + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = success ? "权限修改成功" : "权限修改失败" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "修改用户权限失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "修改用户权限失败" + }); + } + } + + /// + /// 获取用户列表(分页) + /// + [Authorize(UserMask.SuperAdmin)] + [HttpGet("list")] + public async Task> GetUserList([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] UserMask? mask = null) + { + try + { + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + var result = await dbGateService.GetPagedUserListAsync(page, pageSize, mask); + + // 移除敏感信息 + foreach (var user in result.Data) + { + user.PasswordHash = "***************"; + } + + return Ok(new CommonRsp + { + Retcode = 0, + Message = "success", + Data = result + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户列表失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "获取用户列表失败" + }); + } + } + + /// + /// 修改个人昵称 + /// + [Authorize] + [HttpPost("updateNickname")] + public async Task> UpdateNickname([FromBody] UpdateNicknameRequest request) + { + try + { + var userId = HttpContext.Items["User"]?.ToString(); + if (string.IsNullOrEmpty(userId)) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "未登录" + }); + } + + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + var user = await dbGateService.GetUserByIdAsync(userId); + if (user == null) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "用户不存在" + }); + } + + user.Nickname = request.NewNickname; + var success = await dbGateService.UpdateUserAsync(user); + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = success ? "昵称修改成功" : "昵称修改失败" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "修改昵称失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "修改昵称失败" + }); + } + } + + /// + /// 修改密码 + /// + [Authorize] + [HttpPost("updatePassword")] + public async Task> UpdatePassword([FromBody] UpdatePasswordRequest request) + { + try + { + var userId = HttpContext.Items["User"]?.ToString(); + if (string.IsNullOrEmpty(userId)) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "未登录" + }); + } + + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + var user = await dbGateService.GetUserByIdAsync(userId); + if (user == null) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "用户不存在" + }); + } + + // 验证旧密码 + if (!BCrypt.Net.BCrypt.Verify(request.OldPassword, user.PasswordHash)) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "旧密码错误" + }); + } + + // 更新密码 + user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); + var success = await dbGateService.UpdateUserAsync(user); + + // 记录密码修改事件 + if (success) + { + var userEvent = new UserEvent + { + UserId = user.UserId, + EventType = UserEventType.UpdatePassword, + EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", + EventDetail = "密码已修改" + }; + await dbGateService.CreateUserEventAsync(userEvent); + } + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = success ? "密码修改成功" : "密码修改失败" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "修改密码失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "修改密码失败" + }); + } + } + + /// + /// 超级管理员修改用户密码 + /// + [Authorize(UserMask.SuperAdmin)] + [HttpPost("resetPassword/{userId}")] + public async Task> ResetUserPassword(string userId, [FromBody] ResetPasswordRequest request) + { + try + { + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + + // 获取目标用户 + var targetUser = await dbGateService.GetUserByIdAsync(userId); + if (targetUser == null) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "用户不存在" + }); + } + + // 不允许修改超级管理员的密码 + if (targetUser.Mask == UserMask.SuperAdmin && targetUser.UserId != HttpContext.Items["User"]?.ToString()) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "不能修改其他超级管理员的密码" + }); + } + + // 更新密码 + targetUser.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); + var success = await dbGateService.UpdateUserAsync(targetUser); + + // 记录密码重置事件 + if (success) + { + var operatorId = HttpContext.Items["User"]?.ToString(); + var userEvent = new UserEvent + { + UserId = targetUser.UserId, + EventType = UserEventType.UpdatePassword, + EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", + EventDetail = $"密码被管理员重置,操作者ID: {operatorId}" + }; + await dbGateService.CreateUserEventAsync(userEvent); + } + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = success ? "密码重置成功" : "密码重置失败" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "重置密码失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "重置密码失败" + }); + } + } + + /// + /// 获取用户事件列表 + /// + [Authorize] + [HttpGet("events")] + public async Task> GetUserEvents( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? keyword = null, + [FromQuery] string? userId = null) + { + try + { + var currentUserId = HttpContext.Items["User"]?.ToString(); + if (string.IsNullOrEmpty(currentUserId)) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "未登录" + }); + } + + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + var currentUser = await dbGateService.GetUserByIdAsync(currentUserId); + + // 如果不是超级管理员,只能查看自己的事件 + if (currentUser?.Mask != UserMask.SuperAdmin) + { + userId = currentUserId; + } + // 如果是管理员且未指定用户ID,则查看所有事件 + else if (string.IsNullOrEmpty(userId)) + { + userId = null; // 查询所有用户的事件 + } + + var result = await dbGateService.GetPagedUserEventsAsync(page, pageSize, userId, keyword); + + return Ok(new CommonRsp + { + Retcode = 0, + Message = "success", + Data = result + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户事件失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "获取用户事件失败" + }); + } + } + + /// + /// 管理员修改用户昵称 + /// + [Authorize(UserMask.Admin, UserMask.SuperAdmin)] + [HttpPost("updateNickname/{userId}")] + public async Task> UpdateUserNickname(string userId, [FromBody] UpdateNicknameRequest request) + { + try + { + var dbGateService = HttpContext.RequestServices.GetRequiredService(); + + // 获取目标用户 + var targetUser = await dbGateService.GetUserByIdAsync(userId); + if (targetUser == null) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "用户不存在" + }); + } + + // 获取当前操作者信息 + var operatorId = HttpContext.Items["User"]?.ToString(); + var operator_ = await dbGateService.GetUserByIdAsync(operatorId); + + // 如果目标用户是超级管理员,只有超级管理员自己能修改自己的昵称 + if (targetUser.Mask == UserMask.SuperAdmin && + (operator_?.Mask != UserMask.SuperAdmin || targetUser.UserId != operatorId)) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "不能修改超级管理员的昵称" + }); + } + + // 如果目标用户是管理员,只有超级管理员能修改 + if (targetUser.Mask == UserMask.Admin && operator_?.Mask != UserMask.SuperAdmin) + { + return Ok(new CommonRsp + { + Retcode = 1, + Message = "只有超级管理员能修改管理员的昵称" + }); + } + + // 更新昵称 + targetUser.Nickname = request.NewNickname; + var success = await dbGateService.UpdateUserAsync(targetUser); + + // 记录操作事件 + if (success) + { + var userEvent = new UserEvent + { + UserId = targetUser.UserId, + EventType = UserEventType.UpdateNickname, // 需要添加这个事件类型 + EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", + EventDetail = $"昵称被修改为 {request.NewNickname},操作者ID: {operatorId}" + }; + await dbGateService.CreateUserEventAsync(userEvent); + } + + return Ok(new CommonRsp + { + Retcode = success ? 0 : 1, + Message = success ? "昵称修改成功" : "昵称修改失败" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "修改用户昵称失败"); + return Ok(new CommonRsp + { + Retcode = 1, + Message = "修改用户昵称失败" + }); + } + } + + public class UpdateUserMaskRequest + { + public UserMask NewMask { get; set; } + } + + public class UpdateNicknameRequest + { + public string NewNickname { get; set; } = string.Empty; + } + + public class UpdatePasswordRequest + { + public string OldPassword { get; set; } = string.Empty; + public string NewPassword { get; set; } = string.Empty; + } + + public class ResetPasswordRequest + { + public string NewPassword { get; set; } = string.Empty; + } + } + + 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; + } +} \ No newline at end of file diff --git a/src/Middleware/JwtMiddleware.cs b/src/Middleware/JwtMiddleware.cs new file mode 100644 index 0000000..5b33fb4 --- /dev/null +++ b/src/Middleware/JwtMiddleware.cs @@ -0,0 +1,58 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace iFileProxy.Middleware +{ + public class JwtMiddleware + { + private readonly RequestDelegate _next; + private readonly IConfiguration _configuration; + + public JwtMiddleware(RequestDelegate next, IConfiguration configuration) + { + _next = next; + _configuration = configuration; + } + + public async Task InvokeAsync(HttpContext context) + { + var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + + if (token != null) + await AttachUserToContext(context, token); + + await _next(context); + } + + private async Task AttachUserToContext(HttpContext context, string token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]); + tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidateAudience = true, + ValidIssuer = _configuration["Jwt:Issuer"], + ValidAudience = _configuration["Jwt:Audience"], + ClockSkew = TimeSpan.Zero + }, out SecurityToken validatedToken); + + var jwtToken = (JwtSecurityToken)validatedToken; + var userId = jwtToken.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; + + // 将用户信息附加到上下文中 + context.Items["User"] = userId; + } + catch + { + // 令牌验证失败,不附加用户信息 + } + } + } +} \ No newline at end of file diff --git a/src/Models/CommonRsp.cs b/src/Models/CommonRsp.cs index 7c0a6dc..d38463b 100644 --- a/src/Models/CommonRsp.cs +++ b/src/Models/CommonRsp.cs @@ -6,7 +6,7 @@ public class CommonRsp { public string Message { get; set; } = "def msg"; - public object Data { get; set; } = new object(); + public object Data { get; set; } = null; public int Retcode { get; set; } } } diff --git a/src/Models/User.cs b/src/Models/User.cs new file mode 100644 index 0000000..dbd01e5 --- /dev/null +++ b/src/Models/User.cs @@ -0,0 +1,135 @@ +using Newtonsoft.Json; + +namespace iFileProxy.Models +{ + public enum UserMask + { + User = 0, + Admin = 1, + SuperAdmin = 2 // 超级管理员 + } + public enum UserState + { + None = 0, + Blocked = 1 + } + public enum UserEventType + { + Login = 0, + Logout = 1, + Registry = 2, + UpdateMask = 3, // 添加权限修改事件类型 + UpdatePassword = 4, // 添加密码修改事件类型 + UpdateNickname = 5, // 添加昵称修改事件类型 + } + public class User + { + /// + /// 用户ID + /// + [JsonProperty("user_id")] + public string UserId { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 昵称 + /// + [JsonProperty("nickname")] + public string Nickname { get; set; } = string.Empty; + + /// + /// 用户名 + /// + [JsonProperty("username")] + public string Username { get; set; } = string.Empty; + + /// + /// 密码哈希 + /// + [JsonProperty("password_hash")] + public string PasswordHash { get; set; } = string.Empty; + + /// + /// 用户权限掩码 + /// + [JsonProperty("mask")] + public UserMask Mask { get; set; } = UserMask.User; + + /// + /// 用户状态 + /// + [JsonProperty("state")] + public UserState State { get; set; } = UserState.None; + + /// + /// 创建时间 + /// + [JsonProperty("create_time")] + public DateTime CreateTime { get; set; } = DateTime.Now; + + /// + /// 最后登录时间 + /// + [JsonProperty("last_login_time")] + public DateTime LastLoginTime { get; set; } + + /// + /// 最后登录IP + /// + [JsonProperty("last_login_ip")] + public string LastLoginIP { get; set; } = string.Empty; + } + + public class UserEvent + { + /// + /// 事件ID + /// + [JsonProperty("event_id")] + public string EventId { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 用户ID + /// + [JsonProperty("user_id")] + public string UserId { get; set; } = string.Empty; + + /// + /// 事件类型 + /// + [JsonProperty("event_type")] + public UserEventType EventType { get; set; } + + /// + /// 事件时间 + /// + [JsonProperty("event_time")] + public DateTime EventTime { get; set; } = DateTime.Now; + + /// + /// 事件IP + /// + [JsonProperty("event_ip")] + public string EventIP { get; set; } = string.Empty; + + /// + /// 事件详情 + /// + [JsonProperty("event_detail")] + public string EventDetail { get; set; } = string.Empty; + } + + public class UserEventDetail : UserEvent + { + /// + /// 用户名 + /// + [JsonProperty("username")] + public string Username { get; set; } = string.Empty; + + /// + /// 昵称 + /// + [JsonProperty("nickname")] + public string Nickname { get; set; } = string.Empty; + } +} diff --git a/src/Program.cs b/src/Program.cs index 31cbfec..8aebddf 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -3,6 +3,9 @@ using iFileProxy.Helpers; using iFileProxy.Middleware; using iFileProxy.Services; using Serilog; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; namespace iFileProxy { @@ -24,7 +27,7 @@ namespace iFileProxy builder => { builder - .WithOrigins("http://localhost:3000") // ǰ˵ַ + .WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050") // ǰ˵ַ .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); @@ -52,6 +55,26 @@ namespace iFileProxy builder.Services.AddSingleton(); + // ֤ + builder.Services.AddScoped(); + + // JWT֤ + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) + }; + }); + var app = builder.Build(); AppConfig.CheckAppConfig(app.Services); @@ -97,6 +120,9 @@ namespace iFileProxy app.Services.GetRequiredService>>(), AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP); + // м + app.UseMiddleware(); + app.MapControllers(); app.Run(); diff --git a/src/Properties/PublishProfiles/FolderProfile.pubxml b/src/Properties/PublishProfiles/FolderProfile.pubxml index 8285a16..1296e0c 100644 --- a/src/Properties/PublishProfiles/FolderProfile.pubxml +++ b/src/Properties/PublishProfiles/FolderProfile.pubxml @@ -15,7 +15,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <_TargetId>Folder net8.0 - win-x64 + linux-x64 e343bd8a-27ed-47e2-b50d-e3000730e65e false true diff --git a/src/Services/AuthService.cs b/src/Services/AuthService.cs new file mode 100644 index 0000000..7b2a169 --- /dev/null +++ b/src/Services/AuthService.cs @@ -0,0 +1,141 @@ +using BCrypt.Net; +using iFileProxy.Models; +using System.Security.Claims; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace iFileProxy.Services +{ + public class AuthService + { + private readonly DatabaseGateService _dbGateService; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public AuthService(DatabaseGateService dbGateService, IConfiguration configuration, ILogger logger) + { + _dbGateService = dbGateService; + _configuration = configuration; + _logger = logger; + } + + public async Task<(bool success, string message)> RegisterAsync(string username, string password, string ip, string nickname) + { + try + { + // 检查用户名是否已存在 + if (await _dbGateService.UserExistsAsync(username)) + { + return (false, "用户名已存在"); + } + + // 检查是否是第一个用户 + var isFirstUser = await _dbGateService.GetUserCountAsync() == 0; + + // 创建新用户 + var user = new User + { + Username = username, + PasswordHash = BCrypt.Net.BCrypt.HashPassword(password), + LastLoginIP = ip, + // 如果是第一个用户,设置为超级管理员 + Mask = isFirstUser ? UserMask.SuperAdmin : UserMask.User, + Nickname = nickname + }; + + // 保存用户 + await _dbGateService.CreateUserAsync(user); + + // 记录注册事件 + var userEvent = new UserEvent + { + UserId = user.UserId, + EventType = UserEventType.Registry, + EventIP = ip, + EventDetail = isFirstUser ? "管理员注册" : "用户注册" + }; + await _dbGateService.CreateUserEventAsync(userEvent); + + return (true, isFirstUser ? "管理员注册成功" : "注册成功"); + } + catch (Exception ex) + { + _logger.LogError(ex, "用户注册失败"); + return (false, "注册失败"); + } + } + + public async Task<(bool success, string token, string message)> LoginAsync(string username, string password, string ip) + { + try + { + var user = await _dbGateService.GetUserByUsernameAsync(username); + if (user == null) + { + return (false, string.Empty, "用户不存在"); + } + + if (user.State == UserState.Blocked) + { + return (false, string.Empty, "账户已被封禁"); + } + + if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash)) + { + return (false, string.Empty, "密码错误"); + } + + // 更新登录信息 + user.LastLoginTime = DateTime.Now; + user.LastLoginIP = ip; + await _dbGateService.UpdateUserAsync(user); + + // 记录登录事件 + var userEvent = new UserEvent + { + UserId = user.UserId, + EventType = UserEventType.Login, + EventIP = ip, + EventDetail = "用户登录" + }; + await _dbGateService.CreateUserEventAsync(userEvent); + + // 生成JWT令牌 + var token = GenerateJwtToken(user); + + return (true, token, "登录成功"); + } + catch (Exception ex) + { + _logger.LogError(ex, "用户登录失败"); + return (false, string.Empty, "登录失败"); + } + } + + private string GenerateJwtToken(User user) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, user.UserId), + new Claim(ClaimTypes.Name, user.Username), + new Claim(ClaimTypes.Role, user.Mask.ToString()) + }; + + // 确保密钥至少32字节长 + var key = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ?? + "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security")); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _configuration["Jwt:Issuer"] ?? "iFileProxy", + audience: _configuration["Jwt:Audience"] ?? "iFileProxy.Client", + claims: claims, + expires: DateTime.Now.AddDays(1), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + } +} \ No newline at end of file diff --git a/src/Services/DatabaseGateService.cs b/src/Services/DatabaseGateService.cs index 9e864b4..9d76e73 100644 --- a/src/Services/DatabaseGateService.cs +++ b/src/Services/DatabaseGateService.cs @@ -6,6 +6,7 @@ using MySql.Data.MySqlClient; using iFileProxy.Models; using Newtonsoft.Json; using System.Text; +using System.Threading.Tasks; namespace iFileProxy.Services { @@ -17,6 +18,52 @@ namespace iFileProxy.Services Dictionary _dbDictionary = []; + private const string CREATE_TASKS_TABLE_SQL = """ + CREATE TABLE IF NOT EXISTS `t_tasks_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', + `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务id', + `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `client_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', + `add_time` datetime NOT NULL COMMENT '任务在何时被添加', + `update_time` datetime NOT NULL COMMENT '任务状态更新时间', + `status` int(11) NULL DEFAULT NULL COMMENT '任务状态', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url', + `size` int(11) NULL DEFAULT NULL COMMENT '文件大小', + `hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件hash', + PRIMARY KEY (`id`) USING BTREE + ) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + """; + + private const string CREATE_USERS_TABLE_SQL = """ + CREATE TABLE IF NOT EXISTS `t_users` ( + `user_id` varchar(36) NOT NULL, + `username` varchar(50) NOT NULL, + `nickname` varchar(50) NOT NULL, + `password_hash` varchar(255) NOT NULL, + `mask` int NOT NULL DEFAULT 0, + `state` int NOT NULL DEFAULT 0, + `create_time` datetime NOT NULL, + `last_login_time` datetime NULL, + `last_login_ip` varchar(45) NULL, + PRIMARY KEY (`user_id`), + UNIQUE KEY `username` (`username`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """; + + private const string CREATE_USER_EVENTS_TABLE_SQL = """ + CREATE TABLE IF NOT EXISTS `t_user_events` ( + `event_id` varchar(36) NOT NULL, + `user_id` varchar(36) NOT NULL, + `event_type` int NOT NULL, + `event_time` datetime NOT NULL, + `event_ip` varchar(45) NULL, + `event_detail` text NULL, + PRIMARY KEY (`event_id`), + KEY `user_id` (`user_id`), + CONSTRAINT `fk_user_events_user` FOREIGN KEY (`user_id`) REFERENCES `t_users` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """; + public DatabaseGateService(AppConfig appConfig) { _logger.Information("Initializing DatabaseGateService..."); @@ -113,9 +160,11 @@ namespace iFileProxy.Services /// public static string QueryTableData(MySqlCommand sqlCmd) { - DataTable dataTable = new(); - using (MySqlDataAdapter adapter = new (sqlCmd)) - adapter.Fill(dataTable); + var dataTable = new DataTable(); + using (var adapter = new MySqlDataAdapter(sqlCmd)) + { + adapter.Fill(dataTable); + } return JsonConvert.SerializeObject(dataTable); } @@ -367,37 +416,24 @@ namespace iFileProxy.Services public void TryInitialDB() { - string sql = """ - CREATE TABLE `t_tasks_info` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', - `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务id', - `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, - `client_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', - `add_time` datetime NOT NULL COMMENT '任务在何时被添加', - `update_time` datetime NOT NULL COMMENT '任务状态更新时间', - `status` int(11) NULL DEFAULT NULL COMMENT '任务状态', - `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url', - `size` int(11) NULL DEFAULT NULL COMMENT '文件大小', - `hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件hash', - PRIMARY KEY (`id`) USING BTREE - ) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; - """; - MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db"); - + using var conn = GetAndOpenDBConn("iFileProxy_Db"); try { - using MySqlCommand cmd = new(sql, conn); - cmd.ExecuteNonQuery(); + // 创建表 + using (var cmd = new MySqlCommand(CREATE_TASKS_TABLE_SQL, conn)) + cmd.ExecuteNonQuery(); + using (var cmd = new MySqlCommand(CREATE_USERS_TABLE_SQL, conn)) + cmd.ExecuteNonQuery(); + using (var cmd = new MySqlCommand(CREATE_USER_EVENTS_TABLE_SQL, conn)) + cmd.ExecuteNonQuery(); + + _logger.Information("数据库表结构初始化完成"); } - catch (Exception) + catch (Exception ex) { - _logger.Error($"Db Init Fai.l"); + _logger.Error($"数据库初始化失败: {ex.Message}"); throw; } - finally - { - conn.Close(); - } } public T ExecuteScalar(string sql, Dictionary parameters) { @@ -574,5 +610,265 @@ namespace iFileProxy.Services throw; } } + + public async Task GetUserByIdAsync(string userId) + { + var sql = "SELECT * FROM t_users WHERE user_id = @userId"; + var parameters = new Dictionary + { + { "@userId", userId } + }; + + var users = await ExecuteQueryAsync(sql, parameters); + return users.FirstOrDefault(); + } + + public async Task UserExistsAsync(string username) + { + var sql = "SELECT COUNT(*) FROM t_users WHERE username = @username"; + var parameters = new Dictionary + { + { "@username", username } + }; + var count = await ExecuteScalarAsync(sql, parameters); + return count > 0; + } + + public async Task 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) + VALUES (@userId, @username, @passwordHash, @mask, @state, @createTime, @lastLoginTime, @lastLoginIp, @nickname)"; + + var parameters = new Dictionary + { + { "@userId", user.UserId }, + { "@username", user.Username }, + { "@passwordHash", user.PasswordHash }, + { "@mask", (int)user.Mask }, + { "@state", (int)user.State }, + { "@createTime", user.CreateTime }, + { "@lastLoginTime", user.LastLoginTime }, + { "@lastLoginIp", user.LastLoginIP }, + { "@nickname", user.Nickname }, + + }; + + var result = await ExecuteNonQueryAsync(sql, parameters); + return result > 0; + } + + public async Task CreateUserEventAsync(UserEvent userEvent) + { + var sql = @"INSERT INTO t_user_events (event_id, user_id, event_type, event_time, event_ip, event_detail) + VALUES (@eventId, @userId, @eventType, @eventTime, @eventIp, @eventDetail)"; + + var parameters = new Dictionary + { + { "@eventId", userEvent.EventId }, + { "@userId", userEvent.UserId }, + { "@eventType", (int)userEvent.EventType }, + { "@eventTime", userEvent.EventTime }, + { "@eventIp", userEvent.EventIP }, + { "@eventDetail", userEvent.EventDetail } + }; + + var result = await ExecuteNonQueryAsync(sql, parameters); + return result > 0; + } + + public async Task GetUserByUsernameAsync(string username) + { + var sql = "SELECT * FROM t_users WHERE username = @username"; + var parameters = new Dictionary + { + { "@username", username } + }; + + var users = await ExecuteQueryAsync(sql, parameters); + return users.FirstOrDefault(); + } + + public async Task UpdateUserAsync(User user) + { + var sql = @"UPDATE t_users SET + username = @username, + password_hash = @passwordHash, + mask = @mask, + state = @state, + last_login_time = @lastLoginTime, + last_login_ip = @lastLoginIp, + nickname = @nickname + WHERE user_id = @userId"; + + var parameters = new Dictionary + { + { "@userId", user.UserId }, + { "@username", user.Username }, + { "@passwordHash", user.PasswordHash }, + { "@mask", (int)user.Mask }, + { "@state", (int)user.State }, + { "@lastLoginTime", user.LastLoginTime }, + { "@lastLoginIp", user.LastLoginIP }, + { "@nickname", user.Nickname } + + }; + + var result = await ExecuteNonQueryAsync(sql, parameters); + return result > 0; + } + + private async Task ExecuteScalarAsync(string sql, Dictionary parameters) + { + using var conn = GetAndOpenDBConn("iFileProxy_Db"); + using var cmd = new MySqlCommand(sql, conn); + foreach (var param in parameters) + { + cmd.Parameters.AddWithValue(param.Key, param.Value); + } + return (T)await cmd.ExecuteScalarAsync(); + } + + private async Task> ExecuteQueryAsync(string sql, Dictionary parameters) + { + using var conn = GetAndOpenDBConn("iFileProxy_Db"); + using var cmd = new MySqlCommand(sql, conn); + foreach (var param in parameters) + { + cmd.Parameters.AddWithValue(param.Key, param.Value); + } + + // 使用 DataTable 来避免 DataReader 冲突 + var dataTable = new DataTable(); + using (var adapter = new MySqlDataAdapter(cmd)) + { + await Task.Run(() => adapter.Fill(dataTable)); + } + + return JsonConvert.DeserializeObject>( + JsonConvert.SerializeObject(dataTable) + ); + } + + private async Task ExecuteNonQueryAsync(string sql, Dictionary parameters) + { + using var conn = GetAndOpenDBConn("iFileProxy_Db"); + using var cmd = new MySqlCommand(sql, conn); + foreach (var param in parameters) + { + cmd.Parameters.AddWithValue(param.Key, param.Value); + } + return await cmd.ExecuteNonQueryAsync(); + } + + public async Task GetUserCountAsync() + { + var sql = "SELECT COUNT(*) FROM t_users"; + var parameters = new Dictionary(); + return await ExecuteScalarAsync(sql, parameters); + } + + public async Task> GetPagedUserListAsync(int page, int pageSize, UserMask? mask = null) + { + // 构建基础SQL + var sql = new StringBuilder("SELECT * FROM t_users"); + var parameters = new Dictionary(); + + // 添加权限过滤 + if (mask.HasValue) + { + sql.Append(" WHERE mask = @mask"); + parameters.Add("@mask", (int)mask.Value); + } + + // 添加分页 + sql.Append(" ORDER BY create_time DESC LIMIT @offset, @limit"); + parameters.Add("@offset", (page - 1) * pageSize); + parameters.Add("@limit", pageSize); + + // 获取总记录数 + var countSql = "SELECT COUNT(*) FROM t_users" + + (mask.HasValue ? " WHERE mask = @mask" : ""); + var totalCount = await ExecuteScalarAsync(countSql, parameters); + + // 获取分页数据 + var users = await ExecuteQueryAsync(sql.ToString(), parameters); + + return new PagedResult + { + Total = totalCount, + PageSize = pageSize, + CurrentPage = page, + TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize), + Data = users + }; + } + + public async Task> GetPagedUserEventsAsync( + int page, + int pageSize, + string? userId = null, + string? keyword = null) + { + // 构建基础SQL + var sql = new StringBuilder(@" + SELECT e.*, u.username, u.nickname + FROM t_user_events e + LEFT JOIN t_users u ON e.user_id = u.user_id + WHERE 1=1"); + + var parameters = new Dictionary(); + + // 添加用户ID过滤 + if (!string.IsNullOrEmpty(userId)) + { + sql.Append(" AND e.user_id = @userId"); + parameters.Add("@userId", userId); + } + + // 添加关键词搜索 + if (!string.IsNullOrEmpty(keyword)) + { + sql.Append(@" AND ( + e.event_detail LIKE @keyword OR + u.username LIKE @keyword OR + u.nickname LIKE @keyword OR + e.event_ip LIKE @keyword + )"); + parameters.Add("@keyword", $"%{keyword}%"); + } + + // 添加分页 + sql.Append(" ORDER BY e.event_time DESC LIMIT @offset, @limit"); + parameters.Add("@offset", (page - 1) * pageSize); + parameters.Add("@limit", pageSize); + + // 获取总记录数 + var countSql = @" + SELECT COUNT(*) + FROM t_user_events e + LEFT JOIN t_users u ON e.user_id = u.user_id + WHERE 1=1" + + (!string.IsNullOrEmpty(userId) ? " AND e.user_id = @userId" : "") + + (!string.IsNullOrEmpty(keyword) ? @" AND ( + e.event_detail LIKE @keyword OR + u.username LIKE @keyword OR + u.nickname LIKE @keyword OR + e.event_ip LIKE @keyword + )" : ""); + + var totalCount = await ExecuteScalarAsync(countSql, parameters); + + // 获取分页数据 + var events = await ExecuteQueryAsync(sql.ToString(), parameters); + + return new PagedResult + { + Total = totalCount, + PageSize = pageSize, + CurrentPage = page, + TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize), + Data = events + }; + } } } diff --git a/src/appsettings.json b/src/appsettings.json index 10f68b8..42f9c78 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -5,5 +5,10 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Jwt": { + "Key": "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security", + "Issuer": "iFileProxy", + "Audience": "iFileProxy.Client" + } } diff --git a/src/document/iFileProxy.sql b/src/document/iFileProxy.sql new file mode 100644 index 0000000..f5a86d6 --- /dev/null +++ b/src/document/iFileProxy.sql @@ -0,0 +1,67 @@ +/* + Navicat Premium Data Transfer + + Source Server : 香港轻量应用_阿里云 + Source Server Type : MySQL + Source Server Version : 50743 + Source Host : 127.0.0.1:3306 + Source Schema : iFileProxy + + Target Server Type : MySQL + Target Server Version : 50743 + File Encoding : 65001 + + Date: 30/11/2024 19:32:04 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for t_tasks_info +-- ---------------------------- +DROP TABLE IF EXISTS `t_tasks_info`; +CREATE TABLE `t_tasks_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', + `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务id', + `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件名', + `client_ip` varchar(46) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', + `add_time` datetime NOT NULL COMMENT '任务在何时被添加', + `update_time` datetime NOT NULL COMMENT '任务状态更新时间', + `status` int(1) NULL DEFAULT NULL COMMENT '任务状态', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url', + `size` bigint(11) NULL DEFAULT NULL COMMENT '文件大小', + `hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件hash', + `tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标记', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `tid`(`tid`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8128 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_user_events +-- ---------------------------- +DROP TABLE IF EXISTS `t_user_events`; +CREATE TABLE `t_user_events` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', + `event_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '事件类型', + `trigger_time` datetime NOT NULL COMMENT '触发时间(Server)', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '触发者IP', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_users +-- ---------------------------- +DROP TABLE IF EXISTS `t_users`; +CREATE TABLE `t_users` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电子邮件', + `role_mask` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '身份掩码', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '外显名称(昵称)', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/iFileProxy.csproj b/src/iFileProxy.csproj index f030dee..b974f2d 100644 --- a/src/iFileProxy.csproj +++ b/src/iFileProxy.csproj @@ -14,6 +14,8 @@ + +