Compare commits
4 Commits
a5dae37525
...
ee5d6ef821
Author | SHA1 | Date | |
---|---|---|---|
ee5d6ef821 | |||
d7b443e678 | |||
98b2eeb13d | |||
ec3961d546 |
@ -174,13 +174,4 @@ public class AccountController(
|
|||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/maintenance/ensureProfileCreated")]
|
|
||||||
[Authorize]
|
|
||||||
[RequiredPermission("maintenance", "accounts.profiles")]
|
|
||||||
public async Task<ActionResult> EnsureProfileCreated()
|
|
||||||
{
|
|
||||||
await accounts.EnsureAccountProfileCreated();
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -288,7 +288,7 @@ public class NotificationService(
|
|||||||
|
|
||||||
var client = httpFactory.CreateClient();
|
var client = httpFactory.CreateClient();
|
||||||
client.BaseAddress = _notifyEndpoint;
|
client.BaseAddress = _notifyEndpoint;
|
||||||
var request = await client.PostAsync("/api/push", new StringContent(
|
var request = await client.PostAsync("/push", new StringContent(
|
||||||
JsonSerializer.Serialize(requestDict),
|
JsonSerializer.Serialize(requestDict),
|
||||||
Encoding.UTF8,
|
Encoding.UTF8,
|
||||||
"application/json"
|
"application/json"
|
||||||
|
@ -15,6 +15,7 @@ public static class AuthConstants
|
|||||||
{
|
{
|
||||||
public const string SchemeName = "DysonToken";
|
public const string SchemeName = "DysonToken";
|
||||||
public const string TokenQueryParamName = "tk";
|
public const string TokenQueryParamName = "tk";
|
||||||
|
public const string CookieTokenName = "AuthToken";
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TokenType
|
public enum TokenType
|
||||||
@ -182,7 +183,7 @@ public class DysonTokenAuthHandler(
|
|||||||
return Convert.FromBase64String(padded);
|
return Convert.FromBase64String(padded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TokenInfo? _ExtractToken(HttpRequest request)
|
private TokenInfo? _ExtractToken(HttpRequest request)
|
||||||
{
|
{
|
||||||
// Check for token in query parameters
|
// Check for token in query parameters
|
||||||
if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken))
|
if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken))
|
||||||
@ -227,7 +228,7 @@ public class DysonTokenAuthHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for token in cookies
|
// 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
|
return new TokenInfo
|
||||||
{
|
{
|
||||||
|
@ -131,7 +131,7 @@ public class AuthService(
|
|||||||
case "google":
|
case "google":
|
||||||
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||||
"application/x-www-form-urlencoded");
|
"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();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
json = await response.Content.ReadAsStringAsync();
|
json = await response.Content.ReadAsStringAsync();
|
||||||
|
@ -39,7 +39,7 @@ public class AfdianOidcService(
|
|||||||
return Task.FromResult(new OidcDiscoveryDocument
|
return Task.FromResult(new OidcDiscoveryDocument
|
||||||
{
|
{
|
||||||
AuthorizationEndpoint = "https://afdian.com/oauth2/authorize",
|
AuthorizationEndpoint = "https://afdian.com/oauth2/authorize",
|
||||||
TokenEndpoint = "https://afdian.com/api/oauth2/access_token",
|
TokenEndpoint = "https://afdian.com/oauth2/access_token",
|
||||||
UserinfoEndpoint = null,
|
UserinfoEndpoint = null,
|
||||||
JwksUri = null
|
JwksUri = null
|
||||||
})!;
|
})!;
|
||||||
@ -60,7 +60,7 @@ public class AfdianOidcService(
|
|||||||
});
|
});
|
||||||
|
|
||||||
var client = HttpClientFactory.CreateClient();
|
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;
|
request.Content = content;
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
@ -30,7 +30,7 @@ public class DiscordOidcService(
|
|||||||
};
|
};
|
||||||
|
|
||||||
var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}"));
|
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<OidcDiscoveryDocument?> GetDiscoveryDocumentAsync()
|
protected override Task<OidcDiscoveryDocument?> GetDiscoveryDocumentAsync()
|
||||||
@ -38,8 +38,8 @@ public class DiscordOidcService(
|
|||||||
return Task.FromResult(new OidcDiscoveryDocument
|
return Task.FromResult(new OidcDiscoveryDocument
|
||||||
{
|
{
|
||||||
AuthorizationEndpoint = "https://discord.com/oauth2/authorize",
|
AuthorizationEndpoint = "https://discord.com/oauth2/authorize",
|
||||||
TokenEndpoint = "https://discord.com/api/oauth2/token",
|
TokenEndpoint = "https://discord.com/oauth2/token",
|
||||||
UserinfoEndpoint = "https://discord.com/api/users/@me",
|
UserinfoEndpoint = "https://discord.com/users/@me",
|
||||||
JwksUri = null
|
JwksUri = null
|
||||||
})!;
|
})!;
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ public class DiscordOidcService(
|
|||||||
{ "redirect_uri", config.RedirectUri },
|
{ "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();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<OidcTokenResponse>();
|
return await response.Content.ReadFromJsonAsync<OidcTokenResponse>();
|
||||||
@ -84,7 +84,7 @@ public class DiscordOidcService(
|
|||||||
private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken)
|
private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken)
|
||||||
{
|
{
|
||||||
var client = HttpClientFactory.CreateClient();
|
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}");
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
@ -8,7 +8,7 @@ namespace DysonNetwork.Sphere.Connection.WebReader;
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("feeds")]
|
[Route("/feeds")]
|
||||||
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase
|
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase
|
||||||
{
|
{
|
||||||
public class CreateWebFeedRequest
|
public class CreateWebFeedRequest
|
||||||
|
@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace DysonNetwork.Sphere.Discovery;
|
namespace DysonNetwork.Sphere.Discovery;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("discovery")]
|
[Route("/discovery")]
|
||||||
public class DiscoveryController(DiscoveryService discoveryService) : ControllerBase
|
public class DiscoveryController(DiscoveryService discoveryService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("realms")]
|
[HttpGet("realms")]
|
||||||
|
78
DysonNetwork.Sphere/Pages/Account/Profile.cshtml
Normal file
78
DysonNetwork.Sphere/Pages/Account/Profile.cshtml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
@page "/web/account/profile"
|
||||||
|
@model DysonNetwork.Sphere.Pages.Account.ProfileModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="h-full flex items-center justify-center bg-gray-100 dark:bg-gray-900 py-12">
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md w-full max-w-2xl">
|
||||||
|
<h1 class="text-3xl font-bold text-center text-gray-900 dark:text-white mb-8">User Profile</h1>
|
||||||
|
|
||||||
|
@if (Model.Account != null)
|
||||||
|
{
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4">Account Information</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Username:</strong> @Model.Account.Name</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Nickname:</strong> @Model.Account.Nick</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Language:</strong> @Model.Account.Language</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<strong>Activated:</strong> @Model.Account.ActivatedAt?.ToString("yyyy-MM-dd HH:mm", System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Superuser:</strong> @Model.Account.IsSuperuser
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4">Profile Details</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<strong>Name:</strong> @Model.Account.Profile.FirstName @Model.Account.Profile.MiddleName @Model.Account.Profile.LastName
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Bio:</strong> @Model.Account.Profile.Bio</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Gender:</strong> @Model.Account.Profile.Gender
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<strong>Location:</strong> @Model.Account.Profile.Location</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<strong>Birthday:</strong> @Model.Account.Profile.Birthday?.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<strong>Experience:</strong> @Model.Account.Profile.Experience</p>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-2"><strong>Level:</strong> @Model.Account.Profile.Level
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4">Access Token</h2>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="text" id="accessToken" value="@Model.AccessToken" readonly
|
||||||
|
class="form-input flex-grow rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"/>
|
||||||
|
<button onclick="copyAccessToken()"
|
||||||
|
class="ml-4 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" asp-page-handler="Logout" class="text-center">
|
||||||
|
<button type="submit"
|
||||||
|
class="bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="text-red-500 text-center">User profile not found. Please log in.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyAccessToken() {
|
||||||
|
var copyText = document.getElementById("accessToken");
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||||||
|
document.execCommand("copy");
|
||||||
|
alert("Access Token copied to clipboard!");
|
||||||
|
}
|
||||||
|
</script>
|
28
DysonNetwork.Sphere/Pages/Account/Profile.cshtml.cs
Normal file
28
DysonNetwork.Sphere/Pages/Account/Profile.cshtml.cs
Normal file
@ -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<IActionResult> OnGetAsync()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account currentUser)
|
||||||
|
return Task.FromResult<IActionResult>(RedirectToPage("/Auth/Login"));
|
||||||
|
|
||||||
|
Account = currentUser;
|
||||||
|
AccessToken = Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out var value) ? value : null;
|
||||||
|
|
||||||
|
return Task.FromResult<IActionResult>(Page());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OnPostLogout()
|
||||||
|
{
|
||||||
|
HttpContext.Response.Cookies.Delete(AuthConstants.CookieTokenName);
|
||||||
|
return RedirectToPage("/Auth/Login");
|
||||||
|
}
|
||||||
|
}
|
13
DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml
Normal file
13
DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
@page "/web/auth/challenge/{id:guid}"
|
||||||
|
@model DysonNetwork.Sphere.Pages.Auth.ChallengeModel
|
||||||
|
@{
|
||||||
|
// This page is kept for backward compatibility
|
||||||
|
// It will automatically redirect to the new SelectFactor page
|
||||||
|
Response.Redirect($"/web/auth/challenge/{Model.Id}/select-factor");
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="h-full flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md w-full max-w-md text-center">
|
||||||
|
<p>Redirecting to authentication page...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
16
DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs
Normal file
16
DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Pages.Auth
|
||||||
|
{
|
||||||
|
public class ChallengeModel() : PageModel
|
||||||
|
{
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public IActionResult OnGet()
|
||||||
|
{
|
||||||
|
return RedirectToPage("SelectFactor", new { id = Id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
DysonNetwork.Sphere/Pages/Auth/Login.cshtml
Normal file
29
DysonNetwork.Sphere/Pages/Auth/Login.cshtml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@page "/web/auth/login"
|
||||||
|
@model DysonNetwork.Sphere.Pages.Auth.LoginModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Login";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="h-full flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md w-full max-w-md">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-900 dark:text-white mb-6">Login</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label asp-for="Username"
|
||||||
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"></label>
|
||||||
|
<input asp-for="Username"
|
||||||
|
class="form-input mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white px-4 py-2"/>
|
||||||
|
<span asp-validation-for="Username" class="text-red-500 text-sm mt-1"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||||
|
}
|
81
DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs
Normal file
81
DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs
Normal file
@ -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<IActionResult> 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<string>(),
|
||||||
|
Scopes = new List<string>(),
|
||||||
|
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<string, object> { { "challenge_id", challenge.Id } }, Request, account
|
||||||
|
);
|
||||||
|
|
||||||
|
return RedirectToPage("Challenge", new { id = challenge.Id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml
Normal file
81
DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
@page "/web/auth/challenge/{id:guid}/select-factor"
|
||||||
|
@using DysonNetwork.Sphere.Account
|
||||||
|
@model DysonNetwork.Sphere.Pages.Auth.SelectFactorModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Select Authentication Method";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="h-full flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md w-full max-w-md">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-900 dark:text-white mb-6">Select Authentication Method</h1>
|
||||||
|
|
||||||
|
@if (Model.AuthChallenge == null)
|
||||||
|
{
|
||||||
|
<p class="text-red-500 text-center">Challenge not found or expired.</p>
|
||||||
|
}
|
||||||
|
else if (Model.AuthChallenge.StepRemain == 0)
|
||||||
|
{
|
||||||
|
<p class="text-green-600 dark:text-green-400 text-center">Challenge completed. Redirecting...</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">Please select an authentication method:</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
@foreach (var factor in Model.AuthFactors)
|
||||||
|
{
|
||||||
|
<div class="mb-4">
|
||||||
|
<form method="post" asp-page-handler="SelectFactor" class="w-full" id="factor-@factor.Id">
|
||||||
|
<input type="hidden" name="factorId" value="@factor.Id"/>
|
||||||
|
|
||||||
|
@if (factor.Type == AccountAuthFactorType.EmailCode)
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="hint-@factor.Id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Email to send code to
|
||||||
|
</label>
|
||||||
|
<input type="email"
|
||||||
|
id="hint-@factor.Id"
|
||||||
|
name="hint"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full text-left p-4 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors">
|
||||||
|
<div class="font-medium text-gray-900 dark:text-white">@GetFactorDisplayName(factor.Type)</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">@GetFactorDescription(factor.Type)</div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
|
||||||
|
private string GetFactorDisplayName(AccountAuthFactorType type) => type switch
|
||||||
|
{
|
||||||
|
AccountAuthFactorType.InAppCode => "Authenticator App",
|
||||||
|
AccountAuthFactorType.EmailCode => "Email",
|
||||||
|
AccountAuthFactorType.TimedCode => "Timed Code",
|
||||||
|
AccountAuthFactorType.PinCode => "PIN Code",
|
||||||
|
AccountAuthFactorType.Password => "Password",
|
||||||
|
_ => type.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetFactorDescription(AccountAuthFactorType type) => type switch
|
||||||
|
{
|
||||||
|
AccountAuthFactorType.InAppCode => "Enter a code from your authenticator app",
|
||||||
|
AccountAuthFactorType.EmailCode => "Receive a verification code via email",
|
||||||
|
AccountAuthFactorType.TimedCode => "Use a time-based verification code",
|
||||||
|
AccountAuthFactorType.PinCode => "Enter your PIN code",
|
||||||
|
AccountAuthFactorType.Password => "Enter your password",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
87
DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml.cs
Normal file
87
DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Pages.Auth;
|
||||||
|
|
||||||
|
public class SelectFactorModel(
|
||||||
|
AppDatabase db,
|
||||||
|
AccountService accounts
|
||||||
|
)
|
||||||
|
: PageModel
|
||||||
|
{
|
||||||
|
[BindProperty(SupportsGet = true)] public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Challenge? AuthChallenge { get; set; }
|
||||||
|
public List<AccountAuthFactor> AuthFactors { get; set; } = [];
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync()
|
||||||
|
{
|
||||||
|
await LoadChallengeAndFactors();
|
||||||
|
if (AuthChallenge == null) return NotFound();
|
||||||
|
if (AuthChallenge.StepRemain == 0) return await ExchangeTokenAndRedirect();
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostSelectFactorAsync(Guid factorId, string? hint = null)
|
||||||
|
{
|
||||||
|
var challenge = await db.AuthChallenges
|
||||||
|
.Include(e => e.Account)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||||
|
|
||||||
|
if (challenge == null) return NotFound();
|
||||||
|
|
||||||
|
var factor = await db.AccountAuthFactors.FindAsync(factorId);
|
||||||
|
if (factor?.EnabledAt == null || factor.Trustworthy <= 0)
|
||||||
|
return BadRequest("Invalid authentication method.");
|
||||||
|
|
||||||
|
// For OTP factors that require code delivery
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate hint for factors that require it
|
||||||
|
if (factor.Type == AccountAuthFactorType.EmailCode
|
||||||
|
&& string.IsNullOrWhiteSpace(hint))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, $"Please provide a {factor.Type.ToString().ToLower().Replace("code", "")} to send the code to.");
|
||||||
|
await LoadChallengeAndFactors();
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
await accounts.SendFactorCode(challenge.Account, factor, hint);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, $"An error occurred while sending the verification code: {ex.Message}");
|
||||||
|
await LoadChallengeAndFactors();
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to verify the page with the selected factor
|
||||||
|
return RedirectToPage("VerifyFactor", new { id = Id, factorId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadChallengeAndFactors()
|
||||||
|
{
|
||||||
|
AuthChallenge = await db.AuthChallenges
|
||||||
|
.Include(e => e.Account)
|
||||||
|
.ThenInclude(e => e.AuthFactors)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||||
|
|
||||||
|
if (AuthChallenge != null)
|
||||||
|
{
|
||||||
|
AuthFactors = AuthChallenge.Account.AuthFactors
|
||||||
|
.Where(e => e is { EnabledAt: not null, Trustworthy: >= 1 })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> ExchangeTokenAndRedirect()
|
||||||
|
{
|
||||||
|
// This method is kept for backward compatibility
|
||||||
|
// The actual token exchange is now handled in the VerifyFactor page
|
||||||
|
await Task.CompletedTask; // Add this to fix the async warning
|
||||||
|
return RedirectToPage("/Account/Profile");
|
||||||
|
}
|
||||||
|
}
|
76
DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml
Normal file
76
DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
@page "/web/auth/challenge/{id:guid}/verify/{factorId:guid}"
|
||||||
|
@using DysonNetwork.Sphere.Account
|
||||||
|
@model DysonNetwork.Sphere.Pages.Auth.VerifyFactorModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Verify Your Identity";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="h-full flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||||
|
<div class="bg-white dark:bg-gray-800 px-8 pt-8 pb-4 rounded-lg shadow-md w-full max-w-md">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-900 dark:text-white mb-2">Verify Your Identity</h1>
|
||||||
|
<p class="text-center text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
@switch (Model.FactorType)
|
||||||
|
{
|
||||||
|
case AccountAuthFactorType.EmailCode:
|
||||||
|
<span>We've sent a verification code to your email.</span>
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.InAppCode:
|
||||||
|
<span>Enter the code from your authenticator app.</span>
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.TimedCode:
|
||||||
|
<span>Enter your time-based verification code.</span>
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.PinCode:
|
||||||
|
<span>Enter your PIN code.</span>
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.Password:
|
||||||
|
<span>Enter your password.</span>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<span>Please verify your identity.</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (Model.AuthChallenge == null)
|
||||||
|
{
|
||||||
|
<p class="text-red-500 text-center">Challenge not found or expired.</p>
|
||||||
|
}
|
||||||
|
else if (Model.AuthChallenge.StepRemain == 0)
|
||||||
|
{
|
||||||
|
<p class="text-green-600 dark:text-green-400 text-center">Verification successful. Redirecting...</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form method="post" class="space-y-4">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-red-500 text-sm"></div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label asp-for="Code" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
@(Model.FactorType == AccountAuthFactorType.Password ? "Use your password" : "Verification Code")
|
||||||
|
</label>
|
||||||
|
<input asp-for="Code"
|
||||||
|
class="form-input mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white px-4 py-2"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
autofocus />
|
||||||
|
<span asp-validation-for="Code" class="text-red-500 text-sm mt-1"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
|
||||||
|
Verify
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<a asp-page="SelectFactor" asp-route-id="@Model.Id" class="text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
← Back to authentication methods
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||||
|
}
|
187
DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml.cs
Normal file
187
DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml.cs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Pages.Auth
|
||||||
|
{
|
||||||
|
public class VerifyFactorModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly AppDatabase _db;
|
||||||
|
private readonly AccountService _accounts;
|
||||||
|
private readonly AuthService _auth;
|
||||||
|
private readonly ActionLogService _als;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public Guid FactorId { get; set; }
|
||||||
|
|
||||||
|
[BindProperty, Required]
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Challenge? AuthChallenge { get; set; }
|
||||||
|
public AccountAuthFactor? Factor { get; set; }
|
||||||
|
public AccountAuthFactorType FactorType => Factor?.Type ?? AccountAuthFactorType.EmailCode;
|
||||||
|
|
||||||
|
public VerifyFactorModel(
|
||||||
|
AppDatabase db,
|
||||||
|
AccountService accounts,
|
||||||
|
AuthService auth,
|
||||||
|
ActionLogService als,
|
||||||
|
IConfiguration configuration,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_accounts = accounts;
|
||||||
|
_auth = auth;
|
||||||
|
_als = als;
|
||||||
|
_configuration = configuration;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync()
|
||||||
|
{
|
||||||
|
await LoadChallengeAndFactor();
|
||||||
|
if (AuthChallenge == null) return NotFound("Challenge not found or expired.");
|
||||||
|
if (Factor == null) return NotFound("Authentication method not found.");
|
||||||
|
if (AuthChallenge.StepRemain == 0) return await ExchangeTokenAndRedirect();
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync()
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await LoadChallengeAndFactor();
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadChallengeAndFactor();
|
||||||
|
if (AuthChallenge == null) return NotFound("Challenge not found or expired.");
|
||||||
|
if (Factor == null) return NotFound("Authentication method not found.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await _accounts.VerifyFactorCode(Factor, Code))
|
||||||
|
{
|
||||||
|
AuthChallenge.StepRemain -= Factor.Trustworthy;
|
||||||
|
AuthChallenge.StepRemain = Math.Max(0, AuthChallenge.StepRemain);
|
||||||
|
AuthChallenge.BlacklistFactors.Add(Factor.Id);
|
||||||
|
_db.Update(AuthChallenge);
|
||||||
|
|
||||||
|
_als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "challenge_id", AuthChallenge.Id },
|
||||||
|
{ "factor_id", Factor?.Id.ToString() ?? string.Empty }
|
||||||
|
}, Request, AuthChallenge.Account);
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (AuthChallenge.StepRemain == 0)
|
||||||
|
{
|
||||||
|
_als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "challenge_id", AuthChallenge.Id },
|
||||||
|
{ "account_id", AuthChallenge.AccountId }
|
||||||
|
}, Request, AuthChallenge.Account);
|
||||||
|
|
||||||
|
return await ExchangeTokenAndRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If more steps are needed, redirect back to select factor
|
||||||
|
return RedirectToPage("SelectFactor", new { id = Id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Invalid verification code.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (AuthChallenge != null)
|
||||||
|
{
|
||||||
|
AuthChallenge.FailedAttempts++;
|
||||||
|
_db.Update(AuthChallenge);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "challenge_id", AuthChallenge.Id },
|
||||||
|
{ "factor_id", Factor?.Id.ToString() ?? string.Empty }
|
||||||
|
}, Request, AuthChallenge.Account);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ModelState.AddModelError(string.Empty, ex.Message);
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadChallengeAndFactor()
|
||||||
|
{
|
||||||
|
AuthChallenge = await _db.AuthChallenges
|
||||||
|
.Include(e => e.Account)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||||
|
|
||||||
|
if (AuthChallenge?.Account != null)
|
||||||
|
{
|
||||||
|
Factor = await _db.AccountAuthFactors
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == FactorId &&
|
||||||
|
e.AccountId == AuthChallenge.Account.Id &&
|
||||||
|
e.EnabledAt != null &&
|
||||||
|
e.Trustworthy > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> ExchangeTokenAndRedirect()
|
||||||
|
{
|
||||||
|
var challenge = await _db.AuthChallenges
|
||||||
|
.Include(e => e.Account)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||||
|
|
||||||
|
if (challenge == null) return BadRequest("Authorization code not found or expired.");
|
||||||
|
if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed.");
|
||||||
|
|
||||||
|
var session = await _db.AuthSessions
|
||||||
|
.FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id);
|
||||||
|
|
||||||
|
if (session != 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 token = _auth.CreateToken(session);
|
||||||
|
Response.Cookies.Append(AuthConstants.CookieTokenName, token, new()
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
Secure = !_configuration.GetValue<bool>("Debug"),
|
||||||
|
SameSite = SameSiteMode.Strict,
|
||||||
|
Path = "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToPage("/Account/Profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
@using DysonNetwork.Sphere.Auth
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="h-full">
|
<html lang="en" class="h-full">
|
||||||
<head>
|
<head>
|
||||||
@ -13,6 +14,19 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white">Solar Network</a>
|
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white">Solar Network</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center ml-auto">
|
||||||
|
@if (Context.Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out _))
|
||||||
|
{
|
||||||
|
<a href="/Account/Profile" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 px-3 py-2 rounded-md text-sm font-medium">Profile</a>
|
||||||
|
<form method="post" asp-page="/Account/Profile" asp-page-handler="Logout" class="inline">
|
||||||
|
<button type="submit" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 px-3 py-2 rounded-md text-sm font-medium">Logout</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/web/auth/login" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 px-3 py-2 rounded-md text-sm font-medium">Login</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||||
|
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
@ -216,13 +216,6 @@ public class PostController(
|
|||||||
return BadRequest(err.Message);
|
return BadRequest(err.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
using var scope = factory.CreateScope();
|
|
||||||
var subs = scope.ServiceProvider.GetRequiredService<PublisherSubscriptionService>();
|
|
||||||
await subs.NotifySubscriberPost(post);
|
|
||||||
});
|
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(
|
als.CreateActionLogFromRequest(
|
||||||
ActionLogType.PostCreate,
|
ActionLogType.PostCreate,
|
||||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||||
|
@ -69,7 +69,7 @@ public class PublisherSubscriptionService(
|
|||||||
|
|
||||||
// Notify each subscriber
|
// Notify each subscriber
|
||||||
var notifiedCount = 0;
|
var notifiedCount = 0;
|
||||||
foreach (var subscription in subscribers.GroupBy(s => s.AccountId).Select(g => g.First()))
|
foreach (var subscription in subscribers.DistinctBy(s => s.AccountId))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -194,7 +194,8 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services,
|
||||||
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddScoped<CompactTokenService>();
|
services.AddScoped<CompactTokenService>();
|
||||||
services.AddScoped<RazorViewRenderer>();
|
services.AddScoped<RazorViewRenderer>();
|
||||||
|
@ -51,7 +51,7 @@ public class AfdianPaymentHandler(
|
|||||||
var sign = CalculateSign(token, userId, paramsJson, ts);
|
var sign = CalculateSign(token, userId, paramsJson, ts);
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient();
|
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
|
Content = new StringContent(JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
@ -107,7 +107,7 @@ public class AfdianPaymentHandler(
|
|||||||
var sign = CalculateSign(token, userId, paramsJson, ts);
|
var sign = CalculateSign(token, userId, paramsJson, ts);
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient();
|
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
|
Content = new StringContent(JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
@ -176,7 +176,7 @@ public class AfdianPaymentHandler(
|
|||||||
var sign = CalculateSign(token, userId, paramsJson, ts);
|
var sign = CalculateSign(token, userId, paramsJson, ts);
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient();
|
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
|
Content = new StringContent(JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
|
@ -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 properties;
|
||||||
@layer theme, base, components, utilities;
|
@layer theme, base, components, utilities;
|
||||||
@layer theme {
|
@layer theme {
|
||||||
@ -7,19 +7,27 @@
|
|||||||
'Noto Color Emoji';
|
'Noto Color Emoji';
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
monospace;
|
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-100: oklch(97.3% 0.071 103.193);
|
||||||
--color-yellow-200: oklch(94.5% 0.129 101.54);
|
--color-yellow-200: oklch(94.5% 0.129 101.54);
|
||||||
--color-yellow-800: oklch(47.6% 0.114 61.907);
|
--color-yellow-800: oklch(47.6% 0.114 61.907);
|
||||||
--color-yellow-900: oklch(42.1% 0.095 57.708);
|
--color-yellow-900: oklch(42.1% 0.095 57.708);
|
||||||
--color-green-100: oklch(96.2% 0.044 156.743);
|
--color-green-100: oklch(96.2% 0.044 156.743);
|
||||||
--color-green-200: oklch(92.5% 0.084 155.995);
|
--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-800: oklch(44.8% 0.119 151.328);
|
||||||
--color-green-900: oklch(39.3% 0.095 152.535);
|
--color-green-900: oklch(39.3% 0.095 152.535);
|
||||||
|
--color-blue-300: oklch(80.9% 0.105 251.813);
|
||||||
--color-blue-400: oklch(70.7% 0.165 254.624);
|
--color-blue-400: oklch(70.7% 0.165 254.624);
|
||||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||||
|
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
--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-300: oklch(87.2% 0.01 258.338);
|
||||||
--color-gray-400: oklch(70.7% 0.022 261.325);
|
--color-gray-400: oklch(70.7% 0.022 261.325);
|
||||||
--color-gray-500: oklch(55.1% 0.027 264.364);
|
--color-gray-500: oklch(55.1% 0.027 264.364);
|
||||||
@ -29,6 +37,7 @@
|
|||||||
--color-gray-900: oklch(21% 0.034 264.665);
|
--color-gray-900: oklch(21% 0.034 264.665);
|
||||||
--color-white: #fff;
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
|
--container-md: 28rem;
|
||||||
--container-lg: 32rem;
|
--container-lg: 32rem;
|
||||||
--container-2xl: 42rem;
|
--container-2xl: 42rem;
|
||||||
--container-7xl: 80rem;
|
--container-7xl: 80rem;
|
||||||
@ -50,6 +59,7 @@
|
|||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--tracking-tight: -0.025em;
|
--tracking-tight: -0.025em;
|
||||||
|
--radius-md: 0.375rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@ -206,6 +216,9 @@
|
|||||||
.invisible {
|
.invisible {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
.fixed {
|
.fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
@ -248,6 +261,9 @@
|
|||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.mt-4 {
|
.mt-4 {
|
||||||
margin-top: calc(var(--spacing) * 4);
|
margin-top: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@ -269,6 +285,9 @@
|
|||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: calc(var(--spacing) * 4);
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@ -278,6 +297,12 @@
|
|||||||
.mb-8 {
|
.mb-8 {
|
||||||
margin-bottom: calc(var(--spacing) * 8);
|
margin-bottom: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.ml-4 {
|
||||||
|
margin-left: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.ml-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -290,6 +315,9 @@
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
@ -302,6 +330,9 @@
|
|||||||
.h-full {
|
.h-full {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -311,12 +342,21 @@
|
|||||||
.max-w-lg {
|
.max-w-lg {
|
||||||
max-width: var(--container-lg);
|
max-width: var(--container-lg);
|
||||||
}
|
}
|
||||||
|
.max-w-md {
|
||||||
|
max-width: var(--container-md);
|
||||||
|
}
|
||||||
|
.flex-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
.border-collapse {
|
.border-collapse {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
.transform {
|
.transform {
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
}
|
}
|
||||||
|
.resize {
|
||||||
|
resize: both;
|
||||||
|
}
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -329,12 +369,27 @@
|
|||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.space-y-4 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.gap-x-6 {
|
.gap-x-6 {
|
||||||
column-gap: calc(var(--spacing) * 6);
|
column-gap: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.rounded-lg {
|
.rounded-lg {
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
.border {
|
.border {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@ -349,12 +404,21 @@
|
|||||||
.bg-blue-500 {
|
.bg-blue-500 {
|
||||||
background-color: var(--color-blue-500);
|
background-color: var(--color-blue-500);
|
||||||
}
|
}
|
||||||
|
.bg-blue-600 {
|
||||||
|
background-color: var(--color-blue-600);
|
||||||
|
}
|
||||||
|
.bg-gray-50 {
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
}
|
||||||
.bg-gray-100 {
|
.bg-gray-100 {
|
||||||
background-color: var(--color-gray-100);
|
background-color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
.bg-green-100 {
|
.bg-green-100 {
|
||||||
background-color: var(--color-green-100);
|
background-color: var(--color-green-100);
|
||||||
}
|
}
|
||||||
|
.bg-red-600 {
|
||||||
|
background-color: var(--color-red-600);
|
||||||
|
}
|
||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
}
|
}
|
||||||
@ -367,21 +431,42 @@
|
|||||||
.p-6 {
|
.p-6 {
|
||||||
padding: calc(var(--spacing) * 6);
|
padding: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.p-8 {
|
||||||
|
padding: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.px-3 {
|
||||||
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.px-4 {
|
.px-4 {
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.px-8 {
|
||||||
|
padding-inline: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.py-2 {
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.py-3 {
|
.py-3 {
|
||||||
padding-block: calc(var(--spacing) * 3);
|
padding-block: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
.py-12 {
|
.py-12 {
|
||||||
padding-block: calc(var(--spacing) * 12);
|
padding-block: calc(var(--spacing) * 12);
|
||||||
}
|
}
|
||||||
|
.pt-8 {
|
||||||
|
padding-top: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.pb-4 {
|
||||||
|
padding-bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
@ -426,6 +511,9 @@
|
|||||||
--tw-tracking: var(--tracking-tight);
|
--tw-tracking: var(--tracking-tight);
|
||||||
letter-spacing: var(--tracking-tight);
|
letter-spacing: var(--tracking-tight);
|
||||||
}
|
}
|
||||||
|
.text-blue-600 {
|
||||||
|
color: var(--color-blue-600);
|
||||||
|
}
|
||||||
.text-gray-500 {
|
.text-gray-500 {
|
||||||
color: var(--color-gray-500);
|
color: var(--color-gray-500);
|
||||||
}
|
}
|
||||||
@ -435,12 +523,21 @@
|
|||||||
.text-gray-700 {
|
.text-gray-700 {
|
||||||
color: var(--color-gray-700);
|
color: var(--color-gray-700);
|
||||||
}
|
}
|
||||||
|
.text-gray-800 {
|
||||||
|
color: var(--color-gray-800);
|
||||||
|
}
|
||||||
.text-gray-900 {
|
.text-gray-900 {
|
||||||
color: var(--color-gray-900);
|
color: var(--color-gray-900);
|
||||||
}
|
}
|
||||||
|
.text-green-600 {
|
||||||
|
color: var(--color-green-600);
|
||||||
|
}
|
||||||
.text-green-800 {
|
.text-green-800 {
|
||||||
color: var(--color-green-800);
|
color: var(--color-green-800);
|
||||||
}
|
}
|
||||||
|
.text-red-500 {
|
||||||
|
color: var(--color-red-500);
|
||||||
|
}
|
||||||
.text-white {
|
.text-white {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
@ -464,10 +561,18 @@
|
|||||||
--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));
|
--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);
|
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 {
|
.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));
|
--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);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
.outline {
|
||||||
|
outline-style: var(--tw-outline-style);
|
||||||
|
outline-width: 1px;
|
||||||
|
}
|
||||||
.blur {
|
.blur {
|
||||||
--tw-blur: blur(8px);
|
--tw-blur: blur(8px);
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
@ -497,6 +602,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-blue-700 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-blue-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:bg-gray-100 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:bg-red-700 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-red-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:text-blue-500 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-blue-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:text-blue-600 {
|
.hover\:text-blue-600 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@ -504,6 +637,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\:ring-2 {
|
||||||
&:focus {
|
&:focus {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
@ -515,6 +666,16 @@
|
|||||||
--tw-ring-color: var(--color-blue-400);
|
--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\:outline-none {
|
||||||
&:focus {
|
&:focus {
|
||||||
--tw-outline-style: none;
|
--tw-outline-style: none;
|
||||||
@ -532,6 +693,11 @@
|
|||||||
border-color: var(--color-gray-600);
|
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 {
|
.dark\:bg-gray-800 {
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
background-color: var(--color-gray-800);
|
background-color: var(--color-gray-800);
|
||||||
@ -552,6 +718,16 @@
|
|||||||
background-color: var(--color-yellow-900);
|
background-color: var(--color-yellow-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:text-blue-400 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color: var(--color-blue-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-gray-200 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:text-gray-300 {
|
.dark\:text-gray-300 {
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color: var(--color-gray-300);
|
color: var(--color-gray-300);
|
||||||
@ -567,6 +743,11 @@
|
|||||||
color: var(--color-green-200);
|
color: var(--color-green-200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:text-green-400 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color: var(--color-green-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:text-white {
|
.dark\:text-white {
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
@ -577,6 +758,24 @@
|
|||||||
color: var(--color-yellow-200);
|
color: var(--color-yellow-200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:hover\:bg-gray-600 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:hover\:text-blue-300 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-blue-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:hover\:text-blue-400 {
|
.dark\:hover\:text-blue-400 {
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -586,6 +785,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, base, components, utilities;
|
||||||
@layer theme;
|
@layer theme;
|
||||||
@ -828,6 +1036,11 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-space-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
@property --tw-border-style {
|
@property --tw-border-style {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@ -910,6 +1123,11 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0 0 #0000;
|
initial-value: 0 0 #0000;
|
||||||
}
|
}
|
||||||
|
@property --tw-outline-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
@property --tw-blur {
|
@property --tw-blur {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@ -986,6 +1204,7 @@
|
|||||||
--tw-rotate-z: initial;
|
--tw-rotate-z: initial;
|
||||||
--tw-skew-x: initial;
|
--tw-skew-x: initial;
|
||||||
--tw-skew-y: initial;
|
--tw-skew-y: initial;
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
--tw-border-style: solid;
|
--tw-border-style: solid;
|
||||||
--tw-leading: initial;
|
--tw-leading: initial;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
@ -1004,6 +1223,7 @@
|
|||||||
--tw-ring-offset-width: 0px;
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-outline-style: solid;
|
||||||
--tw-blur: initial;
|
--tw-blur: initial;
|
||||||
--tw-brightness: initial;
|
--tw-brightness: initial;
|
||||||
--tw-contrast: initial;
|
--tw-contrast: initial;
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AChapterData_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fe6_003F64a6c0f7_003FChapterData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AChapterData_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fe6_003F64a6c0f7_003FChapterData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaim_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa7fdc52b6e574ae7b9822133be91162a15800_003Ff7_003Feebffd8d_003FClaim_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaim_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa7fdc52b6e574ae7b9822133be91162a15800_003Ff7_003Feebffd8d_003FClaim_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConnectionMultiplexer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ed0e2f073b1d77b98dadb822da09ee8a9dfb91bf29bf2bbaecb8750d7e74cc9_003FConnectionMultiplexer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003Ff6_003Fdf150bb3_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003Ff6_003Fdf150bb3_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACookieOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F663f33943e4c4e889dc7050c1e97e703e000_003F89_003Fb06980d7_003FCookieOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACorsPolicyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F051ad509d0504b7ca10dedd9c2cabb9914200_003F8e_003Fb28257cb_003FCorsPolicyBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACorsPolicyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F051ad509d0504b7ca10dedd9c2cabb9914200_003F8e_003Fb28257cb_003FCorsPolicyBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalScheduleBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F2b_003Ff86eadcb_003FDailyTimeIntervalScheduleBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalScheduleBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F2b_003Ff86eadcb_003FDailyTimeIntervalScheduleBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalTriggerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F5c_003F297b8312_003FDailyTimeIntervalTriggerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalTriggerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F5c_003F297b8312_003FDailyTimeIntervalTriggerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user