diff --git a/Apimanager_backend/Apimanager_backend.csproj b/Apimanager_backend/Apimanager_backend.csproj index 533afb7..4874a8a 100644 --- a/Apimanager_backend/Apimanager_backend.csproj +++ b/Apimanager_backend/Apimanager_backend.csproj @@ -8,11 +8,17 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Apimanager_backend/Config/ServiceCollectionExtensions.cs b/Apimanager_backend/Config/ServiceCollectionExtensions.cs index 28a5528..295bb77 100644 --- a/Apimanager_backend/Config/ServiceCollectionExtensions.cs +++ b/Apimanager_backend/Config/ServiceCollectionExtensions.cs @@ -1,5 +1,10 @@ using Apimanager_backend.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; +using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Text; namespace Apimanager_backend.Config { @@ -8,7 +13,36 @@ namespace Apimanager_backend.Config public static IServiceCollection AddAllService(this IServiceCollection services,IConfiguration configuration) { services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + services.AddJWTService(configuration); services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + public static IServiceCollection AddJWTService(this IServiceCollection services,IConfiguration configuration) + { + var jwtSettings = configuration.GetSection("JwtSettings"); + var key = Encoding.ASCII.GetBytes(jwtSettings["Secret"]); + // JWT配置 + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings["Issuer"], + ValidAudience = jwtSettings["Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key) + }; + }); + + //redis配置 + services.AddSingleton(ConnectionMultiplexer.Connect(configuration["Redis:ConnectionString"])); return services; } } diff --git a/Apimanager_backend/Controllers/AuthController.cs b/Apimanager_backend/Controllers/AuthController.cs new file mode 100644 index 0000000..a6f8465 --- /dev/null +++ b/Apimanager_backend/Controllers/AuthController.cs @@ -0,0 +1,158 @@ +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Models; +using Apimanager_backend.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualBasic; + +namespace Apimanager_backend.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class AuthController : ControllerBase + { + private readonly IAuthService authService; + private readonly ITokenService tokenService; + private readonly IRefreshTokenService refreshTokenService; + private readonly IUserService userService; + public AuthController(IAuthService authService, ITokenService tokenService, IRefreshTokenService refreshTokenService,IUserService userService) + { + this.authService = authService; + this.tokenService = tokenService; + this.refreshTokenService = refreshTokenService; + this.userService = userService; + } + /// + /// 用户登录控制器 + /// + /// 登录信息 + /// 通用返回信息格式 + [HttpPost] + public async Task>> Login([FromBody] UserLoginDto dto) + { + UserInfoDto user = await authService.LoginAsync(dto.UserName, dto.Password); + //生成token + string token = tokenService.GenerateAccessToken(user.Id.ToString(), user.Roles); + //生成refreshtoken + string refreshToken = await refreshTokenService.CreateRefereshTokenAsync(user.Id.ToString()); + var responseInfo = new ResponseBase( + code: 2000, + message: "Login successful", + data: new LoginResponseDto + { + UserInfo = user, + Token = token, + RefreshToken = refreshToken + } + ); + return Ok(responseInfo); + } + /// + /// 令牌刷新 + /// + /// 传入用户令牌 + /// 返回新令牌 + [HttpPost] + public async Task>> Refresh([FromBody]RefreshResponseDto dto) + { + var IsRefreshToken = await refreshTokenService.ValidateRefreshTokenAsync(dto.UserId.ToString(),dto.RefreshToken); + //刷新令牌无效 + if (!IsRefreshToken) + { + var ret = new ResponseBase( + code: 2008, + message: "Refresh expires or is invalid", + data: null + ); + return Unauthorized(ret); + } + //获取刷新令牌对应用户信息 + var userInfo = await userService.GetUserAsync(dto.UserId); + //重新生成令牌 + var token = tokenService.GenerateAccessToken(userInfo.Id.ToString(), userInfo.Roles); + //刷新刷新令牌有效期(小于三天才会刷新) + await refreshTokenService.UpdateRefreshTokenAsync(userInfo.Id.ToString()); + var result = new ResponseBase( + code: 1000, + message: "Success", + data: new RefreshResponseDto + { + UserId = dto.UserId, + Token = token, + RefreshToken = dto.RefreshToken + } + + ); + return Ok(result); + } + /// + /// 用户注册 + /// + /// + /// + [HttpPost] + public async Task>> Register(RegisterRequestDto requestDto) + { + var isUsernameExist = await userService.IsUsernameExist(requestDto.Username); + if (isUsernameExist) + { + var errorRes = new ResponseBase( + code:2003, + message:"用户名已存在", + data:null + ); + return StatusCode(409,errorRes); + } + try + { + var userInfo = await authService.RegisterAsync(requestDto); + var res = new ResponseBase( + code:1000, + message:"Success", + data:userInfo + ); + return Ok(res); + }catch(BaseException e) + { + var res = new ResponseBase( + code:e.code, + message:e.message, + data: null + ); + return StatusCode(500,res); + } + + } + /// + /// 发送邮箱校验码 + /// + /// + /// + [HttpPost] + public async Task>> SendValidateCode([FromQuery]string email) + { + //检测邮箱是否被使用 + var emailIsUse = await userService.IsEmailExist(email); + if (emailIsUse) + { + var errorRes = new ResponseBase( + code:2005, + message: "邮箱已存在", + data:null + ); + return StatusCode(409,errorRes); + } + //发送注册验证码 + await authService.SendRegisterCodeAsync(email); + var res = new ResponseBase( + code:1000, + message:"Success", + data: null + ); + return Ok(res); + } + + } +} diff --git a/Apimanager_backend/Controllers/UserController.cs b/Apimanager_backend/Controllers/UserController.cs index 279a954..6babc8f 100644 --- a/Apimanager_backend/Controllers/UserController.cs +++ b/Apimanager_backend/Controllers/UserController.cs @@ -7,8 +7,7 @@ using Apimanager_backend.Filters; namespace Apimanager_backend.Controllers { - [ModelValidationFilter()] - [Route("api/[controller]")] + [Route("api/[controller]/[action]")] [ApiController] public class UserController : ControllerBase { @@ -17,37 +16,6 @@ namespace Apimanager_backend.Controllers { this.userService = userService; } - /// - /// 用户登录控制器 - /// - /// 登录信息 - /// 通用返回信息格式 - [HttpPost("Login")] - public async Task>> Login([FromBody]UserLoginDto dto) - { - try - { - UserInfoDto user = await userService.LoginAsync(dto.UserName, dto.Password); - - var responseInfo = new ResponseBase( - code: 2000, - message: "Login successful", - data: user - ); - return Ok(responseInfo); - } - catch (BaseException e) - { - - //错误时,构建错误信息对象 - var responseInfo = new ResponseBase( - code:e.code, - message: e.message, - data: null - ); - - return Unauthorized(responseInfo); - } - } + } } diff --git a/Apimanager_backend/Data/ApiContext.cs b/Apimanager_backend/Data/ApiContext.cs index 9038552..f43a410 100644 --- a/Apimanager_backend/Data/ApiContext.cs +++ b/Apimanager_backend/Data/ApiContext.cs @@ -20,6 +20,10 @@ namespace Apimanager_backend.Data public DbSet Orders { get; set; } //用户已订购套餐表 public DbSet UserPackages { get; set; } + //用户角色表 + public DbSet UserRoles { get; set; } + //日志表 + public DbSet Logs { get; set; } public ApiContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -27,6 +31,11 @@ namespace Apimanager_backend.Data // 配置全局查询筛选器 modelBuilder.Entity().HasQueryFilter(u => !u.IsDelete); modelBuilder.Entity().HasQueryFilter(a => !a.IsDelete); + //配置日志表 + modelBuilder.Entity().HasKey(x => x.Id); + modelBuilder.Entity() + .Property(x => x.Id) + .ValueGeneratedOnAdd(); } } diff --git a/Apimanager_backend/Data/UserRoleConfig.cs b/Apimanager_backend/Data/UserRoleConfig.cs new file mode 100644 index 0000000..401058d --- /dev/null +++ b/Apimanager_backend/Data/UserRoleConfig.cs @@ -0,0 +1,21 @@ +using Apimanager_backend.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Apimanager_backend.Data +{ + public class UserRoleConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + //自增 + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + //外键 + builder.HasOne(x => x.User) + .WithMany(u => u.Roles) + .HasForeignKey(x => x.UserId); + } + } +} diff --git a/Apimanager_backend/Dtos/LoginResponseDto.cs b/Apimanager_backend/Dtos/LoginResponseDto.cs new file mode 100644 index 0000000..0dd5e0d --- /dev/null +++ b/Apimanager_backend/Dtos/LoginResponseDto.cs @@ -0,0 +1,11 @@ +using Apimanager_backend.Models; + +namespace Apimanager_backend.Dtos +{ + public class LoginResponseDto + { + public UserInfoDto UserInfo { get; set; } + public string Token { get; set; } + public string RefreshToken { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/RefreshResponseDto.cs b/Apimanager_backend/Dtos/RefreshResponseDto.cs new file mode 100644 index 0000000..642b43c --- /dev/null +++ b/Apimanager_backend/Dtos/RefreshResponseDto.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Apimanager_backend.Dtos +{ + public class RefreshResponseDto + { + [Required(ErrorMessage = "用户ID必填!")] + public int UserId { get; set; } + public string? Token { get; set; } + [Required(ErrorMessage = "刷新令牌必填!")] + public string RefreshToken { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/RegisterRequestDto.cs b/Apimanager_backend/Dtos/RegisterRequestDto.cs new file mode 100644 index 0000000..192b02f --- /dev/null +++ b/Apimanager_backend/Dtos/RegisterRequestDto.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Apimanager_backend.Dtos +{ + public class RegisterRequestDto + { + [Required(ErrorMessage = "用户名必填!")] + [MaxLength(20,ErrorMessage = "用户名最长20字符!")] + public string Username { get; set; } + [Required(ErrorMessage = "密码必填!")] + public string Password { get; set; } + [Required(ErrorMessage = "邮箱必填!")] + public string Email { get; set; } + [Required(ErrorMessage = "验证码必填!")] + public string VerificationCode { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/UserInfoBaseDto.cs b/Apimanager_backend/Dtos/UserInfoBaseDto.cs new file mode 100644 index 0000000..485c1b2 --- /dev/null +++ b/Apimanager_backend/Dtos/UserInfoBaseDto.cs @@ -0,0 +1,16 @@ +using Apimanager_backend.Models; + +namespace Apimanager_backend.Dtos +{ + public class UserInfoBaseDto + { + public UserInfoDto UserInfo { get; set; } + public List Roles { get; set; } + public UserInfoBaseDto(UserInfoDto userInfo, List roles) + { + UserInfo = userInfo; + Roles = roles; + } + public UserInfoBaseDto() { } + } +} diff --git a/Apimanager_backend/Dtos/UserInfoDto.cs b/Apimanager_backend/Dtos/UserInfoDto.cs index 927ffed..afe3cca 100644 --- a/Apimanager_backend/Dtos/UserInfoDto.cs +++ b/Apimanager_backend/Dtos/UserInfoDto.cs @@ -7,7 +7,7 @@ namespace Apimanager_backend.Dtos public int Id { get; set; } public string UserName { get; set; } public string Email { get; set; } - public UserRole Role { get; set; } + public List Roles { get; set; } public bool IsBan { get; set; } public decimal Balance { get; set; } public DateTime Created { get; set; } diff --git a/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs b/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs new file mode 100644 index 0000000..67d376f --- /dev/null +++ b/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs @@ -0,0 +1,30 @@ +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Tools; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Apimanager_backend.Filters.ExceptionFilter +{ + public class generalExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + if(context.Exception is BaseException) + { + //构造通用错误返回结果 + BaseException exception = (BaseException)context.Exception; + var res = new ResponseBase( + code:exception.code, + message: exception.message, + data:null + ); + int httpCode = StatusCodeHelper.GetHttpStatusCode(exception.code); + context.Result = new JsonResult(res) { + StatusCode = httpCode + }; + context.ExceptionHandled = true; + } + } + } +} diff --git a/Apimanager_backend/Filters/ModelValidationFilter.cs b/Apimanager_backend/Filters/ModelValidationFilter.cs index 8349db8..7760d2e 100644 --- a/Apimanager_backend/Filters/ModelValidationFilter.cs +++ b/Apimanager_backend/Filters/ModelValidationFilter.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace Apimanager_backend.Filters { - public class ModelValidationFilter : Attribute,IActionFilter + public class ModelValidationFilter :IActionFilter { public void OnActionExecuted(ActionExecutedContext context) { } diff --git a/Apimanager_backend/Migrations/20241030160723_add-userrole-table.Designer.cs b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.Designer.cs new file mode 100644 index 0000000..1fb1294 --- /dev/null +++ b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.Designer.cs @@ -0,0 +1,408 @@ +// +using System; +using Apimanager_backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + [DbContext(typeof(ApiContext))] + [Migration("20241030160723_add-userrole-table")] + partial class adduserroletable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("IsThirdParty") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.ToTable("Apis"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiId") + .HasColumnType("int"); + + b.Property("CallResult") + .HasColumnType("int"); + + b.Property("CallTime") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApiId"); + + b.HasIndex("UserId"); + + b.ToTable("CallLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CallLimit") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Apipackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("Operation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TargetId") + .HasColumnType("int"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UserAgent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("OperationLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OrderType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThirdPartyOrderId") + .HasColumnType("varchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("ThirdPartyOrderId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Balance") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsBan") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("PassHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("PurchasedAt") + .HasColumnType("datetime(6)"); + + b.Property("RemainingCalls") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Apis") + .HasForeignKey("PackageId"); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.HasOne("Apimanager_backend.Models.Api", "Api") + .WithMany("ApiCalls") + .HasForeignKey("ApiId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("CallLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Api"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("operationLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Packages") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Packages") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Navigation("ApiCalls"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Navigation("Apis"); + + b.Navigation("Packages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Navigation("CallLogs"); + + b.Navigation("Orders"); + + b.Navigation("Packages"); + + b.Navigation("Roles"); + + b.Navigation("operationLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apimanager_backend/Migrations/20241030160723_add-userrole-table.cs b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.cs new file mode 100644 index 0000000..f57483e --- /dev/null +++ b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + /// + public partial class adduserroletable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Role", + table: "Users"); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + Role = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => x.Id); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_UserId", + table: "UserRoles", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.AddColumn( + name: "Role", + table: "Users", + type: "int", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/Apimanager_backend/Migrations/20241103110714_update-logtable.Designer.cs b/Apimanager_backend/Migrations/20241103110714_update-logtable.Designer.cs new file mode 100644 index 0000000..f6857aa --- /dev/null +++ b/Apimanager_backend/Migrations/20241103110714_update-logtable.Designer.cs @@ -0,0 +1,442 @@ +// +using System; +using Apimanager_backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + [DbContext(typeof(ApiContext))] + [Migration("20241103110714_update-logtable")] + partial class updatelogtable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("IsThirdParty") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.ToTable("Apis"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiId") + .HasColumnType("int"); + + b.Property("CallResult") + .HasColumnType("int"); + + b.Property("CallTime") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApiId"); + + b.HasIndex("UserId"); + + b.ToTable("CallLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CallLimit") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Apipackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Exception") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MessageTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("Operation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TargetId") + .HasColumnType("int"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UserAgent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("OperationLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OrderType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThirdPartyOrderId") + .HasColumnType("varchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("ThirdPartyOrderId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Balance") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsBan") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("PassHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("PurchasedAt") + .HasColumnType("datetime(6)"); + + b.Property("RemainingCalls") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Apis") + .HasForeignKey("PackageId"); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.HasOne("Apimanager_backend.Models.Api", "Api") + .WithMany("ApiCalls") + .HasForeignKey("ApiId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("CallLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Api"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("operationLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Packages") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Packages") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Navigation("ApiCalls"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Navigation("Apis"); + + b.Navigation("Packages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Navigation("CallLogs"); + + b.Navigation("Orders"); + + b.Navigation("Packages"); + + b.Navigation("Roles"); + + b.Navigation("operationLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apimanager_backend/Migrations/20241103110714_update-logtable.cs b/Apimanager_backend/Migrations/20241103110714_update-logtable.cs new file mode 100644 index 0000000..81ee381 --- /dev/null +++ b/Apimanager_backend/Migrations/20241103110714_update-logtable.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + /// + public partial class updatelogtable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs b/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs index 83d8249..06c0db0 100644 --- a/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs +++ b/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs @@ -114,6 +114,40 @@ namespace Apimanager_backend.Migrations b.ToTable("Apipackages"); }); + modelBuilder.Entity("Apimanager_backend.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Exception") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MessageTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => { b.Property("Id") @@ -233,9 +267,6 @@ namespace Apimanager_backend.Migrations .HasMaxLength(255) .HasColumnType("varchar(255)"); - b.Property("Role") - .HasColumnType("int"); - b.Property("Username") .IsRequired() .HasColumnType("varchar(255)"); @@ -278,6 +309,26 @@ namespace Apimanager_backend.Migrations b.ToTable("UserPackages"); }); + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles"); + }); + modelBuilder.Entity("Apimanager_backend.Models.Api", b => { b.HasOne("Apimanager_backend.Models.Apipackage", "Package") @@ -347,6 +398,17 @@ namespace Apimanager_backend.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Apimanager_backend.Models.Api", b => { b.Navigation("ApiCalls"); @@ -367,6 +429,8 @@ namespace Apimanager_backend.Migrations b.Navigation("Packages"); + b.Navigation("Roles"); + b.Navigation("operationLogs"); }); #pragma warning restore 612, 618 diff --git a/Apimanager_backend/Models/Log.cs b/Apimanager_backend/Models/Log.cs new file mode 100644 index 0000000..ed15d8d --- /dev/null +++ b/Apimanager_backend/Models/Log.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Apimanager_backend.Models +{ + [Table("Logs")] + public class Log + { + public int Id { get; set; } + public DateTime Timestamp { get; set; } + public string Message { get; set; } + public string Exception { get; set; } + public string MessageTemplate { get; set; } + public string Properties { get; set; } + public string LogLevel { get; set; } + + } +} diff --git a/Apimanager_backend/Models/User.cs b/Apimanager_backend/Models/User.cs index 07c11fc..c3c3eb1 100644 --- a/Apimanager_backend/Models/User.cs +++ b/Apimanager_backend/Models/User.cs @@ -30,7 +30,7 @@ namespace Apimanager_backend.Models /// /// 用户角色 /// - public UserRole Role { get; set; } // Enum('Admin','User') + //public UserRole Role { get; set; } // Enum('Admin','User') /// /// 是否禁用 @@ -56,5 +56,6 @@ namespace Apimanager_backend.Models public ICollection operationLogs { get; set; } public ICollection CallLogs { get; set; } public ICollection Orders { get; set; } + public ICollection Roles { get; set; } = new List(); } } diff --git a/Apimanager_backend/Models/UserRole.cs b/Apimanager_backend/Models/UserRole.cs index cbd29ff..1df0b7a 100644 --- a/Apimanager_backend/Models/UserRole.cs +++ b/Apimanager_backend/Models/UserRole.cs @@ -1,8 +1,15 @@ -namespace Apimanager_backend.Models +using System.Text.Json.Serialization; + +namespace Apimanager_backend.Models { - public enum UserRole + public class UserRole { - Admin = 0, - User = 1 + public int Id { get; set; } + public int UserId { get; set; } + public string Role { get; set; } + + //导航属性 + [JsonIgnore] + public User User { get; set; } } } diff --git a/Apimanager_backend/Program.cs b/Apimanager_backend/Program.cs index 78c9887..6004bed 100644 --- a/Apimanager_backend/Program.cs +++ b/Apimanager_backend/Program.cs @@ -3,6 +3,8 @@ using Apimanager_backend.Data; using Apimanager_backend.Filters; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Sinks.MariaDB.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -12,8 +14,19 @@ IConfiguration configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); - +string? redStr = configuration["Redis:ConnectionString"]; string? constr = configuration.GetConnectionString("DefaultConnection"); +//־ +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.MariaDB( + connectionString: constr, + tableName: "Logs", + autoCreateTable:true + ).CreateLogger(); +builder.Host.UseSerilog(); + builder.Services.AddDbContext(option => option.UseMySql(constr, MySqlServerVersion.AutoDetect(constr)) ); @@ -21,11 +34,18 @@ builder.Services.AddAllService(configuration); builder.Services.AddControllers(options => { options.Filters.Add(); +}).ConfigureApiBehaviorOptions(option => +{ + option.SuppressModelStateInvalidFilter = true; +}) +.AddJsonOptions(options => +{ + options.JsonSerializerOptions.MaxDepth = 64; }); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - var app = builder.Build(); // Configure the HTTP request pipeline. @@ -37,6 +57,7 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/Apimanager_backend/Services/AuthService.cs b/Apimanager_backend/Services/AuthService.cs new file mode 100644 index 0000000..e1773b0 --- /dev/null +++ b/Apimanager_backend/Services/AuthService.cs @@ -0,0 +1,105 @@ +using Apimanager_backend.Data; +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Models; +using Apimanager_backend.Tools; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Apimanager_backend.Services +{ + public class AuthService:IAuthService + { + private readonly ApiContext apiContext; + private readonly ILogger logger; + private readonly IConnectionMultiplexer redis; + private readonly IEmailService emailService; + private readonly IMapper mapper; + private readonly int DbIndex = 1; + public AuthService(ApiContext apiContext, IMapper automapper,ILogger logger,IConnectionMultiplexer redis,IEmailService emailService) + { + this.apiContext = apiContext; + this.mapper = automapper; + this.logger = logger; + this.redis = redis; + this.emailService = emailService; + } + public async Task LoginAsync(string username, string password) + { + //查找用户 + User? user = await apiContext.Users.Include(x => x.Roles).SingleOrDefaultAsync(x => + x.Username == username && x.PassHash == password + ); + + //用户不存在或密码错误都为登录失败 + if (user == null) + { + throw new BaseException(2001, "Invalid username or password"); + } + + //用户被禁用 + if (user.IsBan) + { + throw new BaseException(2002, "User account is disabled"); + } + + return mapper.Map(user); + } + + public async Task RegisterAsync(RegisterRequestDto dto) + { + var db = redis.GetDatabase(DbIndex); + //获取邮箱对应验证码 + var code = await db.StringGetAsync(dto.Email); + if(!code.HasValue || code.ToString() != dto.VerificationCode) + { + throw new BaseException(5005,"验证码错误"); + } + User user = new User + { + Username = dto.Username, + PassHash = dto.Password, + Email = dto.Email, + IsBan = false, + IsDelete = false, + Balance = 0, + }; + try + { + //添加新用户 + await apiContext.Users.AddAsync(user); + await apiContext.SaveChangesAsync(); + UserRole userRole = new UserRole + { + UserId = user.Id, + Role = "User" + }; + await apiContext.UserRoles.AddAsync(userRole); + await apiContext.SaveChangesAsync(); + return mapper.Map(user); + }catch(Exception e) + { + throw new BaseException(1005,e.Message); + } + + } + + public async Task SendRegisterCodeAsync(string email) + { + //生成随机码 + string code = RandomCodeHelper.GetRandomCodeStr(); + string subject = "注册验证码"; + string body = $"您的注册验证码为:{code}
有效期60分钟!"; + //随机码写入redis + var db = redis.GetDatabase(DbIndex); + bool redisSuccess = await db.StringSetAsync(email,code,TimeSpan.FromHours(1)); + if (!redisSuccess) + { + throw new BaseException(1005,"Redis Str Set Error"); + } + //发送邮件 + await emailService.SendEmailAsync(email,subject,body); + } + } +} diff --git a/Apimanager_backend/Services/EmailSerivce.cs b/Apimanager_backend/Services/EmailSerivce.cs new file mode 100644 index 0000000..7205208 --- /dev/null +++ b/Apimanager_backend/Services/EmailSerivce.cs @@ -0,0 +1,49 @@ +using Apimanager_backend.Exceptions; +using System.Net; +using System.Net.Mail; + +namespace Apimanager_backend.Services +{ + public class EmailService:IEmailService + { + private readonly IConfiguration _configuration; + public EmailService(IConfiguration configuration) + { + _configuration = configuration; + SmtpHost = _configuration["EmailSettings:Server"]; + Port = int.Parse(_configuration["EmailSettings:Port"]); + Username = _configuration["EmailSettings:Username"]; + Password = _configuration["EmailSettings:Password"]; + EnableSSL = bool.Parse(_configuration["EmailSettings:Ssl"]); + } + private string SmtpHost { get; set; } + private int Port { get; set; } + public bool EnableSSL { get; set; } + private string Username { get; set; } + private string Password { get; set; } + public async Task SendEmailAsync(string toEmail,string subject,string body) + { + try + { + using SmtpClient smtpClient = new SmtpClient(SmtpHost, Port) + { + Credentials = new NetworkCredential(Username, Password), + EnableSsl = EnableSSL, //启用ssl + Timeout = 30000 + }; + using var emailMessage = new MailMessage + { + From = new MailAddress(Username), + Subject = subject, + Body = body, + IsBodyHtml = true + }; + emailMessage.To.Add(toEmail); + await smtpClient.SendMailAsync(emailMessage); + }catch(Exception e) + { + throw new BaseException(5004,e.Message); + } + } + } +} diff --git a/Apimanager_backend/Services/IAuthService.cs b/Apimanager_backend/Services/IAuthService.cs new file mode 100644 index 0000000..19d7ab4 --- /dev/null +++ b/Apimanager_backend/Services/IAuthService.cs @@ -0,0 +1,27 @@ +using Apimanager_backend.Dtos; + +namespace Apimanager_backend.Services +{ + public interface IAuthService + { + /// + /// 登录用户,根据用户名和密码进行身份验证。 + /// + /// 用户名 + /// 密码 + /// 包含用户信息的 + Task LoginAsync(string username, string password); + /// + /// 用户注册邮箱验证码 + /// + /// + /// + Task SendRegisterCodeAsync(string email); + /// + /// 用户注册 + /// + /// + /// + Task RegisterAsync(RegisterRequestDto dto); + } +} diff --git a/Apimanager_backend/Services/IEmailService.cs b/Apimanager_backend/Services/IEmailService.cs new file mode 100644 index 0000000..3dac127 --- /dev/null +++ b/Apimanager_backend/Services/IEmailService.cs @@ -0,0 +1,14 @@ +namespace Apimanager_backend.Services +{ + public interface IEmailService + { + /// + /// 发送邮件 + /// + /// 收件人邮箱 + /// 主题 + /// 正文 + /// + public Task SendEmailAsync(string toEmail,string subject,string body); + } +} diff --git a/Apimanager_backend/Services/IRefreshTokenService.cs b/Apimanager_backend/Services/IRefreshTokenService.cs new file mode 100644 index 0000000..22f7a16 --- /dev/null +++ b/Apimanager_backend/Services/IRefreshTokenService.cs @@ -0,0 +1,30 @@ +namespace Apimanager_backend.Services +{ + public interface IRefreshTokenService + { + /// + /// 创建刷新令牌 + /// + /// 用户id + /// 刷新令牌 + Task CreateRefereshTokenAsync(string userId); + /// + /// 验证刷新令牌 + /// + /// 刷新令牌 + /// 是否验证通过 + Task ValidateRefreshTokenAsync(string userId,string refreshToken); + /// + /// 删除刷新令牌 + /// + /// 刷新令牌 + /// 是否删除成功 + Task DeleterRefreshTokenAsync(string userId); + /// + /// 更新刷新令牌有效期 + /// + /// 刷新令牌 + /// 是否成功 + Task UpdateRefreshTokenAsync(string userId); + } +} diff --git a/Apimanager_backend/Services/ITokenService.cs b/Apimanager_backend/Services/ITokenService.cs new file mode 100644 index 0000000..1016997 --- /dev/null +++ b/Apimanager_backend/Services/ITokenService.cs @@ -0,0 +1,16 @@ +using Apimanager_backend.Models; + +namespace Apimanager_backend.Services +{ + public interface ITokenService + { + /// + /// 拥护凭证 + /// + /// 用户ID + /// 用户名 + /// 角色 + /// token + string GenerateAccessToken(string userId, List roles); + } +} diff --git a/Apimanager_backend/Services/IUserService.cs b/Apimanager_backend/Services/IUserService.cs index 0d2212a..1eccfa6 100644 --- a/Apimanager_backend/Services/IUserService.cs +++ b/Apimanager_backend/Services/IUserService.cs @@ -6,79 +6,83 @@ namespace Apimanager_backend.Services { public interface IUserService { - /// - /// 登录用户,根据用户名和密码进行身份验证。 - /// - /// 用户名 - /// 密码 - /// 包含用户信息的 - Task LoginAsync(string username, string password); + /// + /// 发送密码重置邮件到指定邮箱。 + /// + /// 用户注册的邮箱地址 + /// 异步操作 + Task SendResetPasswordEmailAsync(string email); - /// - /// 发送密码重置邮件到指定邮箱。 - /// - /// 用户注册的邮箱地址 - /// 异步操作 - Task SendResetPasswordEmailAsync(string email); + /// + /// 重置用户密码,验证重置令牌的有效性并更新密码。 + /// + /// 用户邮箱地址 + /// 重置密码的令牌 + /// 新的密码 + /// 异步操作 + Task ResetPasswordAsync(string email, string token, string newPassword); - /// - /// 重置用户密码,验证重置令牌的有效性并更新密码。 - /// - /// 用户邮箱地址 - /// 重置密码的令牌 - /// 新的密码 - /// 异步操作 - Task ResetPasswordAsync(string email, string token, string newPassword); + /// + /// 获取用户信息。 + /// + /// 用户ID + /// 包含用户信息的 + Task GetUserAsync(int userId); - /// - /// 获取用户信息。 - /// - /// 用户名 - /// 包含用户信息的 - Task GetUserAsync(string username); + /// + /// 更新用户信息。 + /// + /// 包含更新信息的 + /// 更新后的 + Task UpdateUserAsync(UpdateUserDto user); - /// - /// 更新用户信息。 - /// - /// 包含更新信息的 - /// 更新后的 - Task UpdateUserAsync(UpdateUserDto user); + /// + /// 删除指定的用户。 + /// + /// 要删除的用户名 + /// 异步操作 + Task DeleteUserAsync(string username); - /// - /// 删除指定的用户。 - /// - /// 要删除的用户名 - /// 异步操作 - Task DeleteUserAsync(string username); + /// + /// 创建新用户。 + /// + /// 包含新用户信息的 + /// 创建成功的用户信息 + Task CreateUserAsync(CreateUserDto user); - /// - /// 创建新用户。 - /// - /// 包含新用户信息的 - /// 创建成功的用户信息 - Task CreateUserAsync(CreateUserDto user); + /// + /// 禁用用户,使其无法登录。 + /// + /// 要禁用的用户名 + /// 异步操作 + Task BanUserAsync(string username); - /// - /// 禁用用户,使其无法登录。 - /// - /// 要禁用的用户名 - /// 异步操作 - Task BanUserAsync(string username); + /// + /// 取消禁用用户,恢复登录权限。 + /// + /// 要取消禁用的用户名 + /// 异步操作 + Task UnbanUserAsync(string username); - /// - /// 取消禁用用户,恢复登录权限。 - /// - /// 要取消禁用的用户名 - /// 异步操作 - Task UnbanUserAsync(string username); - - /// - /// 获取分页的用户列表。 - /// - /// 要获取的页码,从1开始 - /// 每页的用户数量 - /// 是否按降序排序 - /// 包含用户信息的 - Task> GetUsersAsync(int page, int pageSize, bool desc); + /// + /// 获取分页的用户列表。 + /// + /// 要获取的页码,从1开始 + /// 每页的用户数量 + /// 是否按降序排序 + /// 包含用户信息的 + Task> GetUsersAsync(int page, int pageSize, bool desc); + /// + /// 检测用户名是否被使用 + /// + /// 用户名 + /// + Task IsUsernameExist(string username); + /// + /// 检测邮箱是否被使用 + /// + /// 邮箱 + /// + Task IsEmailExist(string email); } } diff --git a/Apimanager_backend/Services/RefreshTokenService.cs b/Apimanager_backend/Services/RefreshTokenService.cs new file mode 100644 index 0000000..cef7e9d --- /dev/null +++ b/Apimanager_backend/Services/RefreshTokenService.cs @@ -0,0 +1,74 @@ +using Apimanager_backend.Exceptions; +using StackExchange.Redis; + +namespace Apimanager_backend.Services +{ + public class RefreshTokenService : IRefreshTokenService + { + private readonly IConnectionMultiplexer redis; + private readonly IConfiguration configuration; + private readonly int DbIndex = 0; + public RefreshTokenService(IConnectionMultiplexer redis, IConfiguration configuration) + { + this.redis = redis; + this.configuration = configuration; + } + + public async Task CreateRefereshTokenAsync(string userId) + { + var refreshToken = Guid.NewGuid().ToString(); + var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]); + + // 保存到Redis,设置过期时间 + var db = redis.GetDatabase(DbIndex); + var res = await db.StringSetAsync( userId , refreshToken, TimeSpan.FromDays(expiryDays)); + if (!res) + { + throw new BaseException(1006, "Service unavailable"); + } + return refreshToken; + } + + public async Task DeleterRefreshTokenAsync(string userId) + { + var db = redis.GetDatabase(DbIndex); + bool res = await db.KeyDeleteAsync(userId); + if (!res) + { + throw new BaseException(1006, "Service unavailable"); + } + } + + public async Task UpdateRefreshTokenAsync(string userId) + { + var db = redis.GetDatabase(DbIndex); + var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]); + //获取refresh剩余有效时间 + var time =await db.KeyTimeToLiveAsync(userId); + //判断有效时间是否大于零天小于三天,否则不刷新有效期 + if(time <= TimeSpan.Zero || time >= TimeSpan.FromDays(3)) + { + return; + } + //刷新过期时间 + await db.KeyExpireAsync(userId,TimeSpan.FromDays(expiryDays)); + } + + public async Task ValidateRefreshTokenAsync(string userId,string refreshToken) + { + var db = redis.GetDatabase(DbIndex); + var redisValue = await db.StringGetAsync(userId); + //验证refreshToken是否存在 + if (!redisValue.HasValue) + { + return false; + } + string refreshTokenTrue = redisValue.ToString(); + if (!refreshToken.Equals(refreshTokenTrue)) + { + return false; + } + return true; + } + } +} diff --git a/Apimanager_backend/Services/TokenService.cs b/Apimanager_backend/Services/TokenService.cs new file mode 100644 index 0000000..28215a5 --- /dev/null +++ b/Apimanager_backend/Services/TokenService.cs @@ -0,0 +1,50 @@ + +using Apimanager_backend.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace Apimanager_backend.Services +{ + public class TokenService:ITokenService + { + public readonly IConfiguration configuration; + public TokenService(IConfiguration configuration) + { + this.configuration = configuration; + } + + public string GenerateAccessToken(string userId,List roles) + { + var jwtSettings = configuration.GetSection("JwtSettings"); + + // 创建Claims列表,包含用户名和角色信息 + var claims = new List + { + new Claim("userId", userId), // 使用userId作为唯一标识 + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + //添加用户角色 + foreach(var role in roles) + { + var claim = new Claim(ClaimTypes.Role, role.Role.ToString()); + claims.Add(claim); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Secret"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: jwtSettings["Issuer"], + audience: jwtSettings["Audience"], + claims: claims, + expires: DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings["AccessTokenExpiryMinutes"])), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + } +} diff --git a/Apimanager_backend/Services/UserService.cs b/Apimanager_backend/Services/UserService.cs index c6859a7..ddfb8d1 100644 --- a/Apimanager_backend/Services/UserService.cs +++ b/Apimanager_backend/Services/UserService.cs @@ -34,9 +34,15 @@ namespace Apimanager_backend.Services throw new NotImplementedException(); } - public Task GetUserAsync(string username) + public async Task GetUserAsync(int userId) { - throw new NotImplementedException(); + User? user = await apiContext.Users.SingleOrDefaultAsync(x => x.Id == userId); + //未找到用户 + if (user == null) + { + throw new BaseException(2004, "User not found"); + } + return mapper.Map(user); } public Task> GetUsersAsync(int page, int pageSize, bool desc) @@ -44,26 +50,14 @@ namespace Apimanager_backend.Services throw new NotImplementedException(); } - public async Task LoginAsync(string username, string password) + public async Task IsEmailExist(string email) { - //查找用户 - User? user = await apiContext.Users.SingleOrDefaultAsync(x => - x.Username == username && x.PassHash == password - ); + return await apiContext.Users.AnyAsync(x => x.Email == email); + } - //用户不存在或密码错误都为登录失败 - if(user == null) - { - throw new BaseException(2001, "Invalid username or password"); - } - - //用户被禁用 - if (user.IsBan) - { - throw new BaseException(2002, "User account is disabled"); - } - - return mapper.Map(user); + public async Task IsUsernameExist(string username) + { + return await apiContext.Users.AnyAsync(x => x.Username == username); } public Task ResetPasswordAsync(string email, string token, string newPassword) diff --git a/Apimanager_backend/Tools/RandomCodeHelper.cs b/Apimanager_backend/Tools/RandomCodeHelper.cs new file mode 100644 index 0000000..3392290 --- /dev/null +++ b/Apimanager_backend/Tools/RandomCodeHelper.cs @@ -0,0 +1,15 @@ +namespace Apimanager_backend.Tools +{ + public static class RandomCodeHelper + { + /// + /// 生成随机数字符串 + /// + /// + public static string GetRandomCodeStr() + { + Random random = new Random(); + return random.Next(10000, 99999).ToString(); + } + } +} diff --git a/Apimanager_backend/Tools/StatusCodeHelper.cs b/Apimanager_backend/Tools/StatusCodeHelper.cs new file mode 100644 index 0000000..e0728a6 --- /dev/null +++ b/Apimanager_backend/Tools/StatusCodeHelper.cs @@ -0,0 +1,133 @@ +namespace Apimanager_backend.Tools +{ + public static class StatusCodeHelper + { + public static int GetHttpStatusCode(int customErrorCode) + { + int httpStatusCode; + + switch (customErrorCode) + { + // 通用错误码 + case 1000: // 成功 + httpStatusCode = 200; + break; + case 1001: // 参数错误 + httpStatusCode = 400; + break; + case 1002: // 用户未登录或认证失败 + httpStatusCode = 401; + break; + case 1003: // 无权限访问 + httpStatusCode = 403; + break; + case 1004: // 资源未找到 + httpStatusCode = 404; + break; + case 1005: // 服务器内部错误 + httpStatusCode = 500; + break; + case 1006: // 服务暂时不可用 + httpStatusCode = 503; + break; + + // 用户模块错误码 + case 2000: // 登录成功 + httpStatusCode = 200; + break; + case 2001: // 用户名或密码错误 + httpStatusCode = 401; + break; + case 2002: // 用户账户被禁用 + httpStatusCode = 401; + break; + case 2003: // 用户名已存在 + httpStatusCode = 409; + break; + case 2004: // 用户不存在 + httpStatusCode = 404; + break; + case 2005: // 邮箱已存在 + httpStatusCode = 409; + break; + case 2006: // 用户无权限进行该操作 + httpStatusCode = 403; + break; + case 2007: // 密码重置失败 + httpStatusCode = 400; + break; + case 2008: // 凭证到期或无效 + httpStatusCode = 403; + break; + case 2009: // 刷新令牌到期或无效 + httpStatusCode = 403; + break; + + // API模块错误码 + case 3000: // API调用成功 + httpStatusCode = 200; + break; + case 3001: // API访问受限 + httpStatusCode = 403; + break; + case 3002: // API不存在 + httpStatusCode = 404; + break; + case 3003: // API调用次数超限 + httpStatusCode = 429; + break; + case 3004: // 未购买该API套餐或权限不足 + httpStatusCode = 403; + break; + case 3005: // API调用失败,服务器错误 + httpStatusCode = 500; + break; + + // 套餐与支付模块错误码 + case 4000: // 支付成功 + httpStatusCode = 200; + break; + case 4001: // 支付请求无效 + httpStatusCode = 400; + break; + case 4002: // 支付失败,余额不足 + httpStatusCode = 402; + break; + case 4003: // 订单未找到 + httpStatusCode = 404; + break; + case 4004: // 重复支付或订单冲突 + httpStatusCode = 409; + break; + case 4005: // 支付系统错误 + httpStatusCode = 500; + break; + case 4006: // 退款成功 + httpStatusCode = 200; + break; + + // 日志与系统模块错误码 + case 5000: // 日志查询成功 + httpStatusCode = 200; + break; + case 5001: // 日志记录未找到 + httpStatusCode = 404; + break; + case 5002: // 日志服务异常 + httpStatusCode = 500; + break; + case 5003: // 无权限查看操作日志 + httpStatusCode = 403; + break; + + default: + // 未知错误码,返回 500 + httpStatusCode = 500; + break; + } + + return httpStatusCode; + } + + } +} diff --git a/Apimanager_backend/appsettings.json b/Apimanager_backend/appsettings.json index 8fbdf86..eb6d675 100644 --- a/Apimanager_backend/appsettings.json +++ b/Apimanager_backend/appsettings.json @@ -8,5 +8,22 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "server=192.168.5.200;username=root;password=768788Dyw@;port=3306;database=api_billing_system;SslMode=Preferred;" + }, + "JwtSettings": { + "Secret": "deXtdXode6hv0SI1o6xRw1ALkn0vYsWn", + "Issuer": "qinglan", + "Audience": "qinglan", + "AccessTokenExpiryMinutes": 60, + "RefreshTokenExpiryDays": 7 + }, + "redis": { + "ConnectionString": "192.168.5.200:6379" + }, + "EmailSettings": { + "Server": "smtp.exmail.qq.com", + "Port": "587", + "Ssl": true, + "Username": "nanxun@nxsir.cn", + "Password": "2919054393Dyw" } } diff --git a/ErrorCode.md b/ErrorCode.md index 9c36c73..4b832d6 100644 --- a/ErrorCode.md +++ b/ErrorCode.md @@ -14,16 +14,18 @@ #### 用户模块错误码(2xxx) -| 错误码 | HTTP状态码 | 描述 | Message | -| ------ | ---------- | -------------------- | ---------------------------- | -| 2000 | 200 | 登录成功 | Login successful | -| 2001 | 401 | 用户名或密码错误 | Invalid username or password | -| 2002 | 401 | 用户账户被禁用 | User account is disabled | -| 2003 | 409 | 用户名已存在 | Username already exists | -| 2004 | 404 | 用户不存在 | User not found | -| 2005 | 409 | 邮箱已存在 | Email already exists | -| 2006 | 403 | 用户无权限进行该操作 | Permission denied | -| 2007 | 400 | 密码重置失败 | Password reset failed | +| 错误码 | HTTP状态码 | 描述 | Message | +| ------ | ---------- | -------------------- | ----------------------------- | +| 2000 | 200 | 登录成功 | Login successful | +| 2001 | 401 | 用户名或密码错误 | Invalid username or password | +| 2002 | 401 | 用户账户被禁用 | User account is disabled | +| 2003 | 409 | 用户名已存在 | Username already exists | +| 2004 | 404 | 用户不存在 | User not found | +| 2005 | 409 | 邮箱已存在 | Email already exists | +| 2006 | 403 | 用户无权限进行该操作 | Permission denied | +| 2007 | 400 | 密码重置失败 | Password reset failed | +| 2008 | 403 | 凭证到期或无效 | Token expires or is invalid | +| 2009 | 403 | 刷新令牌到期或无效 | Refresh expires or is invalid | #### API模块错误码(3xxx) @@ -55,4 +57,6 @@ | 5000 | 200 | 日志查询成功 | Log retrieval successful | | 5001 | 404 | 日志记录未找到 | Log record not found | | 5002 | 500 | 日志服务异常 | Log service error | -| 5003 | 403 | 无权限查看操作日志 | No permission to view logs | \ No newline at end of file +| 5003 | 403 | 无权限查看操作日志 | No permission to view logs | +| 5004 | 500 | 邮件发送错误 | Email send error | +| 5005 | 400 | 验证码错误 | ValidateCode Error | \ No newline at end of file