feat(console): complete migration of User, Tenant, and Role management APIs
This commit is contained in:
parent
02b446cfa7
commit
c998686ab6
@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication;
|
|||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Fengling.AuthService.Controllers;
|
namespace Fengling.AuthService.Controllers;
|
||||||
|
|
||||||
@ -13,15 +14,18 @@ public class AccountController : Controller
|
|||||||
{
|
{
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ApplicationDbContext _dbContext;
|
||||||
private readonly ILogger<AccountController> _logger;
|
private readonly ILogger<AccountController> _logger;
|
||||||
|
|
||||||
public AccountController(
|
public AccountController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
SignInManager<ApplicationUser> signInManager,
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
ILogger<AccountController> logger)
|
ILogger<AccountController> logger)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
|
_dbContext = dbContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,12 +83,22 @@ public class AccountController : Controller
|
|||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultTenant = await _dbContext.Tenants
|
||||||
|
.FirstOrDefaultAsync(t => t.TenantId == "default");
|
||||||
|
|
||||||
|
if (defaultTenant == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, "系统配置错误:未找到默认租户");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
var user = new ApplicationUser
|
var user = new ApplicationUser
|
||||||
{
|
{
|
||||||
UserName = model.Username,
|
UserName = model.Username,
|
||||||
Email = model.Email,
|
Email = model.Email,
|
||||||
NormalizedUserName = model.Username.ToUpper(),
|
NormalizedUserName = model.Username.ToUpper(),
|
||||||
NormalizedEmail = model.Email.ToUpper()
|
NormalizedEmail = model.Email.ToUpper(),
|
||||||
|
TenantInfo = new TenantInfo(defaultTenant.Id, defaultTenant.TenantId, defaultTenant.Name)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _userManager.CreateAsync(user, model.Password);
|
var result = await _userManager.CreateAsync(user, model.Password);
|
||||||
|
|||||||
@ -3,7 +3,9 @@ using Fengling.AuthService.Models;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OpenIddict.Abstractions;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace Fengling.AuthService.Controllers;
|
namespace Fengling.AuthService.Controllers;
|
||||||
|
|
||||||
@ -13,13 +15,16 @@ namespace Fengling.AuthService.Controllers;
|
|||||||
public class OAuthClientsController : ControllerBase
|
public class OAuthClientsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly IOpenIddictApplicationManager _applicationManager;
|
||||||
private readonly ILogger<OAuthClientsController> _logger;
|
private readonly ILogger<OAuthClientsController> _logger;
|
||||||
|
|
||||||
public OAuthClientsController(
|
public OAuthClientsController(
|
||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
|
IOpenIddictApplicationManager applicationManager,
|
||||||
ILogger<OAuthClientsController> logger)
|
ILogger<OAuthClientsController> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_applicationManager = applicationManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +74,7 @@ public class OAuthClientsController : ControllerBase
|
|||||||
status = c.Status,
|
status = c.Status,
|
||||||
description = c.Description,
|
description = c.Description,
|
||||||
createdAt = c.CreatedAt,
|
createdAt = c.CreatedAt,
|
||||||
|
updatedAt = c.UpdatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
@ -80,6 +86,46 @@ public class OAuthClientsController : ControllerBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("options")]
|
||||||
|
public ActionResult<object> GetClientOptions()
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
clientTypes = new[]
|
||||||
|
{
|
||||||
|
new { value = "public", label = "Public (SPA, Mobile App)" },
|
||||||
|
new { value = "confidential", label = "Confidential (Server-side)" }
|
||||||
|
},
|
||||||
|
consentTypes = new[]
|
||||||
|
{
|
||||||
|
new { value = "implicit", label = "Implicit" },
|
||||||
|
new { value = "explicit", label = "Explicit" },
|
||||||
|
new { value = "system", label = "System (Pre-authorized)" }
|
||||||
|
},
|
||||||
|
grantTypes = new[]
|
||||||
|
{
|
||||||
|
new { value = "authorization_code", label = "Authorization Code" },
|
||||||
|
new { value = "client_credentials", label = "Client Credentials" },
|
||||||
|
new { value = "refresh_token", label = "Refresh Token" },
|
||||||
|
new { value = "password", label = "Resource Owner Password Credentials" }
|
||||||
|
},
|
||||||
|
scopes = new[]
|
||||||
|
{
|
||||||
|
new { value = "openid", label = "OpenID Connect" },
|
||||||
|
new { value = "profile", label = "Profile" },
|
||||||
|
new { value = "email", label = "Email" },
|
||||||
|
new { value = "api", label = "API Access" },
|
||||||
|
new { value = "offline_access", label = "Offline Access" }
|
||||||
|
},
|
||||||
|
statuses = new[]
|
||||||
|
{
|
||||||
|
new { value = "active", label = "Active" },
|
||||||
|
new { value = "inactive", label = "Inactive" },
|
||||||
|
new { value = "suspended", label = "Suspended" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<ActionResult<OAuthApplication>> GetClient(long id)
|
public async Task<ActionResult<OAuthApplication>> GetClient(long id)
|
||||||
{
|
{
|
||||||
@ -103,45 +149,62 @@ public class OAuthClientsController : ControllerBase
|
|||||||
status = client.Status,
|
status = client.Status,
|
||||||
description = client.Description,
|
description = client.Description,
|
||||||
createdAt = client.CreatedAt,
|
createdAt = client.CreatedAt,
|
||||||
});
|
updatedAt = client.UpdatedAt,
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/secret")]
|
|
||||||
public async Task<ActionResult<object>> GetClientSecret(long id)
|
|
||||||
{
|
|
||||||
var client = await _context.OAuthApplications.FindAsync(id);
|
|
||||||
if (client == null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
clientId = client.ClientId,
|
|
||||||
clientSecret = client.ClientSecret,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<OAuthApplication>> CreateClient(CreateOAuthClientDto dto)
|
public async Task<ActionResult<object>> CreateClient([FromBody] CreateOAuthClientDto dto)
|
||||||
{
|
{
|
||||||
if (await _context.OAuthApplications.AnyAsync(c => c.ClientId == dto.ClientId))
|
var existingClient = await _context.OAuthApplications.FirstOrDefaultAsync(c => c.ClientId == dto.ClientId);
|
||||||
|
if (existingClient != null)
|
||||||
{
|
{
|
||||||
return BadRequest(new { message = "Client ID 已存在" });
|
return BadRequest(new { message = "Client ID 已存在", clientId = dto.ClientId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clientSecret = string.IsNullOrEmpty(dto.ClientSecret) ? GenerateSecureSecret() : dto.ClientSecret;
|
||||||
|
|
||||||
|
var descriptor = new OpenIddictApplicationDescriptor
|
||||||
|
{
|
||||||
|
ClientId = dto.ClientId,
|
||||||
|
ClientSecret = clientSecret,
|
||||||
|
DisplayName = dto.DisplayName
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var uri in (dto.RedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
||||||
|
{
|
||||||
|
descriptor.RedirectUris.Add(new Uri(uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var uri in (dto.PostLogoutRedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
||||||
|
{
|
||||||
|
descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var grantType in dto.GrantTypes ?? Array.Empty<string>())
|
||||||
|
{
|
||||||
|
descriptor.Permissions.Add(grantType);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var scope in dto.Scopes ?? Array.Empty<string>())
|
||||||
|
{
|
||||||
|
descriptor.Permissions.Add(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _applicationManager.CreateAsync(descriptor);
|
||||||
|
|
||||||
var client = new OAuthApplication
|
var client = new OAuthApplication
|
||||||
{
|
{
|
||||||
ClientId = dto.ClientId,
|
ClientId = dto.ClientId,
|
||||||
ClientSecret = dto.ClientSecret,
|
ClientSecret = clientSecret,
|
||||||
DisplayName = dto.DisplayName,
|
DisplayName = dto.DisplayName,
|
||||||
RedirectUris = dto.RedirectUris,
|
RedirectUris = dto.RedirectUris ?? Array.Empty<string>(),
|
||||||
PostLogoutRedirectUris = dto.PostLogoutRedirectUris,
|
PostLogoutRedirectUris = dto.PostLogoutRedirectUris ?? Array.Empty<string>(),
|
||||||
Scopes = dto.Scopes,
|
Scopes = dto.Scopes ?? new[] { "openid", "profile", "email", "api" },
|
||||||
GrantTypes = dto.GrantTypes,
|
GrantTypes = dto.GrantTypes ?? new[] { "authorization_code", "refresh_token" },
|
||||||
ClientType = dto.ClientType,
|
ClientType = dto.ClientType ?? "confidential",
|
||||||
ConsentType = dto.ConsentType,
|
ConsentType = dto.ConsentType ?? "explicit",
|
||||||
Status = dto.Status,
|
Status = dto.Status ?? "active",
|
||||||
Description = dto.Description,
|
Description = dto.Description,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
@ -151,11 +214,19 @@ public class OAuthClientsController : ControllerBase
|
|||||||
|
|
||||||
await CreateAuditLog("oauth", "create", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(dto));
|
await CreateAuditLog("oauth", "create", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(dto));
|
||||||
|
|
||||||
return CreatedAtAction(nameof(GetClient), new { id = client.Id }, client);
|
return CreatedAtAction(nameof(GetClient), new { id = client.Id }, new
|
||||||
|
{
|
||||||
|
client.Id,
|
||||||
|
client.ClientId,
|
||||||
|
client.ClientSecret,
|
||||||
|
client.DisplayName,
|
||||||
|
client.Status,
|
||||||
|
client.CreatedAt
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPost("{id}/generate-secret")]
|
||||||
public async Task<IActionResult> UpdateClient(long id, UpdateOAuthClientDto dto)
|
public async Task<ActionResult<object>> GenerateSecret(long id)
|
||||||
{
|
{
|
||||||
var client = await _context.OAuthApplications.FindAsync(id);
|
var client = await _context.OAuthApplications.FindAsync(id);
|
||||||
if (client == null)
|
if (client == null)
|
||||||
@ -163,21 +234,123 @@ public class OAuthClientsController : ControllerBase
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldValue = SerializeToJson(client);
|
var newSecret = GenerateSecureSecret();
|
||||||
|
client.ClientSecret = newSecret;
|
||||||
|
client.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
client.DisplayName = dto.DisplayName;
|
var application = await _applicationManager.FindByClientIdAsync(client.ClientId);
|
||||||
client.RedirectUris = dto.RedirectUris;
|
if (application != null)
|
||||||
client.PostLogoutRedirectUris = dto.PostLogoutRedirectUris;
|
{
|
||||||
client.Scopes = dto.Scopes;
|
var descriptor = new OpenIddictApplicationDescriptor
|
||||||
client.GrantTypes = dto.GrantTypes;
|
{
|
||||||
client.ClientType = dto.ClientType;
|
ClientId = client.ClientId,
|
||||||
client.ConsentType = dto.ConsentType;
|
ClientSecret = newSecret,
|
||||||
client.Status = dto.Status;
|
DisplayName = client.DisplayName
|
||||||
client.Description = dto.Description;
|
};
|
||||||
|
|
||||||
|
await _applicationManager.UpdateAsync(application, descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
await CreateAuditLog("oauth", "update", "OAuthClient", client.Id, client.DisplayName, oldValue, SerializeToJson(client));
|
await CreateAuditLog("oauth", "generate_secret", "OAuthClient", client.Id, client.DisplayName, "[REDACTED]", "[REDACTED]");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
clientId = client.ClientId,
|
||||||
|
clientSecret = newSecret,
|
||||||
|
message = "新密钥已生成,请妥善保管,刷新后将无法再次查看"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/toggle-status")]
|
||||||
|
public async Task<ActionResult<object>> ToggleStatus(long id)
|
||||||
|
{
|
||||||
|
var client = await _context.OAuthApplications.FindAsync(id);
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldStatus = client.Status;
|
||||||
|
client.Status = client.Status == "active" ? "inactive" : "active";
|
||||||
|
client.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
await CreateAuditLog("oauth", "toggle_status", "OAuthClient", client.Id, client.DisplayName, oldStatus, client.Status);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
clientId = client.ClientId,
|
||||||
|
oldStatus,
|
||||||
|
newStatus = client.Status,
|
||||||
|
message = $"客户端状态已从 {oldStatus} 更改为 {client.Status}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> UpdateClient(long id, [FromBody] UpdateOAuthClientDto dto)
|
||||||
|
{
|
||||||
|
var client = await _context.OAuthApplications.FindAsync(id);
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var application = await _applicationManager.FindByClientIdAsync(client.ClientId);
|
||||||
|
if (application == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptor = new OpenIddictApplicationDescriptor
|
||||||
|
{
|
||||||
|
ClientId = client.ClientId,
|
||||||
|
ClientSecret = client.ClientSecret,
|
||||||
|
DisplayName = dto.DisplayName ?? client.DisplayName
|
||||||
|
};
|
||||||
|
|
||||||
|
var redirectUris = dto.RedirectUris ?? client.RedirectUris;
|
||||||
|
foreach (var uri in redirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
||||||
|
{
|
||||||
|
descriptor.RedirectUris.Add(new Uri(uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
var postLogoutUris = dto.PostLogoutRedirectUris ?? client.PostLogoutRedirectUris;
|
||||||
|
foreach (var uri in postLogoutUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
||||||
|
{
|
||||||
|
descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
var grantTypes = dto.GrantTypes ?? client.GrantTypes;
|
||||||
|
foreach (var grantType in grantTypes)
|
||||||
|
{
|
||||||
|
descriptor.Permissions.Add(grantType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes = dto.Scopes ?? client.Scopes;
|
||||||
|
foreach (var scope in scopes)
|
||||||
|
{
|
||||||
|
descriptor.Permissions.Add(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _applicationManager.UpdateAsync(application, descriptor);
|
||||||
|
|
||||||
|
client.DisplayName = dto.DisplayName ?? client.DisplayName;
|
||||||
|
client.RedirectUris = redirectUris;
|
||||||
|
client.PostLogoutRedirectUris = postLogoutUris;
|
||||||
|
client.Scopes = scopes;
|
||||||
|
client.GrantTypes = grantTypes;
|
||||||
|
client.ClientType = dto.ClientType ?? client.ClientType;
|
||||||
|
client.ConsentType = dto.ConsentType ?? client.ConsentType;
|
||||||
|
client.Status = dto.Status ?? client.Status;
|
||||||
|
client.Description = dto.Description ?? client.Description;
|
||||||
|
client.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
await CreateAuditLog("oauth", "update", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(client));
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@ -191,16 +364,35 @@ public class OAuthClientsController : ControllerBase
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldValue = SerializeToJson(client);
|
try
|
||||||
|
{
|
||||||
|
var application = await _applicationManager.FindByClientIdAsync(client.ClientId);
|
||||||
|
if (application != null)
|
||||||
|
{
|
||||||
|
await _applicationManager.DeleteAsync(application);
|
||||||
|
_logger.LogInformation("Deleted OpenIddict application {ClientId}", client.ClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to delete OpenIddict application for client {ClientId}", client.ClientId);
|
||||||
|
}
|
||||||
|
|
||||||
_context.OAuthApplications.Remove(client);
|
_context.OAuthApplications.Remove(client);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
await CreateAuditLog("oauth", "delete", "OAuthClient", client.Id, client.DisplayName, oldValue);
|
await CreateAuditLog("oauth", "delete", "OAuthClient", client.Id, client.DisplayName, SerializeToJson(client));
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GenerateSecureSecret(int length = 32)
|
||||||
|
{
|
||||||
|
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
var bytes = RandomNumberGenerator.GetBytes(length);
|
||||||
|
return new string(bytes.Select(b => chars[b % chars.Length]).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
|
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
|
||||||
{
|
{
|
||||||
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
|
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
|
||||||
@ -225,7 +417,7 @@ public class OAuthClientsController : ControllerBase
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SerializeToJson(object obj)
|
private static string SerializeToJson(object obj)
|
||||||
{
|
{
|
||||||
return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
|
return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
|
||||||
{
|
{
|
||||||
@ -237,27 +429,27 @@ public class OAuthClientsController : ControllerBase
|
|||||||
public class CreateOAuthClientDto
|
public class CreateOAuthClientDto
|
||||||
{
|
{
|
||||||
public string ClientId { get; set; } = string.Empty;
|
public string ClientId { get; set; } = string.Empty;
|
||||||
public string ClientSecret { get; set; } = string.Empty;
|
public string? ClientSecret { get; set; }
|
||||||
public string DisplayName { get; set; } = string.Empty;
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
public string[] RedirectUris { get; set; } = Array.Empty<string>();
|
public string[]? RedirectUris { get; set; }
|
||||||
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
|
public string[]? PostLogoutRedirectUris { get; set; }
|
||||||
public string[] Scopes { get; set; } = Array.Empty<string>();
|
public string[]? Scopes { get; set; }
|
||||||
public string[] GrantTypes { get; set; } = Array.Empty<string>();
|
public string[]? GrantTypes { get; set; }
|
||||||
public string ClientType { get; set; } = "confidential";
|
public string? ClientType { get; set; }
|
||||||
public string ConsentType { get; set; } = "implicit";
|
public string? ConsentType { get; set; }
|
||||||
public string Status { get; set; } = "active";
|
public string? Status { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UpdateOAuthClientDto
|
public class UpdateOAuthClientDto
|
||||||
{
|
{
|
||||||
public string DisplayName { get; set; } = string.Empty;
|
public string? DisplayName { get; set; }
|
||||||
public string[] RedirectUris { get; set; } = Array.Empty<string>();
|
public string[]? RedirectUris { get; set; }
|
||||||
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
|
public string[]? PostLogoutRedirectUris { get; set; }
|
||||||
public string[] Scopes { get; set; } = Array.Empty<string>();
|
public string[]? Scopes { get; set; }
|
||||||
public string[] GrantTypes { get; set; } = Array.Empty<string>();
|
public string[]? GrantTypes { get; set; }
|
||||||
public string ClientType { get; set; } = "confidential";
|
public string? ClientType { get; set; }
|
||||||
public string ConsentType { get; set; } = "implicit";
|
public string? ConsentType { get; set; }
|
||||||
public string Status { get; set; } = "active";
|
public string? Status { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,7 +123,7 @@ public class RolesController : ControllerBase
|
|||||||
userName = u.UserName,
|
userName = u.UserName,
|
||||||
email = u.Email,
|
email = u.Email,
|
||||||
realName = u.RealName,
|
realName = u.RealName,
|
||||||
tenantId = u.TenantId,
|
tenantId = u.TenantInfo.Id,
|
||||||
roles = await _userManager.GetRolesAsync(u),
|
roles = await _userManager.GetRolesAsync(u),
|
||||||
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
|
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
|
||||||
createdAt = u.CreatedTime,
|
createdAt = u.CreatedTime,
|
||||||
|
|||||||
@ -64,7 +64,7 @@ public class TenantsController : ControllerBase
|
|||||||
|
|
||||||
foreach (var tenant in tenants)
|
foreach (var tenant in tenants)
|
||||||
{
|
{
|
||||||
var userCount = await _context.Users.CountAsync(u => u.TenantId == tenant.Id && !u.IsDeleted);
|
var userCount = await _context.Users.CountAsync(u => u.TenantInfo.Id == tenant.Id && !u.IsDeleted);
|
||||||
result.Add(new
|
result.Add(new
|
||||||
{
|
{
|
||||||
id = tenant.Id,
|
id = tenant.Id,
|
||||||
@ -127,7 +127,7 @@ public class TenantsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var users = await _context.Users
|
var users = await _context.Users
|
||||||
.Where(u => u.TenantId == tenant.Id && !u.IsDeleted)
|
.Where(u => u.TenantInfo.Id == tenant.Id && !u.IsDeleted)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var result = users.Select(async u => new
|
var result = users.Select(async u => new
|
||||||
@ -136,7 +136,7 @@ public class TenantsController : ControllerBase
|
|||||||
userName = u.UserName,
|
userName = u.UserName,
|
||||||
email = u.Email,
|
email = u.Email,
|
||||||
realName = u.RealName,
|
realName = u.RealName,
|
||||||
tenantId = u.TenantId,
|
tenantId = u.TenantInfo.Id,
|
||||||
roles = await _userManager.GetRolesAsync(u),
|
roles = await _userManager.GetRolesAsync(u),
|
||||||
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
|
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
|
||||||
createdAt = u.CreatedTime,
|
createdAt = u.CreatedTime,
|
||||||
@ -268,7 +268,7 @@ public class TenantsController : ControllerBase
|
|||||||
|
|
||||||
var oldValue = JsonSerializer.Serialize(tenant);
|
var oldValue = JsonSerializer.Serialize(tenant);
|
||||||
|
|
||||||
var users = await _context.Users.Where(u => u.TenantId == tenant.Id).ToListAsync();
|
var users = await _context.Users.Where(u => u.TenantInfo.Id == tenant.Id).ToListAsync();
|
||||||
foreach (var user in users)
|
foreach (var user in users)
|
||||||
{
|
{
|
||||||
user.IsDeleted = true;
|
user.IsDeleted = true;
|
||||||
|
|||||||
@ -98,7 +98,7 @@ public class TokenController(
|
|||||||
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
|
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
|
||||||
new(Claims.Name, await userManager.GetUserNameAsync(user)),
|
new(Claims.Name, await userManager.GetUserNameAsync(user)),
|
||||||
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
|
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
|
||||||
new("tenant_id", user.TenantId.ToString())
|
new("tenant_id", user.TenantInfo.Id.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
var roles = await userManager.GetRolesAsync(user);
|
var roles = await userManager.GetRolesAsync(user);
|
||||||
@ -235,7 +235,7 @@ public class TokenController(
|
|||||||
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
|
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
|
||||||
new(Claims.Name, await userManager.GetUserNameAsync(user)),
|
new(Claims.Name, await userManager.GetUserNameAsync(user)),
|
||||||
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
|
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
|
||||||
new("tenant_id", user.TenantId.ToString())
|
new("tenant_id", user.TenantInfo.Id.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
var roles = await userManager.GetRolesAsync(user);
|
var roles = await userManager.GetRolesAsync(user);
|
||||||
|
|||||||
@ -52,7 +52,7 @@ public class UsersController : ControllerBase
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(tenantId))
|
if (!string.IsNullOrEmpty(tenantId))
|
||||||
{
|
{
|
||||||
query = query.Where(u => u.TenantId.ToString() == tenantId);
|
query = query.Where(u => u.TenantInfo.Id.ToString() == tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalCount = await query.CountAsync();
|
var totalCount = await query.CountAsync();
|
||||||
@ -69,7 +69,7 @@ public class UsersController : ControllerBase
|
|||||||
email = u.Email,
|
email = u.Email,
|
||||||
realName = u.RealName,
|
realName = u.RealName,
|
||||||
phone = u.Phone,
|
phone = u.Phone,
|
||||||
tenantId = u.TenantId,
|
tenantId = u.TenantInfo.Id,
|
||||||
roles = (await _userManager.GetRolesAsync(u)).ToList(),
|
roles = (await _userManager.GetRolesAsync(u)).ToList(),
|
||||||
emailConfirmed = u.EmailConfirmed,
|
emailConfirmed = u.EmailConfirmed,
|
||||||
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
|
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
|
||||||
@ -103,7 +103,7 @@ public class UsersController : ControllerBase
|
|||||||
email = user.Email,
|
email = user.Email,
|
||||||
realName = user.RealName,
|
realName = user.RealName,
|
||||||
phone = user.Phone,
|
phone = user.Phone,
|
||||||
tenantId = user.TenantId,
|
tenantId = user.TenantInfo.Id,
|
||||||
roles,
|
roles,
|
||||||
emailConfirmed = user.EmailConfirmed,
|
emailConfirmed = user.EmailConfirmed,
|
||||||
isActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow,
|
isActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow,
|
||||||
@ -114,13 +114,25 @@ public class UsersController : ControllerBase
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<ApplicationUser>> CreateUser(CreateUserDto dto)
|
public async Task<ActionResult<ApplicationUser>> CreateUser(CreateUserDto dto)
|
||||||
{
|
{
|
||||||
|
var tenantId = dto.TenantId ?? 0;
|
||||||
|
Tenant tenant = null;
|
||||||
|
|
||||||
|
if (tenantId != 0)
|
||||||
|
{
|
||||||
|
tenant = await _context.Tenants.FindAsync(tenantId);
|
||||||
|
if (tenant == null)
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid tenant ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var user = new ApplicationUser
|
var user = new ApplicationUser
|
||||||
{
|
{
|
||||||
UserName = dto.UserName,
|
UserName = dto.UserName,
|
||||||
Email = dto.Email,
|
Email = dto.Email,
|
||||||
RealName = dto.RealName,
|
RealName = dto.RealName,
|
||||||
Phone = dto.Phone,
|
Phone = dto.Phone,
|
||||||
TenantId = dto.TenantId ?? 0,
|
TenantInfo = new TenantInfo(tenantId, tenant?.TenantId ?? "default", tenant?.Name ?? "默认租户"),
|
||||||
EmailConfirmed = dto.EmailConfirmed,
|
EmailConfirmed = dto.EmailConfirmed,
|
||||||
CreatedTime = DateTime.UtcNow,
|
CreatedTime = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,7 +24,6 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, Applicati
|
|||||||
{
|
{
|
||||||
entity.Property(e => e.RealName).HasMaxLength(100);
|
entity.Property(e => e.RealName).HasMaxLength(100);
|
||||||
entity.Property(e => e.Phone).HasMaxLength(20);
|
entity.Property(e => e.Phone).HasMaxLength(20);
|
||||||
entity.HasIndex(e => e.TenantId);
|
|
||||||
entity.HasIndex(e => e.Phone).IsUnique();
|
entity.HasIndex(e => e.Phone).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@ public static class SeedData
|
|||||||
Email = "admin@fengling.local",
|
Email = "admin@fengling.local",
|
||||||
RealName = "系统管理员",
|
RealName = "系统管理员",
|
||||||
Phone = "13800138000",
|
Phone = "13800138000",
|
||||||
TenantId = defaultTenant.Id,
|
TenantInfo = new TenantInfo(defaultTenant.Id, defaultTenant.TenantId, defaultTenant.Name),
|
||||||
EmailConfirmed = true,
|
EmailConfirmed = true,
|
||||||
IsDeleted = false,
|
IsDeleted = false,
|
||||||
CreatedTime = DateTime.UtcNow
|
CreatedTime = DateTime.UtcNow
|
||||||
@ -105,7 +105,7 @@ public static class SeedData
|
|||||||
Email = "test@fengling.local",
|
Email = "test@fengling.local",
|
||||||
RealName = "测试用户",
|
RealName = "测试用户",
|
||||||
Phone = "13900139000",
|
Phone = "13900139000",
|
||||||
TenantId = defaultTenant.Id,
|
TenantInfo = new TenantInfo(defaultTenant.Id, defaultTenant.TenantId, defaultTenant.Name),
|
||||||
EmailConfirmed = true,
|
EmailConfirmed = true,
|
||||||
IsDeleted = false,
|
IsDeleted = false,
|
||||||
CreatedTime = DateTime.UtcNow
|
CreatedTime = DateTime.UtcNow
|
||||||
|
|||||||
@ -6,7 +6,7 @@ public class ApplicationUser : IdentityUser<long>
|
|||||||
{
|
{
|
||||||
public string? RealName { get; set; }
|
public string? RealName { get; set; }
|
||||||
public string? Phone { get; set; }
|
public string? Phone { get; set; }
|
||||||
public long TenantId { get; set; }
|
public TenantInfo TenantInfo { get; set; } = null!;
|
||||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime? UpdatedTime { get; set; }
|
public DateTime? UpdatedTime { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|||||||
@ -4,16 +4,32 @@ namespace Fengling.AuthService.Models;
|
|||||||
|
|
||||||
public class Tenant
|
public class Tenant
|
||||||
{
|
{
|
||||||
|
private long _id;
|
||||||
|
private string _tenantId;
|
||||||
|
private string _name;
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
public long Id { get; set; }
|
public long Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
set => _id = value;
|
||||||
|
}
|
||||||
|
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
[Required]
|
[Required]
|
||||||
public string TenantId { get; set; } = string.Empty;
|
public string TenantId
|
||||||
|
{
|
||||||
|
get => _tenantId;
|
||||||
|
set => _tenantId = value;
|
||||||
|
}
|
||||||
|
|
||||||
[MaxLength(100)]
|
[MaxLength(100)]
|
||||||
[Required]
|
[Required]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set => _name = value;
|
||||||
|
}
|
||||||
|
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
[Required]
|
[Required]
|
||||||
@ -43,5 +59,5 @@ public class Tenant
|
|||||||
|
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
public ICollection<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();
|
public TenantInfo Info => new(Id, TenantId, Name);
|
||||||
}
|
}
|
||||||
|
|||||||
3
Models/TenantInfo.cs
Normal file
3
Models/TenantInfo.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Fengling.AuthService.Models;
|
||||||
|
|
||||||
|
public record TenantInfo(long Id, string TenantId, string Name);
|
||||||
@ -31,6 +31,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<form method="post" class="space-y-4">
|
<form method="post" class="space-y-4">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" asp-for="ReturnUrl" />
|
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" asp-for="ReturnUrl" />
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<form method="post" class="space-y-4">
|
<form method="post" class="space-y-4">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" asp-for="ReturnUrl" />
|
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" asp-for="ReturnUrl" />
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
@ -56,24 +56,26 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown Menu -->
|
<!-- Dropdown Menu -->
|
||||||
<div class="absolute right-0 top-full mt-1 w-48 rounded-md border border-border bg-white shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all">
|
<div class="absolute right-0 top-full mt-1 w-48 rounded-md border border-border bg-white shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all">
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<a href="/profile" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors">个人资料</a>
|
<a href="/dashboard/profile" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors">个人资料</a>
|
||||||
<a href="/settings" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors">设置</a>
|
<a href="/dashboard/settings" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors">设置</a>
|
||||||
<hr class="my-2 border-border">
|
<hr class="my-2 border-border">
|
||||||
<a href="/logout" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors text-red-600">退出登录</a>
|
<form method="post" action="/account/logout">
|
||||||
</div>
|
<button type="submit" class="block w-full text-left rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors text-red-600">退出登录</button>
|
||||||
</div>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<!-- Login/Register Buttons (Guest) -->
|
<!-- Login/Register Buttons (Guest) -->
|
||||||
<a href="/login" class="text-sm font-medium hover:text-foreground text-muted-foreground transition-colors">登录</a>
|
<a href="/account/login" class="text-sm font-medium hover:text-foreground text-muted-foreground transition-colors">登录</a>
|
||||||
<a href="/register" class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2">
|
<a href="/account/register" class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2">
|
||||||
注册
|
注册
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,193 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>登录 - 风铃认证服务</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
||||||
padding: 40px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #555;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
input[type="text"],
|
|
||||||
input[type="password"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
input[type="text"]:focus,
|
|
||||||
input[type="password"]:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
.remember-me {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.remember-me input {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
background: #fee;
|
|
||||||
color: #c33;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.loading {
|
|
||||||
display: inline-block;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: white;
|
|
||||||
animation: spin 0.8s ease-in-out infinite;
|
|
||||||
margin-right: 8px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>风铃认证服务</h1>
|
|
||||||
<div id="error-message" class="error" style="display: none;"></div>
|
|
||||||
<form id="login-form">
|
|
||||||
<input type="hidden" id="returnUrl" name="returnUrl">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">用户名</label>
|
|
||||||
<input type="text" id="username" name="username" required autofocus autocomplete="username">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">密码</label>
|
|
||||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
|
||||||
</div>
|
|
||||||
<div class="remember-me">
|
|
||||||
<input type="checkbox" id="rememberMe" name="rememberMe">
|
|
||||||
<label for="rememberMe" style="margin-bottom: 0;">记住我</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit" id="submit-btn">
|
|
||||||
<span id="btn-text">登录</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const form = document.getElementById('login-form');
|
|
||||||
const errorElement = document.getElementById('error-message');
|
|
||||||
const submitBtn = document.getElementById('submit-btn');
|
|
||||||
const btnText = document.getElementById('btn-text');
|
|
||||||
|
|
||||||
// Get returnUrl from query parameter
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const returnUrl = urlParams.get('returnUrl') || '/';
|
|
||||||
document.getElementById('returnUrl').value = returnUrl;
|
|
||||||
|
|
||||||
form.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const username = document.getElementById('username').value;
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
const rememberMe = document.getElementById('rememberMe').checked;
|
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
btnText.innerHTML = '<span class="loading"></span>登录中...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/account/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
rememberMe: rememberMe,
|
|
||||||
returnUrl: returnUrl
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
// Redirect to returnUrl
|
|
||||||
window.location.href = data.returnUrl;
|
|
||||||
} else {
|
|
||||||
// Show error
|
|
||||||
errorElement.textContent = data.error || '登录失败,请重试';
|
|
||||||
errorElement.style.display = 'block';
|
|
||||||
|
|
||||||
// Reset button
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
btnText.textContent = '登录';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorElement.textContent = '网络错误,请重试';
|
|
||||||
errorElement.style.display = 'block';
|
|
||||||
|
|
||||||
// Reset button
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
btnText.textContent = '登录';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in New Issue
Block a user