From 98b2eeb13dc0e7c331d12a2e9a25962eb5e55afe Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 28 Jun 2025 22:53:07 +0800 Subject: [PATCH] :sparkles: Web version login --- .../Account/AccountController.cs | 9 - .../Account/NotificationService.cs | 2 +- DysonNetwork.Sphere/Auth/Auth.cs | 11 +- DysonNetwork.Sphere/Auth/AuthService.cs | 2 +- .../Auth/OpenId/AfdianOidcService.cs | 4 +- .../Auth/OpenId/DiscordOidcService.cs | 10 +- .../Connection/WebReader/WebFeedController.cs | 2 +- .../Discovery/DiscoveryController.cs | 2 +- .../Pages/Account/Profile.cshtml | 78 ++++++++ .../Pages/Account/Profile.cshtml.cs | 28 +++ .../Pages/Auth/Challenge.cshtml | 54 +++++ .../Pages/Auth/Challenge.cshtml.cs | 185 ++++++++++++++++++ DysonNetwork.Sphere/Pages/Auth/Login.cshtml | 29 +++ .../Pages/Auth/Login.cshtml.cs | 81 ++++++++ .../Pages/Shared/_Layout.cshtml | 14 ++ .../Shared/_ValidationScriptsPartial.cshtml | 3 + .../Startup/ApplicationConfiguration.cs | 2 +- .../Startup/ServiceCollectionExtensions.cs | 5 +- .../PaymentHandlers/AfdianPaymentHandler.cs | 6 +- DysonNetwork.Sphere/wwwroot/css/styles.css | 136 ++++++++++++- DysonNetwork.sln.DotSettings.user | 2 + 21 files changed, 633 insertions(+), 32 deletions(-) create mode 100644 DysonNetwork.Sphere/Pages/Account/Profile.cshtml create mode 100644 DysonNetwork.Sphere/Pages/Account/Profile.cshtml.cs create mode 100644 DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml create mode 100644 DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs create mode 100644 DysonNetwork.Sphere/Pages/Auth/Login.cshtml create mode 100644 DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs create mode 100644 DysonNetwork.Sphere/Pages/Shared/_ValidationScriptsPartial.cshtml diff --git a/DysonNetwork.Sphere/Account/AccountController.cs b/DysonNetwork.Sphere/Account/AccountController.cs index 1e67bca..29c4871 100644 --- a/DysonNetwork.Sphere/Account/AccountController.cs +++ b/DysonNetwork.Sphere/Account/AccountController.cs @@ -174,13 +174,4 @@ public class AccountController( .Take(take) .ToListAsync(); } - - [HttpPost("/maintenance/ensureProfileCreated")] - [Authorize] - [RequiredPermission("maintenance", "accounts.profiles")] - public async Task EnsureProfileCreated() - { - await accounts.EnsureAccountProfileCreated(); - return Ok(); - } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Account/NotificationService.cs b/DysonNetwork.Sphere/Account/NotificationService.cs index 8b0d90d..695177c 100644 --- a/DysonNetwork.Sphere/Account/NotificationService.cs +++ b/DysonNetwork.Sphere/Account/NotificationService.cs @@ -288,7 +288,7 @@ public class NotificationService( var client = httpFactory.CreateClient(); client.BaseAddress = _notifyEndpoint; - var request = await client.PostAsync("/api/push", new StringContent( + var request = await client.PostAsync("/push", new StringContent( JsonSerializer.Serialize(requestDict), Encoding.UTF8, "application/json" diff --git a/DysonNetwork.Sphere/Auth/Auth.cs b/DysonNetwork.Sphere/Auth/Auth.cs index c987ee4..683522a 100644 --- a/DysonNetwork.Sphere/Auth/Auth.cs +++ b/DysonNetwork.Sphere/Auth/Auth.cs @@ -15,6 +15,7 @@ public static class AuthConstants { public const string SchemeName = "DysonToken"; public const string TokenQueryParamName = "tk"; + public const string CookieTokenName = "AuthToken"; } public enum TokenType @@ -44,7 +45,7 @@ public class DysonTokenAuthHandler( : AuthenticationHandler(options, logger, encoder) { public const string AuthCachePrefix = "auth:"; - + protected override async Task HandleAuthenticateAsync() { var tokenInfo = _ExtractToken(Request); @@ -126,7 +127,7 @@ public class DysonTokenAuthHandler( SeenAt = SystemClock.Instance.GetCurrentInstant(), }; fbs.Enqueue(lastInfo); - + return AuthenticateResult.Success(ticket); } catch (Exception ex) @@ -182,7 +183,7 @@ public class DysonTokenAuthHandler( return Convert.FromBase64String(padded); } - private static TokenInfo? _ExtractToken(HttpRequest request) + private TokenInfo? _ExtractToken(HttpRequest request) { // Check for token in query parameters if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken)) @@ -206,7 +207,7 @@ public class DysonTokenAuthHandler( Type = TokenType.AuthKey }; } - + if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase)) { return new TokenInfo @@ -227,7 +228,7 @@ public class DysonTokenAuthHandler( } // Check for token in cookies - if (request.Cookies.TryGetValue(AuthConstants.TokenQueryParamName, out var cookieToken)) + if (request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out var cookieToken)) { return new TokenInfo { diff --git a/DysonNetwork.Sphere/Auth/AuthService.cs b/DysonNetwork.Sphere/Auth/AuthService.cs index 9dcc1f4..31ee36d 100644 --- a/DysonNetwork.Sphere/Auth/AuthService.cs +++ b/DysonNetwork.Sphere/Auth/AuthService.cs @@ -131,7 +131,7 @@ public class AuthService( case "google": content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); - response = await client.PostAsync("https://www.google.com/recaptcha/api/siteverify", content); + response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); response.EnsureSuccessStatusCode(); json = await response.Content.ReadAsStringAsync(); diff --git a/DysonNetwork.Sphere/Auth/OpenId/AfdianOidcService.cs b/DysonNetwork.Sphere/Auth/OpenId/AfdianOidcService.cs index 1e126fa..c0e0600 100644 --- a/DysonNetwork.Sphere/Auth/OpenId/AfdianOidcService.cs +++ b/DysonNetwork.Sphere/Auth/OpenId/AfdianOidcService.cs @@ -39,7 +39,7 @@ public class AfdianOidcService( return Task.FromResult(new OidcDiscoveryDocument { AuthorizationEndpoint = "https://afdian.com/oauth2/authorize", - TokenEndpoint = "https://afdian.com/api/oauth2/access_token", + TokenEndpoint = "https://afdian.com/oauth2/access_token", UserinfoEndpoint = null, JwksUri = null })!; @@ -60,7 +60,7 @@ public class AfdianOidcService( }); var client = HttpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/oauth2/access_token"); + var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/oauth2/access_token"); request.Content = content; var response = await client.SendAsync(request); diff --git a/DysonNetwork.Sphere/Auth/OpenId/DiscordOidcService.cs b/DysonNetwork.Sphere/Auth/OpenId/DiscordOidcService.cs index fccdc36..c710b71 100644 --- a/DysonNetwork.Sphere/Auth/OpenId/DiscordOidcService.cs +++ b/DysonNetwork.Sphere/Auth/OpenId/DiscordOidcService.cs @@ -30,7 +30,7 @@ public class DiscordOidcService( }; var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); - return $"https://discord.com/api/oauth2/authorize?{queryString}"; + return $"https://discord.com/oauth2/authorize?{queryString}"; } protected override Task GetDiscoveryDocumentAsync() @@ -38,8 +38,8 @@ public class DiscordOidcService( return Task.FromResult(new OidcDiscoveryDocument { AuthorizationEndpoint = "https://discord.com/oauth2/authorize", - TokenEndpoint = "https://discord.com/api/oauth2/token", - UserinfoEndpoint = "https://discord.com/api/users/@me", + TokenEndpoint = "https://discord.com/oauth2/token", + UserinfoEndpoint = "https://discord.com/users/@me", JwksUri = null })!; } @@ -75,7 +75,7 @@ public class DiscordOidcService( { "redirect_uri", config.RedirectUri }, }); - var response = await client.PostAsync("https://discord.com/api/oauth2/token", content); + var response = await client.PostAsync("https://discord.com/oauth2/token", content); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); @@ -84,7 +84,7 @@ public class DiscordOidcService( private async Task GetUserInfoAsync(string accessToken) { var client = HttpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/api/users/@me"); + var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/users/@me"); request.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(request); diff --git a/DysonNetwork.Sphere/Connection/WebReader/WebFeedController.cs b/DysonNetwork.Sphere/Connection/WebReader/WebFeedController.cs index 0db67fa..0359197 100644 --- a/DysonNetwork.Sphere/Connection/WebReader/WebFeedController.cs +++ b/DysonNetwork.Sphere/Connection/WebReader/WebFeedController.cs @@ -8,7 +8,7 @@ namespace DysonNetwork.Sphere.Connection.WebReader; [Authorize] [ApiController] -[Route("feeds")] +[Route("/feeds")] public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase { public class CreateWebFeedRequest diff --git a/DysonNetwork.Sphere/Discovery/DiscoveryController.cs b/DysonNetwork.Sphere/Discovery/DiscoveryController.cs index 5c9a7ea..baede36 100644 --- a/DysonNetwork.Sphere/Discovery/DiscoveryController.cs +++ b/DysonNetwork.Sphere/Discovery/DiscoveryController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; namespace DysonNetwork.Sphere.Discovery; [ApiController] -[Route("discovery")] +[Route("/discovery")] public class DiscoveryController(DiscoveryService discoveryService) : ControllerBase { [HttpGet("realms")] diff --git a/DysonNetwork.Sphere/Pages/Account/Profile.cshtml b/DysonNetwork.Sphere/Pages/Account/Profile.cshtml new file mode 100644 index 0000000..a303144 --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Account/Profile.cshtml @@ -0,0 +1,78 @@ +@page "/web/account/profile" +@model DysonNetwork.Sphere.Pages.Account.ProfileModel +@{ + ViewData["Title"] = "Profile"; +} + +
+
+

User Profile

+ + @if (Model.Account != null) + { +
+

Account Information

+

Username: @Model.Account.Name

+

Nickname: @Model.Account.Nick

+

Language: @Model.Account.Language

+

+ Activated: @Model.Account.ActivatedAt?.ToString("yyyy-MM-dd HH:mm", System.Globalization.CultureInfo.InvariantCulture) +

+

Superuser: @Model.Account.IsSuperuser +

+
+ +
+

Profile Details

+

+ Name: @Model.Account.Profile.FirstName @Model.Account.Profile.MiddleName @Model.Account.Profile.LastName +

+

Bio: @Model.Account.Profile.Bio

+

Gender: @Model.Account.Profile.Gender +

+

+ Location: @Model.Account.Profile.Location

+

+ Birthday: @Model.Account.Profile.Birthday?.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture) +

+

+ Experience: @Model.Account.Profile.Experience

+

Level: @Model.Account.Profile.Level +

+
+ +
+

Access Token

+
+ + +
+
+ +
+ +
+ } + else + { +

User profile not found. Please log in.

+ } +
+
+ + \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Account/Profile.cshtml.cs b/DysonNetwork.Sphere/Pages/Account/Profile.cshtml.cs new file mode 100644 index 0000000..1dbb86b --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Account/Profile.cshtml.cs @@ -0,0 +1,28 @@ +using DysonNetwork.Sphere.Auth; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace DysonNetwork.Sphere.Pages.Account; + +public class ProfileModel : PageModel +{ + public DysonNetwork.Sphere.Account.Account? Account { get; set; } + public string? AccessToken { get; set; } + + public Task OnGetAsync() + { + if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account currentUser) + return Task.FromResult(RedirectToPage("/Auth/Login")); + + Account = currentUser; + AccessToken = Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out var value) ? value : null; + + return Task.FromResult(Page()); + } + + public IActionResult OnPostLogout() + { + HttpContext.Response.Cookies.Delete(AuthConstants.CookieTokenName); + return RedirectToPage("/Auth/Login"); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml b/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml new file mode 100644 index 0000000..ebd9925 --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml @@ -0,0 +1,54 @@ +@page "/web/auth/challenge/{id:guid}" +@model DysonNetwork.Sphere.Pages.Auth.ChallengeModel +@{ + ViewData["Title"] = "Challenge"; +} + +
+
+

Authentication Challenge

+ + @if (Model.AuthChallenge == null) + { +

Challenge not found or expired.

+ } + else + { +

Remaining steps: @Model.AuthChallenge.StepRemain

+ + @if (Model.AuthChallenge.StepRemain > 0) + { +
+ +
+ + +
+
+ + + +
+ +
+ } + else + { +

Challenge completed. Redirecting...

+ } + } +
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs new file mode 100644 index 0000000..d5ddd13 --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs @@ -0,0 +1,185 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using System.ComponentModel.DataAnnotations; +using DysonNetwork.Sphere.Auth; +using DysonNetwork.Sphere.Account; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Sphere.Pages.Auth +{ + public class ChallengeModel( + AppDatabase db, + AccountService accounts, + AuthService auth, + ActionLogService als, + IConfiguration configuration + ) + : PageModel + { + [BindProperty(SupportsGet = true)] public Guid Id { get; set; } + + public Challenge? AuthChallenge { get; set; } + + [BindProperty] public Guid SelectedFactorId { get; set; } + + [BindProperty] [Required] public string Secret { get; set; } = string.Empty; + + public List AuthFactors { get; set; } = new(); + + public async Task OnGetAsync() + { + await LoadChallengeAndFactors(); + if (AuthChallenge == null) return NotFound(); + if (AuthChallenge.StepRemain == 0) return await ExchangeTokenAndRedirect(); + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + await LoadChallengeAndFactors(); + return Page(); + } + + var challenge = await db.AuthChallenges.Include(e => e.Account).FirstOrDefaultAsync(e => e.Id == Id); + if (challenge is null) return NotFound("Auth challenge was not found."); + + var factor = await db.AccountAuthFactors.FindAsync(SelectedFactorId); + if (factor is null) return NotFound("Auth factor was not found."); + if (factor.EnabledAt is null) return BadRequest("Auth factor is not enabled."); + if (factor.Trustworthy <= 0) return BadRequest("Auth factor is not trustworthy."); + + if (challenge.StepRemain == 0) return Page(); // Challenge already completed + if (challenge.ExpiredAt.HasValue && challenge.ExpiredAt.Value < Instant.FromDateTimeUtc(DateTime.UtcNow)) + { + ModelState.AddModelError(string.Empty, "Challenge expired."); + await LoadChallengeAndFactors(); + return Page(); + } + + try + { + if (await accounts.VerifyFactorCode(factor, Secret)) + { + challenge.StepRemain -= factor.Trustworthy; + challenge.StepRemain = Math.Max(0, challenge.StepRemain); + challenge.BlacklistFactors.Add(factor.Id); + db.Update(challenge); + als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, + new Dictionary + { + { "challenge_id", challenge.Id }, + { "factor_id", factor.Id } + }, Request, challenge.Account + ); + } + else + { + throw new ArgumentException("Invalid password."); + } + } + catch (Exception ex) + { + challenge.FailedAttempts++; + db.Update(challenge); + await db.SaveChangesAsync(); + als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure, + new Dictionary + { + { "challenge_id", challenge.Id }, + { "factor_id", factor.Id } + }, Request, challenge.Account + ); + ModelState.AddModelError(string.Empty, ex.Message); + await LoadChallengeAndFactors(); + return Page(); + } + + if (challenge.StepRemain == 0) + { + als.CreateActionLogFromRequest(ActionLogType.NewLogin, + new Dictionary + { + { "challenge_id", challenge.Id }, + { "account_id", challenge.AccountId } + }, Request, challenge.Account + ); + } + + await db.SaveChangesAsync(); + AuthChallenge = challenge; + + if (AuthChallenge.StepRemain == 0) + { + return await ExchangeTokenAndRedirect(); + } + + await LoadChallengeAndFactors(); + return Page(); + } + + private async Task LoadChallengeAndFactors() + { + var challenge = await db.AuthChallenges + .Include(e => e.Account) + .ThenInclude(e => e.AuthFactors) + .FirstOrDefaultAsync(e => e.Id == Id); + + AuthChallenge = challenge; + + if (AuthChallenge != null) + { + var factorsResponse = AuthChallenge.Account.AuthFactors + .Where(e => e is { EnabledAt: not null, Trustworthy: >= 1 }) + .ToList(); + + AuthFactors = factorsResponse.Select(f => new SelectListItem + { + Value = f.Id.ToString(), + Text = f.Type.ToString() // You might want a more user-friendly display for factor types + }).ToList(); + } + } + + private async Task ExchangeTokenAndRedirect() + { + var challenge = await db.AuthChallenges + .Include(e => e.Account) + .Where(e => e.Id == Id) + .FirstOrDefaultAsync(); + + if (challenge is null) return BadRequest("Authorization code not found or expired."); + if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed."); + + var session = await db.AuthSessions + .Where(e => e.Challenge == challenge) + .FirstOrDefaultAsync(); + + if (session is not null) return BadRequest("Session already exists for this challenge."); + + session = new Session + { + LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), + ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), + Account = challenge.Account, + Challenge = challenge, + }; + + db.AuthSessions.Add(session); + await db.SaveChangesAsync(); + + var tk = auth.CreateToken(session); + HttpContext.Response.Cookies.Append(AuthConstants.CookieTokenName, tk, new CookieOptions() + { + HttpOnly = true, + Secure = !configuration.GetValue("Debug"), + SameSite = SameSiteMode.Strict, + Path = "/" + }); + return RedirectToPage("/Account/Profile"); // Redirect to profile page + } + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/Login.cshtml b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml new file mode 100644 index 0000000..1d693a3 --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml @@ -0,0 +1,29 @@ +@page "/web/auth/login" +@model DysonNetwork.Sphere.Pages.Auth.LoginModel +@{ + ViewData["Title"] = "Login"; +} + +
+
+

Login

+ +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs new file mode 100644 index 0000000..b49ad6f --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; +using DysonNetwork.Sphere.Auth; +using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Connection; +using NodaTime; +using Microsoft.EntityFrameworkCore; + +namespace DysonNetwork.Sphere.Pages.Auth +{ + public class LoginModel( + AppDatabase db, + AccountService accounts, + AuthService auth, + GeoIpService geo, + ActionLogService als + ) : PageModel + { + [BindProperty] [Required] public string Username { get; set; } = string.Empty; + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var account = await accounts.LookupAccount(Username); + if (account is null) + { + ModelState.AddModelError(string.Empty, "Account was not found."); + return Page(); + } + + var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); + var userAgent = HttpContext.Request.Headers.UserAgent.ToString(); + var now = Instant.FromDateTimeUtc(DateTime.UtcNow); + + var existingChallenge = await db.AuthChallenges + .Where(e => e.Account == account) + .Where(e => e.IpAddress == ipAddress) + .Where(e => e.UserAgent == userAgent) + .Where(e => e.StepRemain > 0) + .Where(e => e.ExpiredAt != null && now < e.ExpiredAt) + .FirstOrDefaultAsync(); + + if (existingChallenge is not null) + { + return RedirectToPage("Challenge", new { id = existingChallenge.Id }); + } + + var challenge = new Challenge + { + ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)), + StepTotal = await auth.DetectChallengeRisk(Request, account), + Platform = ChallengePlatform.Web, + Audiences = new List(), + Scopes = new List(), + IpAddress = ipAddress, + UserAgent = userAgent, + Location = geo.GetPointFromIp(ipAddress), + DeviceId = "web-browser", + AccountId = account.Id + }.Normalize(); + + await db.AuthChallenges.AddAsync(challenge); + await db.SaveChangesAsync(); + + als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt, + new Dictionary { { "challenge_id", challenge.Id } }, Request, account + ); + + return RedirectToPage("Challenge", new { id = challenge.Id }); + } + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Shared/_Layout.cshtml b/DysonNetwork.Sphere/Pages/Shared/_Layout.cshtml index ca87193..7092162 100644 --- a/DysonNetwork.Sphere/Pages/Shared/_Layout.cshtml +++ b/DysonNetwork.Sphere/Pages/Shared/_Layout.cshtml @@ -1,3 +1,4 @@ +@using DysonNetwork.Sphere.Auth @@ -13,6 +14,19 @@ +
+ @if (Context.Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out _)) + { + Profile +
+ +
+ } + else + { + Login + } +
diff --git a/DysonNetwork.Sphere/Pages/Shared/_ValidationScriptsPartial.cshtml b/DysonNetwork.Sphere/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..e36029c --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs index 8b9fc76..08c7c67 100644 --- a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs @@ -17,7 +17,7 @@ public static class ApplicationConfiguration app.UseSwagger(); app.UseSwaggerUI(); - + app.UseRequestLocalization(); ConfigureForwardedHeaders(app, configuration); diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index 9f66050..7a560f6 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -194,7 +194,8 @@ public static class ServiceCollectionExtensions return services; } - public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, + IConfiguration configuration) { services.AddScoped(); services.AddScoped(); @@ -236,4 +237,4 @@ public static class ServiceCollectionExtensions return services; } -} +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Wallet/PaymentHandlers/AfdianPaymentHandler.cs b/DysonNetwork.Sphere/Wallet/PaymentHandlers/AfdianPaymentHandler.cs index b265d99..13d96fc 100644 --- a/DysonNetwork.Sphere/Wallet/PaymentHandlers/AfdianPaymentHandler.cs +++ b/DysonNetwork.Sphere/Wallet/PaymentHandlers/AfdianPaymentHandler.cs @@ -51,7 +51,7 @@ public class AfdianPaymentHandler( var sign = CalculateSign(token, userId, paramsJson, ts); var client = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/open/query-order") + var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/open/query-order") { Content = new StringContent(JsonSerializer.Serialize(new { @@ -107,7 +107,7 @@ public class AfdianPaymentHandler( var sign = CalculateSign(token, userId, paramsJson, ts); var client = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/open/query-order") + var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/open/query-order") { Content = new StringContent(JsonSerializer.Serialize(new { @@ -176,7 +176,7 @@ public class AfdianPaymentHandler( var sign = CalculateSign(token, userId, paramsJson, ts); var client = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/open/query-order") + var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/open/query-order") { Content = new StringContent(JsonSerializer.Serialize(new { diff --git a/DysonNetwork.Sphere/wwwroot/css/styles.css b/DysonNetwork.Sphere/wwwroot/css/styles.css index 7b65a38..8fac2eb 100644 --- a/DysonNetwork.Sphere/wwwroot/css/styles.css +++ b/DysonNetwork.Sphere/wwwroot/css/styles.css @@ -1,4 +1,4 @@ -/*! tailwindcss v4.1.10 | MIT License | https://tailwindcss.com */ +/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ @layer properties; @layer theme, base, components, utilities; @layer theme { @@ -7,12 +7,17 @@ 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-700: oklch(50.5% 0.213 27.518); --color-yellow-100: oklch(97.3% 0.071 103.193); --color-yellow-200: oklch(94.5% 0.129 101.54); --color-yellow-800: oklch(47.6% 0.114 61.907); --color-yellow-900: oklch(42.1% 0.095 57.708); --color-green-100: oklch(96.2% 0.044 156.743); --color-green-200: oklch(92.5% 0.084 155.995); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-600: oklch(62.7% 0.194 149.214); --color-green-800: oklch(44.8% 0.119 151.328); --color-green-900: oklch(39.3% 0.095 152.535); --color-blue-400: oklch(70.7% 0.165 254.624); @@ -20,6 +25,7 @@ --color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-700: oklch(48.8% 0.243 264.376); --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); --color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-400: oklch(70.7% 0.022 261.325); --color-gray-500: oklch(55.1% 0.027 264.364); @@ -29,6 +35,7 @@ --color-gray-900: oklch(21% 0.034 264.665); --color-white: #fff; --spacing: 0.25rem; + --container-md: 28rem; --container-lg: 32rem; --container-2xl: 42rem; --container-7xl: 80rem; @@ -50,6 +57,7 @@ --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-tight: -0.025em; + --radius-md: 0.375rem; --radius-lg: 0.5rem; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -206,6 +214,9 @@ .invisible { visibility: hidden; } + .absolute { + position: absolute; + } .fixed { position: fixed; } @@ -248,6 +259,9 @@ .mx-auto { margin-inline: auto; } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } .mt-4 { margin-top: calc(var(--spacing) * 4); } @@ -278,6 +292,12 @@ .mb-8 { margin-bottom: calc(var(--spacing) * 8); } + .ml-4 { + margin-left: calc(var(--spacing) * 4); + } + .ml-auto { + margin-left: auto; + } .block { display: block; } @@ -290,6 +310,9 @@ .hidden { display: none; } + .inline { + display: inline; + } .table { display: table; } @@ -302,6 +325,9 @@ .h-full { height: 100%; } + .min-h-screen { + min-height: 100vh; + } .w-full { width: 100%; } @@ -311,6 +337,12 @@ .max-w-lg { max-width: var(--container-lg); } + .max-w-md { + max-width: var(--container-md); + } + .flex-grow { + flex-grow: 1; + } .border-collapse { border-collapse: collapse; } @@ -332,9 +364,17 @@ .gap-x-6 { column-gap: calc(var(--spacing) * 6); } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .rounded-lg { border-radius: var(--radius-lg); } + .rounded-md { + border-radius: var(--radius-md); + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -349,12 +389,18 @@ .bg-blue-500 { background-color: var(--color-blue-500); } + .bg-blue-600 { + background-color: var(--color-blue-600); + } .bg-gray-100 { background-color: var(--color-gray-100); } .bg-green-100 { background-color: var(--color-green-100); } + .bg-red-600 { + background-color: var(--color-red-600); + } .bg-white { background-color: var(--color-white); } @@ -367,12 +413,21 @@ .p-6 { padding: calc(var(--spacing) * 6); } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } .px-4 { padding-inline: calc(var(--spacing) * 4); } .px-6 { padding-inline: calc(var(--spacing) * 6); } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } .py-3 { padding-block: calc(var(--spacing) * 3); } @@ -435,12 +490,21 @@ .text-gray-700 { color: var(--color-gray-700); } + .text-gray-800 { + color: var(--color-gray-800); + } .text-gray-900 { color: var(--color-gray-900); } + .text-green-600 { + color: var(--color-green-600); + } .text-green-800 { color: var(--color-green-800); } + .text-red-500 { + color: var(--color-red-500); + } .text-white { color: var(--color-white); } @@ -464,6 +528,10 @@ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-sm { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -497,6 +565,20 @@ } } } + .hover\:bg-blue-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-700); + } + } + } + .hover\:bg-red-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-700); + } + } + } .hover\:text-blue-600 { &:hover { @media (hover: hover) { @@ -504,6 +586,24 @@ } } } + .hover\:text-gray-700 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-700); + } + } + } + .focus\:border-blue-500 { + &:focus { + border-color: var(--color-blue-500); + } + } + .focus\:ring { + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } .focus\:ring-2 { &:focus { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); @@ -515,6 +615,16 @@ --tw-ring-color: var(--color-blue-400); } } + .focus\:ring-blue-500 { + &:focus { + --tw-ring-color: var(--color-blue-500); + } + } + .focus\:ring-red-500 { + &:focus { + --tw-ring-color: var(--color-red-500); + } + } .focus\:outline-none { &:focus { --tw-outline-style: none; @@ -532,6 +642,11 @@ border-color: var(--color-gray-600); } } + .dark\:bg-gray-700 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-gray-700); + } + } .dark\:bg-gray-800 { @media (prefers-color-scheme: dark) { background-color: var(--color-gray-800); @@ -552,6 +667,11 @@ background-color: var(--color-yellow-900); } } + .dark\:text-gray-200 { + @media (prefers-color-scheme: dark) { + color: var(--color-gray-200); + } + } .dark\:text-gray-300 { @media (prefers-color-scheme: dark) { color: var(--color-gray-300); @@ -567,6 +687,11 @@ color: var(--color-green-200); } } + .dark\:text-green-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-green-400); + } + } .dark\:text-white { @media (prefers-color-scheme: dark) { color: var(--color-white); @@ -586,6 +711,15 @@ } } } + .dark\:hover\:text-gray-300 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + color: var(--color-gray-300); + } + } + } + } } @layer theme, base, components, utilities; @layer theme; diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 23b3a09..e79a76b 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -10,7 +10,9 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded