feat: Enhance user profile and address management, add SMTP settings retrieval, and improve navigation links
This commit is contained in:
parent
c9c0396ad2
commit
028cc39a40
@ -5,6 +5,7 @@ namespace MemberCenter.Application.Abstractions;
|
||||
public interface ISecuritySettingsService
|
||||
{
|
||||
Task<SecuritySettingsDto> GetAsync();
|
||||
Task<SmtpSettingsDto> GetSmtpSettingsAsync();
|
||||
Task SaveAsync(SecuritySettingsDto settings, Guid? actorUserId = null);
|
||||
Task<int> SendTestEmailAsync(string toEmail, Guid? actorUserId = null);
|
||||
}
|
||||
|
||||
@ -129,10 +129,10 @@ public class MemberCenterDbContext
|
||||
entity.HasIndex(x => x.UserId).HasDatabaseName("idx_user_addresses_user_id");
|
||||
entity.HasIndex(x => new { x.UserId, x.Usage })
|
||||
.HasDatabaseName("idx_user_addresses_user_id_usage");
|
||||
entity.HasIndex(x => new { x.UserId, x.Usage, x.IsDefault })
|
||||
entity.HasIndex(x => new { x.UserId, x.IsDefault })
|
||||
.IsUnique()
|
||||
.HasFilter("\"is_default\" = true")
|
||||
.HasDatabaseName("ux_user_addresses_default_per_usage");
|
||||
.HasFilter("\"IsDefault\" = true")
|
||||
.HasDatabaseName("ux_user_addresses_default_per_user");
|
||||
entity.HasOne<ApplicationUser>()
|
||||
.WithMany(x => x.Addresses)
|
||||
.HasForeignKey(x => x.UserId)
|
||||
|
||||
@ -380,7 +380,7 @@ namespace MemberCenter.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId", "Usage", "IsDefault")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ux_user_addresses_default_per_usage")
|
||||
.HasFilter("\"is_default\" = true");
|
||||
.HasFilter("\"IsDefault\" = true");
|
||||
|
||||
b.ToTable("user_addresses", (string)null);
|
||||
});
|
||||
|
||||
@ -127,7 +127,7 @@ namespace MemberCenter.Infrastructure.Persistence.Migrations
|
||||
table: "user_addresses",
|
||||
columns: new[] { "UserId", "Usage", "IsDefault" },
|
||||
unique: true,
|
||||
filter: "\"is_default\" = true");
|
||||
filter: "\"IsDefault\" = true");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
using MemberCenter.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MemberCenter.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(MemberCenterDbContext))]
|
||||
[Migration("20260417043000_ChangeUserAddressDefaultToSinglePerUser")]
|
||||
public partial class ChangeUserAddressDefaultToSinglePerUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
WITH ranked_defaults AS (
|
||||
SELECT "Id",
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "UserId"
|
||||
ORDER BY "UpdatedAt" DESC, "CreatedAt" DESC, "Id"
|
||||
) AS rn
|
||||
FROM user_addresses
|
||||
WHERE "IsDefault" = true
|
||||
)
|
||||
UPDATE user_addresses ua
|
||||
SET "IsDefault" = false,
|
||||
"UpdatedAt" = now()
|
||||
FROM ranked_defaults rd
|
||||
WHERE ua."Id" = rd."Id"
|
||||
AND rd.rn > 1;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
WITH users_without_default AS (
|
||||
SELECT ua."UserId"
|
||||
FROM user_addresses ua
|
||||
GROUP BY ua."UserId"
|
||||
HAVING BOOL_OR(ua."IsDefault") = false
|
||||
),
|
||||
replacement AS (
|
||||
SELECT ranked."Id"
|
||||
FROM (
|
||||
SELECT ua."Id",
|
||||
ua."UserId",
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY ua."UserId"
|
||||
ORDER BY ua."UpdatedAt" DESC, ua."CreatedAt" DESC, ua."Id"
|
||||
) AS rn
|
||||
FROM user_addresses ua
|
||||
INNER JOIN users_without_default uwd ON uwd."UserId" = ua."UserId"
|
||||
) ranked
|
||||
WHERE ranked.rn = 1
|
||||
)
|
||||
UPDATE user_addresses ua
|
||||
SET "IsDefault" = true,
|
||||
"UpdatedAt" = now()
|
||||
FROM replacement r
|
||||
WHERE ua."Id" = r."Id";
|
||||
""");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ux_user_addresses_default_per_usage",
|
||||
table: "user_addresses");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ux_user_addresses_default_per_user",
|
||||
table: "user_addresses",
|
||||
columns: new[] { "UserId", "IsDefault" },
|
||||
unique: true,
|
||||
filter: "\"IsDefault\" = true");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ux_user_addresses_default_per_user",
|
||||
table: "user_addresses");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ux_user_addresses_default_per_usage",
|
||||
table: "user_addresses",
|
||||
columns: new[] { "UserId", "Usage", "IsDefault" },
|
||||
unique: true,
|
||||
filter: "\"IsDefault\" = true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -374,10 +374,10 @@ namespace MemberCenter.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId", "Usage")
|
||||
.HasDatabaseName("idx_user_addresses_user_id_usage");
|
||||
|
||||
b.HasIndex("UserId", "Usage", "IsDefault")
|
||||
b.HasIndex("UserId", "IsDefault")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ux_user_addresses_default_per_usage")
|
||||
.HasFilter("\"is_default\" = true");
|
||||
.HasDatabaseName("ux_user_addresses_default_per_user")
|
||||
.HasFilter("\"IsDefault\" = true");
|
||||
|
||||
b.ToTable("user_addresses", (string)null);
|
||||
});
|
||||
|
||||
@ -107,10 +107,13 @@ public sealed class ProfileService : IProfileService
|
||||
|
||||
public async Task<UserAddressDto> SaveAddressAsync(Guid userId, SaveUserAddressRequest request)
|
||||
{
|
||||
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var usage = NormalizeUsage(request.Usage);
|
||||
var address = request.Id.HasValue
|
||||
? await _dbContext.UserAddresses.FirstOrDefaultAsync(x => x.UserId == userId && x.Id == request.Id.Value)
|
||||
: null;
|
||||
var wasDefault = address?.IsDefault == true;
|
||||
|
||||
if (request.Id.HasValue && address is null)
|
||||
{
|
||||
@ -143,19 +146,33 @@ public sealed class ProfileService : IProfileService
|
||||
address.AddressMetaJson = ParseOptionalJson(request.AddressMetaJson);
|
||||
address.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var existingWithUsage = await _dbContext.UserAddresses
|
||||
.Where(x => x.UserId == userId && x.Usage == usage && x.Id != address.Id)
|
||||
var otherAddresses = await _dbContext.UserAddresses
|
||||
.Where(x => x.UserId == userId && x.Id != address.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var shouldBeDefault = request.IsDefault || !existingWithUsage.Any();
|
||||
address.IsDefault = shouldBeDefault;
|
||||
var shouldBeDefault = !otherAddresses.Any()
|
||||
|| request.IsDefault
|
||||
|| (!otherAddresses.Any(x => x.IsDefault) && wasDefault)
|
||||
|| (!otherAddresses.Any(x => x.IsDefault) && !request.Id.HasValue);
|
||||
|
||||
// Persist the address first with a non-default state so the unique index
|
||||
// never sees two defaults during the switch.
|
||||
address.IsDefault = false;
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
if (shouldBeDefault)
|
||||
{
|
||||
foreach (var item in existingWithUsage)
|
||||
{
|
||||
item.IsDefault = false;
|
||||
item.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
await _dbContext.UserAddresses
|
||||
.Where(x => x.UserId == userId && x.Id != address.Id && x.IsDefault)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.IsDefault, false)
|
||||
.SetProperty(x => x.UpdatedAt, DateTimeOffset.UtcNow));
|
||||
|
||||
address.IsDefault = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
address.IsDefault = false;
|
||||
}
|
||||
|
||||
await _auditLogWriter.WriteAsync("user", userId, request.Id.HasValue ? "address.updated" : "address.created", new
|
||||
@ -167,11 +184,14 @@ public sealed class ProfileService : IProfileService
|
||||
});
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return MapAddress(address);
|
||||
}
|
||||
|
||||
public async Task DeleteAddressAsync(Guid userId, Guid addressId)
|
||||
{
|
||||
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var addresses = await _dbContext.UserAddresses
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.IsDefault)
|
||||
@ -189,20 +209,29 @@ public sealed class ProfileService : IProfileService
|
||||
throw new InvalidOperationException("Cannot delete the last address.");
|
||||
}
|
||||
|
||||
_dbContext.UserAddresses.Remove(address);
|
||||
|
||||
if (address.IsDefault)
|
||||
{
|
||||
var replacement = addresses
|
||||
.Where(x => x.Id != addressId && string.Equals(x.Usage, address.Usage, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(x => x.Id != addressId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (replacement is not null)
|
||||
{
|
||||
_dbContext.UserAddresses.Remove(address);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
replacement.IsDefault = true;
|
||||
replacement.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dbContext.UserAddresses.Remove(address);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_dbContext.UserAddresses.Remove(address);
|
||||
}
|
||||
|
||||
await _auditLogWriter.WriteAsync("user", userId, "address.deleted", new
|
||||
@ -212,6 +241,7 @@ public sealed class ProfileService : IProfileService
|
||||
});
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
private async Task<UserProfile> EnsureProfileAsync(Guid userId)
|
||||
|
||||
@ -56,8 +56,40 @@ public sealed class SecuritySettingsService : ISecuritySettingsService
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
public async Task<SmtpSettingsDto> GetSmtpSettingsAsync()
|
||||
{
|
||||
var relayHost = await GetFlagAsync(SmtpRelayHostKey, string.Empty);
|
||||
var publicBaseUrl = await GetFlagAsync(PublicBaseUrlKey, string.Empty);
|
||||
var relayPort = await GetFlagAsync(SmtpRelayPortKey, 587);
|
||||
var useTls = await GetFlagAsync(SmtpUseTlsKey, true);
|
||||
var useSsl = await GetFlagAsync(SmtpUseSslKey, false);
|
||||
var timeoutSeconds = await GetFlagAsync(SmtpTimeoutSecondsKey, 15);
|
||||
var username = await GetFlagAsync(SmtpUsernameKey, string.Empty);
|
||||
var password = await GetFlagAsync(SmtpPasswordKey, string.Empty);
|
||||
var senderName = await GetFlagAsync(SenderNameKey, "Member Center");
|
||||
var senderEmail = await GetFlagAsync(SenderEmailKey, string.Empty);
|
||||
return new SmtpSettingsDto(
|
||||
publicBaseUrl,
|
||||
relayHost,
|
||||
relayPort,
|
||||
useTls,
|
||||
useSsl,
|
||||
timeoutSeconds,
|
||||
username,
|
||||
password,
|
||||
!string.IsNullOrWhiteSpace(password),
|
||||
senderName,
|
||||
senderEmail);
|
||||
}
|
||||
|
||||
public async Task SaveAsync(SecuritySettingsDto settings, Guid? actorUserId = null)
|
||||
{
|
||||
var publicBaseUrl = Normalize(settings.PublicBaseUrl);
|
||||
var relayHost = Normalize(settings.SmtpRelayHost);
|
||||
var username = Normalize(settings.SmtpUsername);
|
||||
var senderName = Normalize(settings.SenderName);
|
||||
var senderEmail = Normalize(settings.SenderEmail);
|
||||
|
||||
if (settings.SmtpUseTls && settings.SmtpUseSsl)
|
||||
{
|
||||
throw new InvalidOperationException("SMTP TLS and SSL cannot both be enabled.");
|
||||
@ -65,33 +97,33 @@ public sealed class SecuritySettingsService : ISecuritySettingsService
|
||||
|
||||
await SetFlagAsync(AccessTokenKey, settings.AccessTokenMinutes.ToString());
|
||||
await SetFlagAsync(RefreshTokenKey, settings.RefreshTokenDays.ToString());
|
||||
await SetFlagAsync(PublicBaseUrlKey, settings.PublicBaseUrl.Trim());
|
||||
await SetFlagAsync(SmtpRelayHostKey, settings.SmtpRelayHost.Trim());
|
||||
await SetFlagAsync(PublicBaseUrlKey, publicBaseUrl);
|
||||
await SetFlagAsync(SmtpRelayHostKey, relayHost);
|
||||
await SetFlagAsync(SmtpRelayPortKey, settings.SmtpRelayPort.ToString());
|
||||
await SetFlagAsync(SmtpUseTlsKey, settings.SmtpUseTls.ToString());
|
||||
await SetFlagAsync(SmtpUseSslKey, settings.SmtpUseSsl.ToString());
|
||||
await SetFlagAsync(SmtpTimeoutSecondsKey, settings.SmtpTimeoutSeconds.ToString());
|
||||
await SetFlagAsync(SmtpUsernameKey, settings.SmtpUsername.Trim());
|
||||
await SetFlagAsync(SmtpUsernameKey, username);
|
||||
if (!string.IsNullOrWhiteSpace(settings.SmtpPassword))
|
||||
{
|
||||
await SetFlagAsync(SmtpPasswordKey, settings.SmtpPassword);
|
||||
}
|
||||
await SetFlagAsync(SenderNameKey, settings.SenderName.Trim());
|
||||
await SetFlagAsync(SenderEmailKey, settings.SenderEmail.Trim());
|
||||
await SetFlagAsync(SenderNameKey, senderName);
|
||||
await SetFlagAsync(SenderEmailKey, senderEmail);
|
||||
|
||||
await _auditLogWriter.WriteAsync("user", actorUserId, "system.security_settings_updated", new
|
||||
{
|
||||
settings.AccessTokenMinutes,
|
||||
settings.RefreshTokenDays,
|
||||
settings.PublicBaseUrl,
|
||||
settings.SmtpRelayHost,
|
||||
PublicBaseUrl = publicBaseUrl,
|
||||
SmtpRelayHost = relayHost,
|
||||
settings.SmtpRelayPort,
|
||||
settings.SmtpUseTls,
|
||||
settings.SmtpUseSsl,
|
||||
settings.SmtpTimeoutSeconds,
|
||||
settings.SmtpUsername,
|
||||
settings.SenderName,
|
||||
settings.SenderEmail
|
||||
SmtpUsername = username,
|
||||
SenderName = senderName,
|
||||
SenderEmail = senderEmail
|
||||
});
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
@ -133,32 +165,6 @@ public sealed class SecuritySettingsService : ISecuritySettingsService
|
||||
return 1;
|
||||
}
|
||||
|
||||
private async Task<SmtpSettingsDto> GetSmtpSettingsAsync()
|
||||
{
|
||||
var relayHost = await GetFlagAsync(SmtpRelayHostKey, string.Empty);
|
||||
var publicBaseUrl = await GetFlagAsync(PublicBaseUrlKey, string.Empty);
|
||||
var relayPort = await GetFlagAsync(SmtpRelayPortKey, 587);
|
||||
var useTls = await GetFlagAsync(SmtpUseTlsKey, true);
|
||||
var useSsl = await GetFlagAsync(SmtpUseSslKey, false);
|
||||
var timeoutSeconds = await GetFlagAsync(SmtpTimeoutSecondsKey, 15);
|
||||
var username = await GetFlagAsync(SmtpUsernameKey, string.Empty);
|
||||
var password = await GetFlagAsync(SmtpPasswordKey, string.Empty);
|
||||
var senderName = await GetFlagAsync(SenderNameKey, "Member Center");
|
||||
var senderEmail = await GetFlagAsync(SenderEmailKey, string.Empty);
|
||||
return new SmtpSettingsDto(
|
||||
publicBaseUrl,
|
||||
relayHost,
|
||||
relayPort,
|
||||
useTls,
|
||||
useSsl,
|
||||
timeoutSeconds,
|
||||
username,
|
||||
password,
|
||||
!string.IsNullOrWhiteSpace(password),
|
||||
senderName,
|
||||
senderEmail);
|
||||
}
|
||||
|
||||
private static void ValidateSmtpSettings(SmtpSettingsDto smtp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(smtp.RelayHost))
|
||||
@ -225,4 +231,6 @@ public sealed class SecuritySettingsService : ISecuritySettingsService
|
||||
}
|
||||
}
|
||||
|
||||
private static string Normalize(string? value) => value?.Trim() ?? string.Empty;
|
||||
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ public sealed class SmtpEmailSender : IEmailSender
|
||||
|
||||
public async Task<int> SendAsync(string toEmail, string subject, string textBody, string? htmlBody = null)
|
||||
{
|
||||
var settings = await _securitySettingsService.GetAsync();
|
||||
var settings = await _securitySettingsService.GetSmtpSettingsAsync();
|
||||
Validate(settings);
|
||||
|
||||
using var message = new MailMessage
|
||||
@ -36,30 +36,30 @@ public sealed class SmtpEmailSender : IEmailSender
|
||||
message.IsBodyHtml = false;
|
||||
}
|
||||
|
||||
using var client = new SmtpClient(settings.SmtpRelayHost, settings.SmtpRelayPort)
|
||||
using var client = new SmtpClient(settings.RelayHost, settings.RelayPort)
|
||||
{
|
||||
EnableSsl = settings.SmtpUseSsl || settings.SmtpUseTls,
|
||||
EnableSsl = settings.UseSsl || settings.UseTls,
|
||||
DeliveryMethod = SmtpDeliveryMethod.Network,
|
||||
Timeout = Math.Max(1000, settings.SmtpTimeoutSeconds * 1000)
|
||||
Timeout = Math.Max(1000, settings.TimeoutSeconds * 1000)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.SmtpUsername))
|
||||
if (!string.IsNullOrWhiteSpace(settings.Username))
|
||||
{
|
||||
client.Credentials = new NetworkCredential(settings.SmtpUsername, settings.SmtpPassword);
|
||||
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
|
||||
}
|
||||
|
||||
await client.SendMailAsync(message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void Validate(SecuritySettingsDto settings)
|
||||
private static void Validate(SmtpSettingsDto settings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(settings.SmtpRelayHost))
|
||||
if (string.IsNullOrWhiteSpace(settings.RelayHost))
|
||||
{
|
||||
throw new InvalidOperationException("SMTP relay host is empty. Please save SMTP settings first.");
|
||||
}
|
||||
|
||||
if (settings.SmtpUseTls && settings.SmtpUseSsl)
|
||||
if (settings.UseTls && settings.UseSsl)
|
||||
{
|
||||
throw new InvalidOperationException("SMTP TLS and SSL cannot both be enabled.");
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ public class AccountController : Controller
|
||||
return LocalRedirect(model.ReturnUrl);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
return RedirectToAction("Index", "Home", new { area = string.Empty });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -144,15 +144,16 @@ public class AccountController : Controller
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
return RedirectToAction("Index", "Home", new { area = string.Empty });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
return RedirectToAction("Index", "Home");
|
||||
return LocalRedirect("~/");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -174,7 +175,7 @@ public class AccountController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
return RedirectToAction(nameof(Login), new { area = string.Empty });
|
||||
}
|
||||
|
||||
var result = await _userManager.ChangePasswordAsync(user, model.CurrentPassword, model.NewPassword);
|
||||
@ -326,7 +327,7 @@ public class AccountController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
return RedirectToAction(nameof(Login), new { area = string.Empty });
|
||||
}
|
||||
|
||||
if (!user.EmailConfirmed)
|
||||
@ -335,7 +336,7 @@ public class AccountController : Controller
|
||||
TempData["Result"] = "Verification email sent.";
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Profile");
|
||||
return RedirectToAction("Index", "Profile", new { area = string.Empty });
|
||||
}
|
||||
|
||||
private string GetBaseUrl() => $"{Request.Scheme}://{Request.Host}{Request.PathBase}";
|
||||
|
||||
@ -31,11 +31,11 @@ public class ProfileController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
var profile = await _profileService.GetProfileAsync(user.Id);
|
||||
return View(MapProfile(profile));
|
||||
return View(MapProfile(profile, user.EmailConfirmed));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -50,7 +50,7 @@ public class ProfileController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
try
|
||||
@ -71,7 +71,7 @@ public class ProfileController : Controller
|
||||
model.InvoiceTitle,
|
||||
model.Remark));
|
||||
ViewData["Result"] = "Saved";
|
||||
return View(MapProfile(profile));
|
||||
return View(MapProfile(profile, user.EmailConfirmed));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
@ -86,7 +86,7 @@ public class ProfileController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
var addresses = await _profileService.ListAddressesAsync(user.Id);
|
||||
@ -109,12 +109,12 @@ public class ProfileController : Controller
|
||||
|
||||
[HttpPost("profile/addresses")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SaveAddress(AddressFormViewModel model)
|
||||
public async Task<IActionResult> SaveAddress([Bind(Prefix = "Form")] AddressFormViewModel model)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -164,7 +164,7 @@ public class ProfileController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
try
|
||||
@ -185,7 +185,7 @@ public class ProfileController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
return View(new SubscriptionsPageViewModel
|
||||
@ -201,17 +201,18 @@ public class ProfileController : Controller
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
{
|
||||
return RedirectToAction("Login", "Account");
|
||||
return RedirectToAction("Login", "Account", new { area = string.Empty });
|
||||
}
|
||||
|
||||
await _newsletterService.UnsubscribeForUserAsync(user.Id, id);
|
||||
return RedirectToAction(nameof(Subscriptions));
|
||||
}
|
||||
|
||||
private static ProfileViewModel MapProfile(UserProfileDto profile) =>
|
||||
private static ProfileViewModel MapProfile(UserProfileDto profile, bool emailConfirmed) =>
|
||||
new()
|
||||
{
|
||||
Email = profile.Email,
|
||||
EmailConfirmed = emailConfirmed,
|
||||
LastName = profile.LastName,
|
||||
FirstName = profile.FirstName,
|
||||
NickName = profile.NickName,
|
||||
|
||||
@ -49,4 +49,5 @@ public sealed class ProfileViewModel
|
||||
public string? Remark { get; set; }
|
||||
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public bool EmailConfirmed { get; set; }
|
||||
}
|
||||
|
||||
@ -128,7 +128,10 @@ builder.Services.AddOpenIddict()
|
||||
.UseDbContext<MemberCenterDbContext>();
|
||||
});
|
||||
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddControllersWithViews(options =>
|
||||
{
|
||||
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
|
||||
});
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
var app = builder.Build();
|
||||
@ -169,6 +172,12 @@ static Task HandleAdminAuthRedirectAsync(RedirectContext<CookieAuthenticationOpt
|
||||
|
||||
static async Task ValidatePrincipalAsync(CookieValidatePrincipalContext context)
|
||||
{
|
||||
await SecurityStampValidator.ValidatePrincipalAsync(context);
|
||||
if (context.Principal?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (!Guid.TryParse(userId, out var parsedUserId))
|
||||
{
|
||||
|
||||
@ -15,6 +15,10 @@
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<p>
|
||||
<a asp-area="" asp-controller="Account" asp-action="ForgotPassword">Forgot your password?</a>
|
||||
</p>
|
||||
|
||||
<form method="post" asp-area="" asp-controller="Account" asp-action="ExternalLogin">
|
||||
<input type="hidden" name="provider" value="Google" />
|
||||
<input type="hidden" name="returnUrl" value="@Model.ReturnUrl" />
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="All"></div>
|
||||
<p>Email: @Model.Email</p>
|
||||
<p>Email verification: @(Model.EmailConfirmed ? "Verified" : "Pending verification")</p>
|
||||
<label asp-for="LastName"></label>
|
||||
<input asp-for="LastName" />
|
||||
<label asp-for="FirstName"></label>
|
||||
@ -50,10 +51,11 @@
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
<p><a asp-controller="Account" asp-action="ChangePassword">Change Password</a></p>
|
||||
<form asp-controller="Account" asp-action="ResendVerification" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit">Resend Verification Email</button>
|
||||
</form>
|
||||
<p><a asp-action="Addresses">Manage Addresses</a></p>
|
||||
<p><a asp-action="Subscriptions">Manage Subscriptions</a></p>
|
||||
<p><a asp-area="" asp-controller="Account" asp-action="ChangePassword">Change Password</a></p>
|
||||
@if (!Model.EmailConfirmed)
|
||||
{
|
||||
<form asp-area="" asp-controller="Account" asp-action="ResendVerification" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit">Resend Verification Email</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Use the unsubscribe button in the last column to stop a subscription immediately.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -32,9 +33,13 @@ else
|
||||
{
|
||||
<form asp-action="Unsubscribe" asp-route-id="@subscription.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit">Unsubscribe</button>
|
||||
<button type="submit">Unsubscribe Now</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Already unsubscribed</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@ -36,6 +36,8 @@
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
<a asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
||||
<a asp-area="" asp-controller="Profile" asp-action="Index">Profile</a>
|
||||
<a asp-area="" asp-controller="Profile" asp-action="Addresses">Addresses</a>
|
||||
<a asp-area="" asp-controller="Profile" asp-action="Subscriptions">Subscriptions</a>
|
||||
<a asp-area="" asp-controller="Newsletter" asp-action="Preferences">Preferences</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user