feat:首次启动创建默认账户
This commit is contained in:
@@ -22,6 +22,7 @@ public static class CloudSyncEndpointExtensions
|
||||
auth.MapPost("/bootstrap", BootstrapAdminAsync).AllowAnonymous();
|
||||
auth.MapPost("/login", LoginAsync).AllowAnonymous();
|
||||
auth.MapPost("/step-up", StepUpAsync).RequireAuthorization();
|
||||
auth.MapPost("/change-password", ChangePasswordAsync).RequireAuthorization();
|
||||
|
||||
var tasks = app.MapGroup("/tasks").WithTags("CloudSync - Tasks");
|
||||
tasks.MapGet("/", GetTasksAsync).RequireAuthorization("tasks:read");
|
||||
@@ -58,19 +59,19 @@ public static class CloudSyncEndpointExtensions
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName))
|
||||
{
|
||||
return CloudApiErrors.BadRequest("UserName and Password are required.");
|
||||
return CloudApiErrors.BadRequest("UserName is required.");
|
||||
}
|
||||
|
||||
var (ip, ua) = GetClientInfo(httpContext);
|
||||
var ok = await authService.BootstrapAdminAsync(request.UserName, request.Password, ip, ua, cancellationToken);
|
||||
if (!ok)
|
||||
var result = await authService.BootstrapAdminAsync(request.UserName, request.Password, ip, ua, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return CloudApiErrors.Forbidden("Bootstrap is not allowed (already initialized or invalid input).");
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
return Results.Json(result);
|
||||
}
|
||||
|
||||
private static async Task<IResult> LoginAsync(
|
||||
@@ -121,6 +122,38 @@ public static class CloudSyncEndpointExtensions
|
||||
return Results.Json(new StepUpResponse { StepUpExpiresAtUtc = expiresAt.Value });
|
||||
}
|
||||
|
||||
private static async Task<IResult> ChangePasswordAsync(
|
||||
ChangePasswordRequest request,
|
||||
CloudAuthService authService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sessionId = httpContext.User.GetSessionId();
|
||||
if (sessionId == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized();
|
||||
}
|
||||
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.CurrentPassword) || string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
{
|
||||
return CloudApiErrors.BadRequest("CurrentPassword and NewPassword are required.");
|
||||
}
|
||||
|
||||
if (request.NewPassword.Length < 8)
|
||||
{
|
||||
return CloudApiErrors.BadRequest("NewPassword must be at least 8 characters.");
|
||||
}
|
||||
|
||||
var (ip, ua) = GetClientInfo(httpContext);
|
||||
var ok = await authService.ChangePasswordAsync(sessionId.Value, request.CurrentPassword, request.NewPassword, ip, ua, cancellationToken);
|
||||
if (!ok)
|
||||
{
|
||||
return CloudApiErrors.BadRequest("Invalid current password or session expired.");
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetTasksAsync(
|
||||
CloudTaskSyncService taskService,
|
||||
HttpContext httpContext,
|
||||
|
||||
@@ -45,6 +45,11 @@ public class LoginResponse
|
||||
/// 用户权限列表。
|
||||
/// </summary>
|
||||
public List<string> Permissions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否必须修改密码。
|
||||
/// </summary>
|
||||
public bool MustChangePassword { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,9 +63,41 @@ public class BootstrapAdminRequest
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码。
|
||||
/// 密码(可选,若不提供则自动生成随机密码)。
|
||||
/// </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化管理员账号响应。
|
||||
/// </summary>
|
||||
public class BootstrapAdminResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户名。
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 生成的临时密码(仅在首次创建时返回)。
|
||||
/// </summary>
|
||||
public string GeneratedPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改密码请求。
|
||||
/// </summary>
|
||||
public class ChangePasswordRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前密码。
|
||||
/// </summary>
|
||||
public string CurrentPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 新密码。
|
||||
/// </summary>
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,6 +15,7 @@ public class CloudAuthService
|
||||
private readonly TodoDbContext _dbContext;
|
||||
private readonly IPasswordHasher<UserEntity> _passwordHasher;
|
||||
private readonly IRolePermissionMapper _rolePermissionMapper;
|
||||
private static readonly Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="CloudAuthService"/>。
|
||||
@@ -32,6 +33,34 @@ public class CloudAuthService
|
||||
_rolePermissionMapper = rolePermissionMapper;
|
||||
}
|
||||
|
||||
private static string GenerateRandomPassword(int length = 16)
|
||||
{
|
||||
const string uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const string lowercase = "abcdefghijklmnopqrstuvwxyz";
|
||||
const string digits = "0123456789";
|
||||
const string special = "!@#$%^&*";
|
||||
var allChars = uppercase + lowercase + digits + special;
|
||||
|
||||
var password = new char[length];
|
||||
password[0] = uppercase[_random.Next(uppercase.Length)];
|
||||
password[1] = lowercase[_random.Next(lowercase.Length)];
|
||||
password[2] = digits[_random.Next(digits.Length)];
|
||||
password[3] = special[_random.Next(special.Length)];
|
||||
|
||||
for (int i = 4; i < length; i++)
|
||||
{
|
||||
password[i] = allChars[_random.Next(allChars.Length)];
|
||||
}
|
||||
|
||||
for (int i = password.Length - 1; i > 0; i--)
|
||||
{
|
||||
int j = _random.Next(i + 1);
|
||||
(password[i], password[j]) = (password[j], password[i]);
|
||||
}
|
||||
|
||||
return new string(password);
|
||||
}
|
||||
|
||||
private async Task LogAsync(
|
||||
string eventType,
|
||||
string description,
|
||||
@@ -62,17 +91,17 @@ public class CloudAuthService
|
||||
/// 初始化系统管理员账号(仅在系统尚无云用户时可用)。
|
||||
/// </summary>
|
||||
/// <param name="userName">用户名。</param>
|
||||
/// <param name="password">密码。</param>
|
||||
/// <param name="password">密码(可选,若不提供则自动生成)。</param>
|
||||
/// <param name="clientIp">客户端 IP。</param>
|
||||
/// <param name="userAgent">User-Agent。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>是否初始化成功。</returns>
|
||||
public async Task<bool> BootstrapAdminAsync(string userName, string password, string? clientIp, string? userAgent, CancellationToken cancellationToken)
|
||||
/// <returns>初始化结果,包含生成的密码(若自动生成)。</returns>
|
||||
public async Task<BootstrapAdminResponse?> BootstrapAdminAsync(string userName, string? password, string? clientIp, string? userAgent, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedUserName = (userName ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedUserName) || string.IsNullOrWhiteSpace(password))
|
||||
if (string.IsNullOrWhiteSpace(normalizedUserName))
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var hasAnyCloudUser = await _dbContext.Users
|
||||
@@ -82,7 +111,7 @@ public class CloudAuthService
|
||||
if (hasAnyCloudUser)
|
||||
{
|
||||
await LogAsync("BootstrapFailed", "System already initialized.", userName: normalizedUserName, isSuccess: false, clientIp: clientIp, userAgent: userAgent);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var exists = await _dbContext.Users
|
||||
@@ -91,9 +120,10 @@ public class CloudAuthService
|
||||
|
||||
if (exists)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var generatedPassword = password ?? GenerateRandomPassword();
|
||||
var now = DateTime.UtcNow;
|
||||
var user = new UserEntity
|
||||
{
|
||||
@@ -101,17 +131,22 @@ public class CloudAuthService
|
||||
UserName = normalizedUserName,
|
||||
Role = "admin",
|
||||
CreatedAtUtc = now,
|
||||
UpdatedAtUtc = now
|
||||
UpdatedAtUtc = now,
|
||||
MustChangePassword = true
|
||||
};
|
||||
|
||||
user.PasswordHash = _passwordHasher.HashPassword(user, password);
|
||||
user.PasswordHash = _passwordHasher.HashPassword(user, generatedPassword);
|
||||
|
||||
_dbContext.Users.Add(user);
|
||||
_dbContext.SecurityPolicies.Add(new SecurityPolicyEntity { Id = Guid.NewGuid(), UserId = user.Id, AllowPersist = true });
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await LogAsync("BootstrapSuccess", "System administrator created.", user.Id, user.UserName, clientIp: clientIp, userAgent: userAgent);
|
||||
return true;
|
||||
return new BootstrapAdminResponse
|
||||
{
|
||||
UserName = normalizedUserName,
|
||||
GeneratedPassword = generatedPassword
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -176,7 +211,8 @@ public class CloudAuthService
|
||||
ExpiresAtUtc = expiresAt,
|
||||
UserId = user.Id,
|
||||
Role = user.Role,
|
||||
Permissions = _rolePermissionMapper.GetPermissions(user.Role).ToList()
|
||||
Permissions = _rolePermissionMapper.GetPermissions(user.Role).ToList(),
|
||||
MustChangePassword = user.MustChangePassword
|
||||
};
|
||||
}
|
||||
|
||||
@@ -229,5 +265,57 @@ public class CloudAuthService
|
||||
|
||||
return stepUpExpiresAt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改用户密码。
|
||||
/// </summary>
|
||||
/// <param name="sessionId">会话 ID。</param>
|
||||
/// <param name="currentPassword">当前密码。</param>
|
||||
/// <param name="newPassword">新密码。</param>
|
||||
/// <param name="clientIp">客户端 IP。</param>
|
||||
/// <param name="userAgent">User-Agent。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>是否修改成功。</returns>
|
||||
public async Task<bool> ChangePasswordAsync(Guid sessionId, string currentPassword, string newPassword, string? clientIp, string? userAgent, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentPassword) || string.IsNullOrWhiteSpace(newPassword))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newPassword.Length < 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var session = await _dbContext.UserSessions.FirstOrDefaultAsync(s => s.Id == sessionId, cancellationToken);
|
||||
if (session == null || session.ExpiresAtUtc <= now)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == session.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var verify = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, currentPassword);
|
||||
if (verify == PasswordVerificationResult.Failed)
|
||||
{
|
||||
await LogAsync("ChangePasswordFailed", "Invalid current password.", user.Id, user.UserName, isSuccess: false, clientIp: clientIp, userAgent: userAgent);
|
||||
return false;
|
||||
}
|
||||
|
||||
user.PasswordHash = _passwordHasher.HashPassword(user, newPassword);
|
||||
user.MustChangePassword = false;
|
||||
user.UpdatedAtUtc = DateTime.UtcNow;
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await LogAsync("ChangePasswordSuccess", "Password changed successfully.", user.Id, user.UserName, clientIp: clientIp, userAgent: userAgent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hua.Todo.Application.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
[DbContext(typeof(TodoDbContext))]
|
||||
[Migration("20260424164713_AddPasswordSaltToUsers")]
|
||||
partial class AddPasswordSaltToUsers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientIp")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TimestampUtc")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TimestampUtc");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AuditLogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("AllowPersist")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("AllowSync")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsTrustedDeviceOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int>("SecondFactorExpiryMinutes")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30);
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SecurityPolicies", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CreatedAt")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int?>("ParentTaskId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedAt")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentTaskId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedAtUtc")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExpiresAtUtc")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StepUpExpiresAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserSessions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
||||
.WithMany("SubTasks")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentTask");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("SubTasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPasswordSaltToUsers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PasswordSalt",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PasswordSalt",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+276
@@ -0,0 +1,276 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hua.Todo.Application.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
[DbContext(typeof(TodoDbContext))]
|
||||
[Migration("20260510171230_AddMustChangePasswordToUsers")]
|
||||
partial class AddMustChangePasswordToUsers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientIp")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TimestampUtc")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TimestampUtc");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AuditLogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("AllowPersist")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("AllowSync")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsTrustedDeviceOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int>("SecondFactorExpiryMinutes")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30);
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SecurityPolicies", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CreatedAt")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int?>("ParentTaskId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedAt")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentTaskId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("MustChangePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedAtUtc")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExpiresAtUtc")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StepUpExpiresAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserSessions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
||||
.WithMany("SubTasks")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentTask");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
||||
{
|
||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("SubTasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hua.Todo.Application.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMustChangePasswordToUsers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "MustChangePassword",
|
||||
table: "Users",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MustChangePassword",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,10 +159,17 @@ namespace Hua.Todo.Application.Migrations
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("MustChangePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
|
||||
@@ -20,6 +20,11 @@ public class UserEntity
|
||||
/// </summary>
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码盐值(用于增强安全性,防止撞库)。
|
||||
/// </summary>
|
||||
public string PasswordSalt { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户角色(用于 RBAC 权限映射)。
|
||||
/// </summary>
|
||||
@@ -39,5 +44,10 @@ public class UserEntity
|
||||
/// 用户任务集合。
|
||||
/// </summary>
|
||||
public List<TaskEntity> Tasks { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否必须修改密码(首次登录或管理员重置后为 true)。
|
||||
/// </summary>
|
||||
public bool MustChangePassword { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ builder.Services.AddCors(options =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
.AllowAnyHeader()
|
||||
.SetPreflightMaxAge(TimeSpan.FromMinutes(30));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,16 +38,54 @@ using (var scope = app.Services.CreateScope())
|
||||
var dbContext = services.GetRequiredService<Hua.Todo.Application.Data.TodoDbContext>();
|
||||
dbContext.Database.Migrate();
|
||||
|
||||
// Seed default admin from configuration
|
||||
var config = services.GetRequiredService<IConfiguration>();
|
||||
var authService = services.GetRequiredService<CloudAuthService>();
|
||||
|
||||
var adminConfig = config.GetSection("DefaultAdmin");
|
||||
var adminUserName = adminConfig["UserName"];
|
||||
var adminPassword = adminConfig["Password"];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(adminUserName) && !string.IsNullOrWhiteSpace(adminPassword))
|
||||
if (!string.IsNullOrWhiteSpace(adminUserName))
|
||||
{
|
||||
var authService = services.GetRequiredService<CloudAuthService>();
|
||||
await authService.BootstrapAdminAsync(adminUserName, adminPassword, "system", "seeding", CancellationToken.None);
|
||||
var result = await authService.BootstrapAdminAsync(adminUserName, adminPassword, "system", "seeding", CancellationToken.None);
|
||||
if (result != null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine("首次启动:已创建默认管理员账号");
|
||||
Console.WriteLine($"用户名: {result.UserName}");
|
||||
if (!string.IsNullOrEmpty(adminPassword))
|
||||
{
|
||||
Console.WriteLine($"密码: {adminPassword}(已在配置中设置)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"临时密码: {result.GeneratedPassword}");
|
||||
Console.WriteLine("请立即登录并修改密码!");
|
||||
}
|
||||
Console.WriteLine("========================================");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasAnyCloudUser = await dbContext.Users.AsNoTracking().AnyAsync(u => u.Role != "local");
|
||||
if (!hasAnyCloudUser)
|
||||
{
|
||||
var defaultUserName = "admin";
|
||||
var result = await authService.BootstrapAdminAsync(defaultUserName, null, "system", "auto-seeding", CancellationToken.None);
|
||||
if (result != null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine("首次启动:已自动创建管理员账号");
|
||||
Console.WriteLine($"用户名: {result.UserName}");
|
||||
Console.WriteLine($"临时密码: {result.GeneratedPassword}");
|
||||
Console.WriteLine("请立即登录并修改密码!");
|
||||
Console.WriteLine("========================================");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +95,9 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors("AllowAll");
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"AllowedHosts": "*",
|
||||
"DefaultAdmin": {
|
||||
"UserName": "admin",
|
||||
"Password": "ChangeMe@123"
|
||||
"Password": "123456"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user