diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e41b7fb7..2a81627fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.2.2 +- Feature: 新增登录功能 ## 3.2.1 - Feature: 镜像新增linux/386和linux/arm/v7架构 ## 3.2.0 diff --git a/common.props b/common.props index 004af5dd2..10a06d561 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ Ray - 3.2.1 + 3.2.2 $(NoWarn);CS1591;CS0436 diff --git a/src/Ray.BiliBiliTool.Domain/User.cs b/src/Ray.BiliBiliTool.Domain/User.cs new file mode 100644 index 000000000..6b1545d28 --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/User.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ray.BiliBiliTool.Domain; + +[Table("bili_user")] +public class User +{ + [Key] + public long Id { get; set; } + public required string Username { get; set; } + public required string PasswordHash { get; set; } + public required string Salt { get; set; } + public List Roles { get; set; } = []; +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs index 6cb0f8299..1e63bd848 100644 --- a/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs @@ -11,6 +11,7 @@ public class BiliDbContext(IConfiguration config) : DbContext { public DbSet ExecutionLogs { get; set; } public DbSet BiliLogs { get; set; } + public DbSet Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -69,6 +70,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasIndex(x => x.FireInstanceIdComputed) // 在计算列上创建索引 .HasDatabaseName("IX_Logs_FireInstanceIdComputed"); }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Username).IsRequired().HasMaxLength(50); + entity.Property(e => e.PasswordHash).IsRequired(); + entity.Property(e => e.Salt).IsRequired(); + entity + .Property(e => e.Roles) + .HasConversion( + v => string.Join(',', v), + v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() + ); + + entity.HasIndex(e => e.Username).IsUnique(); + }); } private void AddSqliteDateTimeOffsetSupport(ModelBuilder builder) diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs new file mode 100644 index 000000000..61fbea14c --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Infrastructure.EF; + +public class DbInitializer(BiliDbContext context) +{ + private const string DefaultUserName = "admin"; + private const string DefaultPassword = "BiliTool@2233"; + + public async Task InitializeAsync() + { + await context.Database.MigrateAsync(); + + await InitUserAsync(); + } + + private async Task InitUserAsync() + { + if (await context.Users.AnyAsync()) + { + return; + } + + var (hash, salt) = PasswordHelper.HashPassword(DefaultPassword); + var adminUser = new User + { + Username = DefaultUserName, + PasswordHash = hash, + Salt = salt, + Roles = ["Administrator"], + }; + + context.Users.Add(adminUser); + await context.SaveChangesAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs index 896444ae6..a60c390b0 100644 --- a/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs @@ -7,6 +7,7 @@ public static class ServiceCollectionExtension public static IServiceCollection AddEF(this IServiceCollection services) { services.AddDbContextFactory(); + services.AddScoped(); return services; } } diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs new file mode 100644 index 000000000..c818dee3d --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs @@ -0,0 +1,722 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ray.BiliBiliTool.Infrastructure.EF; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + [DbContext(typeof(BiliDbContext))] + [Migration("20250615100041_AddUser")] + partial class AddUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("BLOB_DATA"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("CALENDAR"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_CALENDARS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("CRON_EXPRESSION"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_CRON_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("ENTRY_ID"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("FIRED_TIME"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("SCHED_TIME"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("STATE"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME"); + + b.HasIndex("JobGroup") + .HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP"); + + b.HasIndex("JobName") + .HasDatabaseName("IDX_QRTZ_FT_JOB_NAME"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP"); + + b.HasIndex("TriggerName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP"); + + b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("IS_DURABLE"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("IS_UPDATE_DATA"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_CLASS_NAME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY"); + + b.ToTable("QRTZ_JOB_DETAILS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("LOCK_NAME"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_LOCKS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("CHECKIN_INTERVAL"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("LAST_CHECKIN_TIME"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_SCHEDULER_STATE", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("INT_PROP_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("INT_PROP_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("STR_PROP_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("STR_PROP_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("STR_PROP_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("REPEAT_COUNT"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("REPEAT_INTERVAL"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("TIMES_TRIGGERED"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("END_TIME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("MISFIRE_INSTR"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("NEXT_FIRE_TIME"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("PREV_FIRE_TIME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("START_TIME"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_STATE"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_TYPE"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME"); + + b.HasIndex("TriggerState") + .HasDatabaseName("IDX_QRTZ_T_STATE"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("IDX_QRTZ_T_NFT_ST"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.BiliLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("TEXT") + .HasColumnName("exception"); + + b.Property("FireInstanceIdComputed") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasColumnName("fireInstanceIdComputed") + .HasComputedColumnSql("json_extract(Properties, '$.FireInstanceId')", false); + + b.Property("Level") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("level"); + + b.Property("Properties") + .HasColumnType("TEXT") + .HasColumnName("properties"); + + b.Property("RenderedMessage") + .HasColumnType("TEXT") + .HasColumnName("renderedMessage"); + + b.Property("Timestamp") + .HasColumnType("TEXT") + .HasColumnName("timeStamp"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceIdComputed") + .HasDatabaseName("IX_Logs_FireInstanceIdComputed"); + + b.ToTable("bili_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.Property("LogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAddedUtc") + .HasColumnType("INTEGER"); + + b.Property("ErrorMessage") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("FireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("IsException") + .HasColumnType("INTEGER"); + + b.Property("IsSuccess") + .HasColumnType("INTEGER"); + + b.Property("IsVetoed") + .HasColumnType("INTEGER"); + + b.Property("JobGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobRunTime") + .HasColumnType("TEXT"); + + b.Property("LogType") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.Property("Result") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("ReturnCode") + .HasMaxLength(28) + .HasColumnType("TEXT"); + + b.Property("RunInstanceId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ScheduleFireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("TriggerGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TriggerName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("LogId"); + + b.HasIndex("RunInstanceId") + .IsUnique(); + + b.HasIndex("DateAddedUtc", "LogType"); + + b.HasIndex("TriggerName", "TriggerGroup", "JobName", "JobGroup", "DateAddedUtc"); + + b.ToTable("bili_execution_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("bili_User"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.OwnsOne("Ray.BiliBiliTool.Domain.ExecutionLogDetail", "ExecutionLogDetail", b1 => + { + b1.Property("LogId") + .HasColumnType("INTEGER"); + + b1.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b1.Property("ErrorHelpLink") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b1.Property("ErrorStackTrace") + .HasColumnType("TEXT"); + + b1.Property("ExecutionDetails") + .HasColumnType("TEXT"); + + b1.HasKey("LogId"); + + b1.ToTable("bili_execution_log_details", (string)null); + + b1.WithOwner() + .HasForeignKey("LogId"); + }); + + b.Navigation("ExecutionLogDetail"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs new file mode 100644 index 000000000..7d26e5e68 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + /// + public partial class AddUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "bili_user", + columns: table => new + { + Id = table + .Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Username = table.Column(type: "TEXT", maxLength: 50, nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: false), + Salt = table.Column(type: "TEXT", nullable: false), + Roles = table.Column(type: "TEXT", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_bili_user", x => x.Id); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_bili_user_Username", + table: "bili_user", + column: "Username", + unique: true + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "bili_user"); + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs index 7a5a44475..ccb478f34 100644 --- a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs @@ -580,6 +580,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("bili_execution_logs"); }); + modelBuilder.Entity("Ray.BiliBiliTool.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("bili_user"); + }); + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => { b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/README.md b/src/Ray.BiliBiliTool.Infrastructure.EF/README.md new file mode 100644 index 000000000..505a5a5ea --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/README.md @@ -0,0 +1,12 @@ +## Add new migration + +```bash +cd ./src/Ray.BiliBiliTool.Web +dotnet ef migrations add AddUser --project ../Ray.BiliBiliTool.Infrastructure.EF +``` + +## Remove migration + +```bash +dotnet ef migrations remove --project ../Ray.BiliBiliTool.Infrastructure.EF +``` diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj b/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj index d551ed75b..335bdf4eb 100644 --- a/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj @@ -21,5 +21,6 @@ + diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs new file mode 100644 index 000000000..c052e3753 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs @@ -0,0 +1,34 @@ +using System.Security.Cryptography; + +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +public class PasswordHelper +{ + public static (string hash, string salt) HashPassword(string password) + { + byte[] saltBytes = RandomNumberGenerator.GetBytes(16); + string salt = Convert.ToBase64String(saltBytes); + string hash = ComputeHash(password, salt); + return (hash, salt); + } + + public static bool VerifyPassword(string password, string salt, string hash) + { + string computedHash = ComputeHash(password, salt); + return computedHash == hash; + } + + private static string ComputeHash(string password, string salt) + { + byte[] saltBytes = Convert.FromBase64String(salt); + using var pbkdf2 = new Rfc2898DeriveBytes( + password, + saltBytes, + 100_000, + HashAlgorithmName.SHA256 + ); + byte[] hashBytes = pbkdf2.GetBytes(32); + + return Convert.ToBase64String(hashBytes); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs b/src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs new file mode 100644 index 000000000..e3ec068ed --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs @@ -0,0 +1,26 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Ray.BiliBiliTool.Web.Auth; + +public class CustomAuthStateProvider(IHttpContextAccessor httpContextAccessor) + : AuthenticationStateProvider +{ + public override Task GetAuthenticationStateAsync() + { + var identity = new ClaimsIdentity(); + var user = httpContextAccessor.HttpContext?.User; + + if (user?.Identity?.IsAuthenticated == true) + { + identity = new ClaimsIdentity(user.Claims, "Cookies"); + } + + return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity))); + } + + public void NotifyAuthenticationStateChanged() + { + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor b/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor index def638b5a..849bba5e6 100644 --- a/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor +++ b/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor @@ -1,4 +1,5 @@ -@inherits LayoutComponentBase +@using Microsoft.AspNetCore.Components.Authorization +@inherits LayoutComponentBase