Compare commits
30 Commits
082a096470
...
master
Author | SHA1 | Date | |
---|---|---|---|
b6d416a3a8 | |||
2a8cbbfa24 | |||
29d752bdd9 | |||
b12e3315fe | |||
ce3958d397 | |||
26ea2503a4 | |||
d6ce068490 | |||
da4ee81c95 | |||
bec294365f | |||
51a8b684fd | |||
7b026eeae1 | |||
4dd4542c37 | |||
2a3918134f | |||
734e5ca4a0 | |||
ff0789904d | |||
17330fc104 | |||
7c0ad46deb | |||
b8fcd0d94f | |||
fc6edd7378 | |||
1f2cdb146d | |||
be236a27c6 | |||
99c36ae548 | |||
ed2961a5d5 | |||
08b5ffa02f | |||
837a123c3b | |||
ad1166190f | |||
8e8c938132 | |||
8e5b6ace45 | |||
5757526ea5 | |||
6a9cd0905d |
82
DysonNetwork.Pass/Team/Team.cs
Normal file
82
DysonNetwork.Pass/Team/Team.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Team;
|
||||
|
||||
public enum TeamType
|
||||
{
|
||||
Individual,
|
||||
Organizational
|
||||
}
|
||||
|
||||
[Index(nameof(Name), IsUnique = true)]
|
||||
public class Team : ModelBase, IIdentifiedResource
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public TeamType Type { get; set; }
|
||||
[MaxLength(256)] public string Name { get; set; } = string.Empty;
|
||||
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
[MaxLength(4096)] public string? Bio { get; set; }
|
||||
|
||||
// Outdated fields, for backward compability
|
||||
[MaxLength(32)] public string? PictureId { get; set; }
|
||||
[MaxLength(32)] public string? BackgroundId { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<TeamMember> Members { get; set; } = new List<TeamMember>();
|
||||
[JsonIgnore] public ICollection<TeamFeature> Features { get; set; } = new List<TeamFeature>();
|
||||
|
||||
public Guid? AccountId { get; set; }
|
||||
public Account.Account? Account { get; set; }
|
||||
|
||||
public string ResourceIdentifier => $"publisher/{Id}";
|
||||
}
|
||||
|
||||
public enum TeamMemberRole
|
||||
{
|
||||
Owner = 100,
|
||||
Manager = 75,
|
||||
Editor = 50,
|
||||
Viewer = 25
|
||||
}
|
||||
|
||||
public class TeamMember : ModelBase
|
||||
{
|
||||
public Guid TeamId { get; set; }
|
||||
[JsonIgnore] public Team Team { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
|
||||
public TeamMemberRole Role { get; set; } = TeamMemberRole.Viewer;
|
||||
public Instant? JoinedAt { get; set; }
|
||||
}
|
||||
|
||||
public enum TeamSubscriptionStatus
|
||||
{
|
||||
Active,
|
||||
Expired,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public class TeamFeature : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
[MaxLength(1024)] public string Flag { get; set; } = null!;
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
||||
public Guid TeamId { get; set; }
|
||||
public Team Team { get; set; } = null!;
|
||||
}
|
||||
|
||||
public abstract class TeamFeatureFlag
|
||||
{
|
||||
public static List<string> AllFlags => [Develop];
|
||||
public static string Develop = "develop";
|
||||
}
|
@ -11,7 +11,7 @@ using System.Collections.Generic;
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/accounts")]
|
||||
[Route("/api/accounts")]
|
||||
public class AccountController(
|
||||
AppDatabase db,
|
||||
AuthService auth,
|
||||
|
@ -12,7 +12,7 @@ namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("/accounts/me")]
|
||||
[Route("/api/accounts/me")]
|
||||
public class AccountCurrentController(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
|
@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/spells")]
|
||||
[Route("/api/spells")]
|
||||
public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase
|
||||
{
|
||||
[HttpPost("{spellId:guid}/resend")]
|
||||
|
@ -9,7 +9,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/notifications")]
|
||||
[Route("/api/notifications")]
|
||||
public class NotificationController(AppDatabase db, NotificationService nty) : ControllerBase
|
||||
{
|
||||
[HttpGet("count")]
|
||||
|
@ -7,7 +7,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/relationships")]
|
||||
[Route("/api/relationships")]
|
||||
public class RelationshipController(AppDatabase db, RelationshipService rels) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
@ -230,4 +230,24 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{userId:guid}/block")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> UnblockUser(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
var relationship = await rels.UnblockAccount(currentUser, relatedUser);
|
||||
return relationship;
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ namespace DysonNetwork.Sphere.Account;
|
||||
public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
{
|
||||
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
||||
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
|
||||
|
||||
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
||||
{
|
||||
@ -50,9 +51,8 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
|
||||
db.AccountRelationships.Add(relationship);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.AccountId}");
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.RelatedId}");
|
||||
|
||||
await PurgeRelationshipCache(sender.Id, target.Id);
|
||||
|
||||
return relationship;
|
||||
}
|
||||
@ -63,6 +63,18 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked);
|
||||
return await CreateRelationship(sender, target, RelationshipStatus.Blocked);
|
||||
}
|
||||
|
||||
public async Task<Relationship> UnblockAccount(Account sender, Account target)
|
||||
{
|
||||
var relationship = await GetRelationship(sender.Id, target.Id, RelationshipStatus.Blocked);
|
||||
if (relationship is null) throw new ArgumentException("There is no relationship between you and the user.");
|
||||
db.Remove(relationship);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await PurgeRelationshipCache(sender.Id, target.Id);
|
||||
|
||||
return relationship;
|
||||
}
|
||||
|
||||
public async Task<Relationship> SendFriendRequest(Account sender, Account target)
|
||||
{
|
||||
@ -92,8 +104,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
||||
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
||||
}
|
||||
|
||||
public async Task<Relationship> AcceptFriendRelationship(
|
||||
@ -122,8 +133,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.AccountId}");
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.RelatedId}");
|
||||
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
||||
|
||||
return relationshipBackward;
|
||||
}
|
||||
@ -137,15 +147,14 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
db.Update(relationship);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
||||
await PurgeRelationshipCache(accountId, relatedId);
|
||||
|
||||
return relationship;
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> ListAccountFriends(Account account)
|
||||
{
|
||||
string cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}";
|
||||
var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}";
|
||||
var friends = await cache.GetAsync<List<Guid>>(cacheKey);
|
||||
|
||||
if (friends == null)
|
||||
@ -161,6 +170,25 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
|
||||
return friends ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> ListAccountBlocked(Account account)
|
||||
{
|
||||
var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}";
|
||||
var blocked = await cache.GetAsync<List<Guid>>(cacheKey);
|
||||
|
||||
if (blocked == null)
|
||||
{
|
||||
blocked = await db.AccountRelationships
|
||||
.Where(r => r.RelatedId == account.Id)
|
||||
.Where(r => r.Status == RelationshipStatus.Blocked)
|
||||
.Select(r => r.AccountId)
|
||||
.ToListAsync();
|
||||
|
||||
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
return blocked ?? [];
|
||||
}
|
||||
|
||||
public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
||||
RelationshipStatus status = RelationshipStatus.Friends)
|
||||
@ -168,4 +196,12 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
var relationship = await GetRelationship(accountId, relatedId, status);
|
||||
return relationship is not null;
|
||||
}
|
||||
|
||||
private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId)
|
||||
{
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
||||
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}");
|
||||
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}");
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace DysonNetwork.Sphere.Activity;
|
||||
/// Activity is a universal feed that contains multiple kinds of data. Personalized and generated dynamically.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("/activities")]
|
||||
[Route("/api/activities")]
|
||||
public class ActivityController(
|
||||
ActivityService acts
|
||||
) : ControllerBase
|
||||
|
@ -126,7 +126,7 @@ public class ActivityService(
|
||||
var activities = new List<Activity>();
|
||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
||||
debugInclude ??= new HashSet<string>();
|
||||
debugInclude ??= [];
|
||||
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ using DysonNetwork.Sphere.Connection;
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
[ApiController]
|
||||
[Route("/auth")]
|
||||
[Route("/api/auth")]
|
||||
public class AuthController(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
|
@ -14,7 +14,7 @@ using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
|
||||
|
||||
[Route("/auth/open")]
|
||||
[Route("/api/auth/open")]
|
||||
[ApiController]
|
||||
public class OidcProviderController(
|
||||
AppDatabase db,
|
||||
|
@ -8,7 +8,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
[ApiController]
|
||||
[Route("/accounts/me/connections")]
|
||||
[Route("/api/accounts/me/connections")]
|
||||
[Authorize]
|
||||
public class ConnectionController(
|
||||
AppDatabase db,
|
||||
@ -164,7 +164,7 @@ public class ConnectionController(
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[Route("/auth/callback/{provider}")]
|
||||
[Route("/api/auth/callback/{provider}")]
|
||||
[HttpGet, HttpPost]
|
||||
public async Task<IActionResult> HandleCallback([FromRoute] string provider)
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
[ApiController]
|
||||
[Route("/auth/login")]
|
||||
[Route("/api/auth/login")]
|
||||
public class OidcController(
|
||||
IServiceProvider serviceProvider,
|
||||
AppDatabase db,
|
||||
|
@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/chat")]
|
||||
[Route("/api/chat")]
|
||||
public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomService crs) : ControllerBase
|
||||
{
|
||||
public class MarkMessageReadRequest
|
||||
|
@ -13,7 +13,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/chat")]
|
||||
[Route("/api/chat")]
|
||||
public class ChatRoomController(
|
||||
AppDatabase db,
|
||||
FileReferenceService fileRefService,
|
||||
|
@ -252,7 +252,7 @@ public partial class ChatService(
|
||||
|
||||
if (member.Account.Id == sender.AccountId) continue;
|
||||
if (member.Notify == ChatMemberNotify.None) continue;
|
||||
if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
|
||||
// if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
|
||||
if (message.MembersMentioned is null || !message.MembersMentioned.Contains(member.Account.Id))
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
@ -575,4 +575,4 @@ public class SyncResponse
|
||||
{
|
||||
public List<MessageChange> Changes { get; set; } = [];
|
||||
public Instant CurrentTimestamp { get; set; }
|
||||
}
|
||||
}
|
||||
|
308
DysonNetwork.Sphere/Chat/Realtime/CloudflareService.cs
Normal file
308
DysonNetwork.Sphere/Chat/Realtime/CloudflareService.cs
Normal file
@ -0,0 +1,308 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
|
||||
public class CloudflareRealtimeService : IRealtimeService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IConfiguration _configuration;
|
||||
private RSA? _publicKey;
|
||||
|
||||
public CloudflareRealtimeService(
|
||||
AppDatabase db,
|
||||
HttpClient httpClient,
|
||||
IConfiguration configuration
|
||||
)
|
||||
{
|
||||
_db = db;
|
||||
_httpClient = httpClient;
|
||||
_configuration = configuration;
|
||||
var apiKey = _configuration["Realtime:Cloudflare:ApiKey"];
|
||||
var apiSecret = _configuration["Realtime:Cloudflare:ApiSecret"]!;
|
||||
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:{apiSecret}"));
|
||||
_httpClient.BaseAddress = new Uri("https://rtk.realtime.cloudflare.com/v2/");
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
|
||||
public string ProviderName => "Cloudflare";
|
||||
|
||||
public async Task<RealtimeSessionConfig> CreateSessionAsync(Guid roomId, Dictionary<string, object> metadata)
|
||||
{
|
||||
var roomName = $"Room Call #{roomId.ToString().Replace("-", "")}";
|
||||
var requestBody = new
|
||||
{
|
||||
title = roomName,
|
||||
preferred_region = _configuration["Realtime:Cloudflare:PreferredRegion"]
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync("meetings", content);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var meetingResponse = JsonSerializer.Deserialize<CfMeetingResponse>(responseContent);
|
||||
if (meetingResponse is null) throw new Exception("Failed to create meeting with cloudflare");
|
||||
|
||||
return new RealtimeSessionConfig
|
||||
{
|
||||
SessionId = meetingResponse.Data.Id,
|
||||
Parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "meetingId", meetingResponse.Data.Id }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task EndSessionAsync(string sessionId, RealtimeSessionConfig config)
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
status = "INACTIVE"
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PatchAsync($"sessions/{sessionId}", content);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
return GetUserTokenAsync(account, sessionId, isAdmin).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> GetUserTokenAsync(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
// First try to get the participant by their custom ID
|
||||
var participantCheckResponse = await _httpClient
|
||||
.GetAsync($"meetings/{sessionId}/participants/{account.Id}");
|
||||
|
||||
if (participantCheckResponse.IsSuccessStatusCode)
|
||||
{
|
||||
// Participant exists, get a new token
|
||||
var tokenResponse = await _httpClient
|
||||
.PostAsync($"meetings/{sessionId}/participants/{account.Id}/token", null);
|
||||
tokenResponse.EnsureSuccessStatusCode();
|
||||
var tokenContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||
var tokenData = JsonSerializer.Deserialize<CfResponse<CfTokenResponse>>(tokenContent);
|
||||
if (tokenData == null || !tokenData.Success)
|
||||
{
|
||||
throw new Exception("Failed to get participant token");
|
||||
}
|
||||
|
||||
return tokenData.Data?.Token ?? throw new Exception("Token is null");
|
||||
}
|
||||
|
||||
// Participant doesn't exist, create a new one
|
||||
var baseUrl = _configuration["BaseUrl"];
|
||||
var requestBody = new
|
||||
{
|
||||
name = "@" + account.Name,
|
||||
picture = account.Profile.Picture is not null
|
||||
? $"{baseUrl}/api/files/{account.Profile.Picture.Id}"
|
||||
: null,
|
||||
preset_name = isAdmin ? "group_call_host" : "group_call_participant",
|
||||
custom_participant_id = account.Id.ToString()
|
||||
};
|
||||
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(requestBody),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
var createResponse = await _httpClient.PostAsync($"meetings/{sessionId}/participants", content);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var responseContent = await createResponse.Content.ReadAsStringAsync();
|
||||
var participantData = JsonSerializer.Deserialize<CfResponse<CfParticipantResponse>>(responseContent);
|
||||
if (participantData == null || !participantData.Success)
|
||||
{
|
||||
throw new Exception("Failed to create participant");
|
||||
}
|
||||
|
||||
return participantData.Data?.Token ?? throw new Exception("Token is null");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error or handle it appropriately
|
||||
throw new Exception($"Failed to get or create participant: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReceiveWebhook(string body, string authHeader)
|
||||
{
|
||||
if (string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
throw new ArgumentException("Auth header is missing");
|
||||
}
|
||||
|
||||
if (_publicKey == null)
|
||||
{
|
||||
await GetPublicKeyAsync();
|
||||
}
|
||||
|
||||
var signature = authHeader.Replace("Signature ", "");
|
||||
var bodyBytes = Encoding.UTF8.GetBytes(body);
|
||||
var signatureBytes = Convert.FromBase64String(signature);
|
||||
|
||||
if (!(_publicKey?.VerifyData(bodyBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ??
|
||||
false))
|
||||
{
|
||||
throw new SecurityTokenException("Webhook signature validation failed");
|
||||
}
|
||||
|
||||
// Process the webhook event
|
||||
var evt = JsonSerializer.Deserialize<CfWebhookEvent>(body);
|
||||
if (evt is null) return;
|
||||
|
||||
switch (evt.Type)
|
||||
{
|
||||
case "meeting.ended":
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
await _db.ChatRealtimeCall
|
||||
.Where(c => c.SessionId == evt.Event.Meeting.Id)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(p => p.EndedAt, now)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class WebhooksConfig
|
||||
{
|
||||
[JsonPropertyName("keys")] public List<WebhookKey> Keys { get; set; } = new List<WebhookKey>();
|
||||
}
|
||||
|
||||
private class WebhookKey
|
||||
{
|
||||
[JsonPropertyName("publicKeyPem")] public string PublicKeyPem { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private async Task GetPublicKeyAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://rtk.realtime.cloudflare.com/.well-known/webhooks.json");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var webhooksConfig = JsonSerializer.Deserialize<WebhooksConfig>(content);
|
||||
var publicKeyPem = webhooksConfig?.Keys.FirstOrDefault()?.PublicKeyPem;
|
||||
|
||||
if (string.IsNullOrEmpty(publicKeyPem))
|
||||
{
|
||||
throw new InvalidOperationException("Public key not found in webhooks configuration.");
|
||||
}
|
||||
|
||||
_publicKey = RSA.Create();
|
||||
_publicKey.ImportFromPem(publicKeyPem);
|
||||
}
|
||||
|
||||
private class CfMeetingResponse
|
||||
{
|
||||
[JsonPropertyName("data")] public CfMeetingData Data { get; set; } = new();
|
||||
}
|
||||
|
||||
private class CfMeetingData
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = string.Empty;
|
||||
[JsonPropertyName("roomName")] public string RoomName { get; set; } = string.Empty;
|
||||
[JsonPropertyName("title")] public string Title { get; set; } = string.Empty;
|
||||
[JsonPropertyName("status")] public string Status { get; set; } = string.Empty;
|
||||
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
|
||||
[JsonPropertyName("updatedAt")] public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
private class CfParticipant
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public string CustomParticipantId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CfResponse<T>
|
||||
{
|
||||
[JsonPropertyName("success")] public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("data")] public T? Data { get; set; }
|
||||
}
|
||||
|
||||
public class CfTokenResponse
|
||||
{
|
||||
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CfParticipantResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("customUserId")] public string CustomUserId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("presetName")] public string PresetName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("isActive")] public bool IsActive { get; set; }
|
||||
|
||||
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CfWebhookEvent
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("webhookId")] public string WebhookId { get; set; }
|
||||
|
||||
[JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("event")] public EventData Event { get; set; }
|
||||
}
|
||||
|
||||
public class EventData
|
||||
{
|
||||
[JsonPropertyName("meeting")] public MeetingData Meeting { get; set; }
|
||||
|
||||
[JsonPropertyName("participant")] public ParticipantData Participant { get; set; }
|
||||
}
|
||||
|
||||
public class MeetingData
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("roomName")] public string RoomName { get; set; }
|
||||
|
||||
[JsonPropertyName("title")] public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("status")] public string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updatedAt")] public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ParticipantData
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")] public string UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("customParticipantId")]
|
||||
public string CustomParticipantId { get; set; }
|
||||
|
||||
[JsonPropertyName("presetName")] public string PresetName { get; set; }
|
||||
|
||||
[JsonPropertyName("joinedAt")] public DateTime JoinedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("leftAt")] public DateTime? LeftAt { get; set; }
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ public interface IRealtimeService
|
||||
Task EndSessionAsync(string sessionId, RealtimeSessionConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a token for user to join the session
|
||||
/// Gets a token for user to join the session (synchronous version for backward compatibility)
|
||||
/// </summary>
|
||||
/// <param name="account">The user identifier</param>
|
||||
/// <param name="sessionId">The session identifier</param>
|
||||
@ -38,6 +38,15 @@ public interface IRealtimeService
|
||||
/// <returns>User-specific token for the session</returns>
|
||||
string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a token for user to join the session asynchronously
|
||||
/// </summary>
|
||||
/// <param name="account">The user identifier</param>
|
||||
/// <param name="sessionId">The session identifier</param>
|
||||
/// <param name="isAdmin">The user is the admin of session</param>
|
||||
/// <returns>Task that resolves to the user-specific token for the session</returns>
|
||||
Task<string> GetUserTokenAsync(Account.Account account, string sessionId, bool isAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// Processes incoming webhook requests from the realtime service provider
|
||||
/// </summary>
|
||||
|
@ -10,33 +10,33 @@ namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
/// <summary>
|
||||
/// LiveKit implementation of the real-time communication service
|
||||
/// </summary>
|
||||
public class LivekitRealtimeService : IRealtimeService
|
||||
public class LiveKitRealtimeService : IRealtimeService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly ICacheService _cache;
|
||||
private readonly WebSocketService _ws;
|
||||
private readonly RealtimeStatusService _callStatus;
|
||||
|
||||
private readonly ILogger<LivekitRealtimeService> _logger;
|
||||
private readonly ILogger<LiveKitRealtimeService> _logger;
|
||||
private readonly RoomServiceClient _roomService;
|
||||
private readonly AccessToken _accessToken;
|
||||
private readonly WebhookReceiver _webhookReceiver;
|
||||
|
||||
public LivekitRealtimeService(
|
||||
public LiveKitRealtimeService(
|
||||
IConfiguration configuration,
|
||||
ILogger<LivekitRealtimeService> logger,
|
||||
ILogger<LiveKitRealtimeService> logger,
|
||||
AppDatabase db,
|
||||
ICacheService cache,
|
||||
WebSocketService ws
|
||||
RealtimeStatusService callStatus
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Get LiveKit configuration from appsettings
|
||||
var host = configuration["RealtimeChat:Endpoint"] ??
|
||||
var host = configuration["Realtime:LiveKit:Endpoint"] ??
|
||||
throw new ArgumentNullException("Endpoint configuration is required");
|
||||
var apiKey = configuration["RealtimeChat:ApiKey"] ??
|
||||
var apiKey = configuration["Realtime:LiveKit:ApiKey"] ??
|
||||
throw new ArgumentNullException("ApiKey configuration is required");
|
||||
var apiSecret = configuration["RealtimeChat:ApiSecret"] ??
|
||||
var apiSecret = configuration["Realtime:LiveKit:ApiSecret"] ??
|
||||
throw new ArgumentNullException("ApiSecret configuration is required");
|
||||
|
||||
_roomService = new RoomServiceClient(host, apiKey, apiSecret);
|
||||
@ -45,7 +45,7 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
|
||||
_db = db;
|
||||
_cache = cache;
|
||||
_ws = ws;
|
||||
_callStatus = callStatus;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -112,6 +112,11 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
return GetUserTokenAsync(account, sessionId, isAdmin).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<string> GetUserTokenAsync(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
var token = _accessToken.WithIdentity(account.Name)
|
||||
.WithName(account.Nick)
|
||||
@ -128,7 +133,7 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
.WithMetadata(JsonSerializer.Serialize(new Dictionary<string, string>
|
||||
{ { "account_id", account.Id.ToString() } }))
|
||||
.WithTtl(TimeSpan.FromHours(1));
|
||||
return token.ToJwt();
|
||||
return Task.FromResult(token.ToJwt());
|
||||
}
|
||||
|
||||
public async Task ReceiveWebhook(string body, string authHeader)
|
||||
@ -159,7 +164,8 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
evt.Room.Name, evt.Participant.Identity);
|
||||
|
||||
// Broadcast participant list update to all participants
|
||||
await _BroadcastParticipantUpdate(evt.Room.Name);
|
||||
var info = await GetRoomParticipantsAsync(evt.Room.Name);
|
||||
await _callStatus.BroadcastParticipantUpdate(evt.Room.Name, info);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -174,14 +180,15 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
evt.Room.Name, evt.Participant.Identity);
|
||||
|
||||
// Broadcast participant list update to all participants
|
||||
await _BroadcastParticipantUpdate(evt.Room.Name);
|
||||
var info = await GetRoomParticipantsAsync(evt.Room.Name);
|
||||
await _callStatus.BroadcastParticipantUpdate(evt.Room.Name, info);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _GetParticipantsKey(string roomName)
|
||||
|
||||
private static string _GetParticipantsKey(string roomName)
|
||||
=> $"RoomParticipants_{roomName}";
|
||||
|
||||
private async Task _AddParticipantToCache(string roomName, ParticipantInfo participant)
|
||||
@ -201,7 +208,7 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
// Get the current participants list
|
||||
var participants = await _cache.GetAsync<List<ParticipantCacheItem>>(participantsKey) ??
|
||||
var participants = await _cache.GetAsync<List<ParticipantInfoItem>>(participantsKey) ??
|
||||
[];
|
||||
|
||||
// Check if the participant already exists
|
||||
@ -241,7 +248,7 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
// Get current participants list
|
||||
var participants = await _cache.GetAsync<List<ParticipantCacheItem>>(participantsKey);
|
||||
var participants = await _cache.GetAsync<List<ParticipantInfoItem>>(participantsKey);
|
||||
if (participants == null || !participants.Any())
|
||||
return;
|
||||
|
||||
@ -253,36 +260,24 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
// Helper method to get participants in a room
|
||||
public async Task<List<ParticipantCacheItem>> GetRoomParticipantsAsync(string roomName)
|
||||
public async Task<List<ParticipantInfoItem>> GetRoomParticipantsAsync(string roomName)
|
||||
{
|
||||
var participantsKey = _GetParticipantsKey(roomName);
|
||||
return await _cache.GetAsync<List<ParticipantCacheItem>>(participantsKey) ?? [];
|
||||
return await _cache.GetAsync<List<ParticipantInfoItem>>(participantsKey) ?? [];
|
||||
}
|
||||
|
||||
// Class to represent a participant in the cache
|
||||
public class ParticipantCacheItem
|
||||
{
|
||||
public string Identity { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public Guid? AccountId { get; set; }
|
||||
public ParticipantInfo.Types.State State { get; set; }
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
public DateTime JoinedAt { get; set; }
|
||||
}
|
||||
|
||||
private ParticipantCacheItem CreateParticipantCacheItem(ParticipantInfo participant)
|
||||
private ParticipantInfoItem CreateParticipantCacheItem(ParticipantInfo participant)
|
||||
{
|
||||
// Try to parse account ID from metadata
|
||||
Guid? accountId = null;
|
||||
var metadata = new Dictionary<string, string>();
|
||||
|
||||
if (string.IsNullOrEmpty(participant.Metadata))
|
||||
return new ParticipantCacheItem
|
||||
return new ParticipantInfoItem
|
||||
{
|
||||
Identity = participant.Identity,
|
||||
Name = participant.Name,
|
||||
AccountId = accountId,
|
||||
State = participant.State,
|
||||
Metadata = metadata,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
};
|
||||
@ -300,92 +295,13 @@ public class LivekitRealtimeService : IRealtimeService
|
||||
_logger.LogError(ex, "Failed to parse participant metadata");
|
||||
}
|
||||
|
||||
return new ParticipantCacheItem
|
||||
return new ParticipantInfoItem
|
||||
{
|
||||
Identity = participant.Identity,
|
||||
Name = participant.Name,
|
||||
AccountId = accountId,
|
||||
State = participant.State,
|
||||
Metadata = metadata,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
// Broadcast participant update to all participants in a room
|
||||
private async Task _BroadcastParticipantUpdate(string roomName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the room ID from the session name
|
||||
var roomInfo = await _db.ChatRealtimeCall
|
||||
.Where(c => c.SessionId == roomName && c.EndedAt == null)
|
||||
.Select(c => new { c.RoomId, c.Id })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (roomInfo == null)
|
||||
{
|
||||
_logger.LogWarning("Could not find room info for session: {SessionName}", roomName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current participants
|
||||
var livekitParticipants = await GetRoomParticipantsAsync(roomName);
|
||||
|
||||
// Get all room members who should receive this update
|
||||
var roomMembers = await _db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomInfo.RoomId && m.LeaveAt == null)
|
||||
.Select(m => m.AccountId)
|
||||
.ToListAsync();
|
||||
|
||||
// Get member profiles for participants who have account IDs
|
||||
var accountIds = livekitParticipants
|
||||
.Where(p => p.AccountId.HasValue)
|
||||
.Select(p => p.AccountId!.Value)
|
||||
.ToList();
|
||||
|
||||
var memberProfiles = new Dictionary<Guid, ChatMember>();
|
||||
if (accountIds.Count != 0)
|
||||
{
|
||||
memberProfiles = await _db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomInfo.RoomId && accountIds.Contains(m.AccountId))
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
.ToDictionaryAsync(m => m.AccountId, m => m);
|
||||
}
|
||||
|
||||
// Convert to CallParticipant objects
|
||||
var participants = livekitParticipants.Select(p => new CallParticipant
|
||||
{
|
||||
Identity = p.Identity,
|
||||
Name = p.Name,
|
||||
AccountId = p.AccountId,
|
||||
JoinedAt = p.JoinedAt,
|
||||
Profile = p.AccountId.HasValue && memberProfiles.TryGetValue(p.AccountId.Value, out var profile)
|
||||
? profile
|
||||
: null
|
||||
}).ToList();
|
||||
|
||||
// Create the update packet with CallParticipant objects
|
||||
var updatePacket = new WebSocketPacket
|
||||
{
|
||||
Type = WebSocketPacketType.CallParticipantsUpdate,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "room_id", roomInfo.RoomId },
|
||||
{ "call_id", roomInfo.Id },
|
||||
{ "participants", participants }
|
||||
}
|
||||
};
|
||||
|
||||
// Send the update to all members
|
||||
foreach (var accountId in roomMembers)
|
||||
{
|
||||
_ws.SendPacketToAccount(accountId, updatePacket);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error broadcasting participant update for room {RoomName}", roomName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
91
DysonNetwork.Sphere/Chat/Realtime/RealtimeStatusService.cs
Normal file
91
DysonNetwork.Sphere/Chat/Realtime/RealtimeStatusService.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
|
||||
public class ParticipantInfoItem
|
||||
{
|
||||
public string Identity { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public Guid? AccountId { get; set; }
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
public DateTime JoinedAt { get; set; }
|
||||
}
|
||||
|
||||
public class RealtimeStatusService(AppDatabase db, WebSocketService ws, ILogger<RealtimeStatusService> logger)
|
||||
{
|
||||
// Broadcast participant update to all participants in a room
|
||||
public async Task BroadcastParticipantUpdate(string roomName, List<ParticipantInfoItem> participantsInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the room ID from the session name
|
||||
var roomInfo = await db.ChatRealtimeCall
|
||||
.Where(c => c.SessionId == roomName && c.EndedAt == null)
|
||||
.Select(c => new { c.RoomId, c.Id })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (roomInfo == null)
|
||||
{
|
||||
logger.LogWarning("Could not find room info for session: {SessionName}", roomName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all room members who should receive this update
|
||||
var roomMembers = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomInfo.RoomId && m.LeaveAt == null)
|
||||
.Select(m => m.AccountId)
|
||||
.ToListAsync();
|
||||
|
||||
// Get member profiles for participants who have account IDs
|
||||
var accountIds = participantsInfo
|
||||
.Where(p => p.AccountId.HasValue)
|
||||
.Select(p => p.AccountId!.Value)
|
||||
.ToList();
|
||||
|
||||
var memberProfiles = new Dictionary<Guid, ChatMember>();
|
||||
if (accountIds.Count != 0)
|
||||
{
|
||||
memberProfiles = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomInfo.RoomId && accountIds.Contains(m.AccountId))
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
.ToDictionaryAsync(m => m.AccountId, m => m);
|
||||
}
|
||||
|
||||
// Convert to CallParticipant objects
|
||||
var participants = participantsInfo.Select(p => new CallParticipant
|
||||
{
|
||||
Identity = p.Identity,
|
||||
Name = p.Name,
|
||||
AccountId = p.AccountId,
|
||||
JoinedAt = p.JoinedAt,
|
||||
Profile = p.AccountId.HasValue && memberProfiles.TryGetValue(p.AccountId.Value, out var profile)
|
||||
? profile
|
||||
: null
|
||||
}).ToList();
|
||||
|
||||
// Create the update packet with CallParticipant objects
|
||||
var updatePacket = new WebSocketPacket
|
||||
{
|
||||
Type = WebSocketPacketType.CallParticipantsUpdate,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "room_id", roomInfo.RoomId },
|
||||
{ "call_id", roomInfo.Id },
|
||||
{ "participants", participants }
|
||||
}
|
||||
};
|
||||
|
||||
// Send the update to all members
|
||||
foreach (var accountId in roomMembers)
|
||||
{
|
||||
ws.SendPacketToAccount(accountId, updatePacket);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error broadcasting participant update for room {RoomName}", roomName);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using DysonNetwork.Sphere.Chat.Realtime;
|
||||
using Livekit.Server.Sdk.Dotnet;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -7,13 +6,8 @@ using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
public class RealtimeChatConfiguration
|
||||
{
|
||||
public string Endpoint { get; set; } = null!;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("/chat/realtime")]
|
||||
[Route("/api/chat/realtime")]
|
||||
public class RealtimeCallController(
|
||||
IConfiguration configuration,
|
||||
AppDatabase db,
|
||||
@ -21,9 +15,6 @@ public class RealtimeCallController(
|
||||
IRealtimeService realtime
|
||||
) : ControllerBase
|
||||
{
|
||||
private readonly RealtimeChatConfiguration _config =
|
||||
configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
|
||||
|
||||
/// <summary>
|
||||
/// This endpoint is especially designed for livekit webhooks,
|
||||
/// for update the call participates and more.
|
||||
@ -36,9 +27,9 @@ public class RealtimeCallController(
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var postData = await reader.ReadToEndAsync();
|
||||
var authHeader = Request.Headers.Authorization.ToString();
|
||||
|
||||
|
||||
await realtime.ReceiveWebhook(postData, authHeader);
|
||||
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -91,39 +82,17 @@ public class RealtimeCallController(
|
||||
return BadRequest("Call session is not properly configured.");
|
||||
|
||||
var isAdmin = member.Role >= ChatMemberRole.Moderator;
|
||||
var userToken = realtime.GetUserToken(currentUser, ongoingCall.SessionId, isAdmin);
|
||||
var userToken = await realtime.GetUserTokenAsync(currentUser, ongoingCall.SessionId, isAdmin);
|
||||
|
||||
// Get LiveKit endpoint from configuration
|
||||
var endpoint = _config.Endpoint ??
|
||||
throw new InvalidOperationException("LiveKit endpoint configuration is missing");
|
||||
|
||||
// Inject the ChatRoomService
|
||||
var chatRoomService = HttpContext.RequestServices.GetRequiredService<ChatRoomService>();
|
||||
|
||||
// Get current participants from the LiveKit service
|
||||
var participants = new List<CallParticipant>();
|
||||
if (realtime is LivekitRealtimeService livekitService)
|
||||
var endpoint = configuration[$"Realtime:{realtime.ProviderName}:Endpoint"] ?? realtime.ProviderName switch
|
||||
{
|
||||
var roomParticipants = await livekitService.GetRoomParticipantsAsync(ongoingCall.SessionId);
|
||||
participants = [];
|
||||
|
||||
foreach (var p in roomParticipants)
|
||||
{
|
||||
var participant = new CallParticipant
|
||||
{
|
||||
Identity = p.Identity,
|
||||
Name = p.Name,
|
||||
AccountId = p.AccountId,
|
||||
JoinedAt = p.JoinedAt
|
||||
};
|
||||
|
||||
// Fetch the ChatMember profile if we have an account ID
|
||||
if (p.AccountId.HasValue)
|
||||
participant.Profile = await chatRoomService.GetRoomMember(p.AccountId.Value, roomId);
|
||||
|
||||
participants.Add(participant);
|
||||
}
|
||||
}
|
||||
// Unusable for sure, just for placeholder
|
||||
"LiveKit" => "https://livekit.cloud",
|
||||
"Cloudflare" => "https://rtk.realtime.cloudflare.com/v2",
|
||||
// Unusable for sure, just for placeholder
|
||||
_ => "https://example.com"
|
||||
};
|
||||
|
||||
// Create the response model
|
||||
var response = new JoinCallResponse
|
||||
@ -133,8 +102,7 @@ public class RealtimeCallController(
|
||||
Token = userToken,
|
||||
CallId = ongoingCall.Id,
|
||||
RoomName = ongoingCall.SessionId,
|
||||
IsAdmin = isAdmin,
|
||||
Participants = participants
|
||||
IsAdmin = isAdmin
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@ -192,7 +160,7 @@ public class JoinCallResponse
|
||||
public string Provider { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The LiveKit server endpoint
|
||||
/// The provider server endpoint
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = null!;
|
||||
|
||||
@ -215,11 +183,6 @@ public class JoinCallResponse
|
||||
/// Whether the user is the admin of the call
|
||||
/// </summary>
|
||||
public bool IsAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current participants in the call
|
||||
/// </summary>
|
||||
public List<CallParticipant> Participants { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -231,22 +194,22 @@ public class CallParticipant
|
||||
/// The participant's identity (username)
|
||||
/// </summary>
|
||||
public string Identity { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The participant's display name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The participant's account ID if available
|
||||
/// </summary>
|
||||
public Guid? AccountId { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The participant's profile in the chat
|
||||
/// </summary>
|
||||
public ChatMember? Profile { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// When the participant joined the call
|
||||
/// </summary>
|
||||
|
45
DysonNetwork.Sphere/Connection/ClientTypeMiddleware.cs
Normal file
45
DysonNetwork.Sphere/Connection/ClientTypeMiddleware.cs
Normal file
@ -0,0 +1,45 @@
|
||||
namespace DysonNetwork.Sphere.Connection;
|
||||
|
||||
public class ClientTypeMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
var headers = context.Request.Headers;
|
||||
bool isWebPage;
|
||||
|
||||
// Priority 1: Check for custom header
|
||||
if (headers.TryGetValue("X-Client", out var clientType))
|
||||
{
|
||||
isWebPage = clientType.ToString().Length == 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userAgent = headers.UserAgent.ToString();
|
||||
var accept = headers.Accept.ToString();
|
||||
|
||||
// Priority 2: Check known app User-Agent (backward compatibility)
|
||||
if (!string.IsNullOrEmpty(userAgent) && userAgent.Contains("Solian"))
|
||||
isWebPage = false;
|
||||
// Priority 3: Accept header can help infer intent
|
||||
else if (!string.IsNullOrEmpty(accept) && accept.Contains("text/html"))
|
||||
isWebPage = true;
|
||||
else if (!string.IsNullOrEmpty(accept) && accept.Contains("application/json"))
|
||||
isWebPage = false;
|
||||
else
|
||||
isWebPage = true;
|
||||
}
|
||||
|
||||
context.Items["IsWebPage"] = isWebPage;
|
||||
|
||||
var redirectWhiteList = new[] { "/ws", "/.well-known", "/swagger" };
|
||||
if(redirectWhiteList.Any(w => context.Request.Path.StartsWithSegments(w)))
|
||||
await next(context);
|
||||
else if (!isWebPage && !context.Request.Path.StartsWithSegments("/api"))
|
||||
context.Response.Redirect(
|
||||
$"/api{context.Request.Path.Value}{context.Request.QueryString.Value}",
|
||||
permanent: false
|
||||
);
|
||||
else
|
||||
await next(context);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||
|
||||
[ApiController]
|
||||
[Route("/feeds/articles")]
|
||||
[Route("/api/feeds/articles")]
|
||||
public class WebArticleController(AppDatabase db) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -7,7 +7,7 @@ namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("/publishers/{pubName}/feeds")]
|
||||
[Route("/api/publishers/{pubName}/feeds")]
|
||||
public class WebFeedController(WebFeedService webFeed, PublisherService ps) : ControllerBase
|
||||
{
|
||||
public record WebFeedRequest(
|
||||
|
@ -9,7 +9,7 @@ namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||
/// Controller for web scraping and link preview services
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("/scrap")]
|
||||
[Route("/api/scrap")]
|
||||
[EnableRateLimiting("fixed")]
|
||||
public class WebReaderController(WebReaderService reader, ILogger<WebReaderController> logger)
|
||||
: ControllerBase
|
||||
|
@ -38,14 +38,14 @@ public class CustomApp : ModelBase, IIdentifiedResource
|
||||
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
|
||||
}
|
||||
|
||||
public class CustomAppLinks : ModelBase
|
||||
public class CustomAppLinks
|
||||
{
|
||||
[MaxLength(8192)] public string? HomePage { get; set; }
|
||||
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
|
||||
[MaxLength(8192)] public string? TermsOfService { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAppOauthConfig : ModelBase
|
||||
public class CustomAppOauthConfig
|
||||
{
|
||||
[MaxLength(1024)] public string? ClientUri { get; set; }
|
||||
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
||||
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
|
||||
[ApiController]
|
||||
[Route("/developers/{pubName}/apps")]
|
||||
[Route("/api/developers/{pubName}/apps")]
|
||||
public class CustomAppController(CustomAppService customApps, PublisherService ps) : ControllerBase
|
||||
{
|
||||
public record CustomAppRequest(
|
||||
|
@ -9,7 +9,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
|
||||
[ApiController]
|
||||
[Route("/developers")]
|
||||
[Route("/api/developers")]
|
||||
public class DeveloperController(
|
||||
AppDatabase db,
|
||||
PublisherService ps,
|
||||
|
@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace DysonNetwork.Sphere.Discovery;
|
||||
|
||||
[ApiController]
|
||||
[Route("/discovery")]
|
||||
[Route("/api/discovery")]
|
||||
public class DiscoveryController(DiscoveryService discoveryService) : ControllerBase
|
||||
{
|
||||
[HttpGet("realms")]
|
||||
|
@ -26,6 +26,7 @@
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Markdig" Version="0.41.3" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
@page "/web/account/profile"
|
||||
@page "//account/profile"
|
||||
@model DysonNetwork.Sphere.Pages.Account.ProfileModel
|
||||
@{
|
||||
ViewData["Title"] = "Profile";
|
||||
@ -6,53 +6,44 @@
|
||||
|
||||
@if (Model.Account != null)
|
||||
{
|
||||
<div class="h-full bg-gray-100 dark:bg-gray-900 py-8 px-4">
|
||||
<div class="p-4 sm:p-8 bg-base-200">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Profile Settings</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-2">Manage your account information and preferences</p>
|
||||
<h1 class="text-3xl font-bold">Profile Settings</h1>
|
||||
<p class="text-base-content/70 mt-2">Manage your account information and preferences</p>
|
||||
</div>
|
||||
|
||||
<!-- Two Column Layout -->
|
||||
<div class="flex flex-col md:flex-row gap-8">
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- Left Pane - Profile Card -->
|
||||
<div class="w-full md:w-1/3 lg:w-1/4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 sticky top-8">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="card bg-base-100 shadow-xl sticky top-8">
|
||||
<div class="card-body items-center text-center">
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="w-32 h-32 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center mb-4 overflow-hidden">
|
||||
<span class="text-4xl text-gray-500 dark:text-gray-400">
|
||||
@Model.Account.Name?.Substring(0, 1).ToUpper()
|
||||
</span>
|
||||
<div class="avatar avatar-placeholder mb-4">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-32">
|
||||
<span class="text-4xl">@Model.Account.Name?[..1].ToUpper()</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Info -->
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
@Model.Account.Nick
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">@Model.Account.Name</p>
|
||||
<h2 class="card-title">@Model.Account.Nick</h2>
|
||||
<p class="font-mono text-sm">@@@Model.Account.Name</p>
|
||||
|
||||
<!-- Stats -->
|
||||
<div
|
||||
class="mt-4 flex justify-around w-full border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="text-lg font-semibold text-gray-900 dark:text-white">@Model.Account.Profile.Level</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Level</div>
|
||||
<div class="stats stats-vertical shadow mt-4">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Level</div>
|
||||
<div class="stat-value">@Model.Account.Profile.Level</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="text-lg font-semibold text-gray-900 dark:text-white">@Model.Account.Profile.Experience</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">XP</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">XP</div>
|
||||
<div class="stat-value">@Model.Account.Profile.Experience</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
@Model.Account.CreatedAt.ToDateTimeUtc().ToString("yyyy/MM/dd")
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Member since</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Member since</div>
|
||||
<div class="stat-value">@Model.Account.CreatedAt.ToDateTimeUtc().ToString("yyyy/MM")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,181 +52,107 @@
|
||||
|
||||
<!-- Right Pane - Tabbed Content -->
|
||||
<div class="flex-1">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav class="flex -mb-px">
|
||||
<button type="button"
|
||||
class="tab-button active py-4 px-6 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
data-tab="profile">
|
||||
Profile
|
||||
</button>
|
||||
<button type="button"
|
||||
class="tab-button py-4 px-6 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
data-tab="security">
|
||||
Security
|
||||
</button>
|
||||
<button type="button"
|
||||
class="tab-button py-4 px-6 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
data-tab="sessions">
|
||||
Sessions
|
||||
</button>
|
||||
</nav>
|
||||
<div role="tablist" class="tabs tabs-lift w-full">
|
||||
<input type="radio" name="profile-tabs" role="tab" class="tab" aria-label="Profile" checked />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 p-6">
|
||||
<h2 class="text-xl font-semibold mb-6">Profile Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-4">Basic Information</h3>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Full Name</dt>
|
||||
<dd class="mt-1 text-sm">@($"{Model.Account.Profile.FirstName} {Model.Account.Profile.MiddleName} {Model.Account.Profile.LastName}".Trim())</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Username</dt>
|
||||
<dd class="mt-1 text-sm">@Model.Account.Name</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Nickname</dt>
|
||||
<dd class="mt-1 text-sm">@Model.Account.Nick</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Gender</dt>
|
||||
<dd class="mt-1 text-sm">@Model.Account.Profile.Gender</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-4">Additional Details</h3>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Location</dt>
|
||||
<dd class="mt-1 text-sm">@Model.Account.Profile.Location</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Birthday</dt>
|
||||
<dd class="mt-1 text-sm">@Model.Account.Profile.Birthday?.ToString("MMMM d, yyyy", System.Globalization.CultureInfo.InvariantCulture)</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-base-content/70">Bio</dt>
|
||||
<dd class="mt-1 text-sm">@(string.IsNullOrEmpty(Model.Account.Profile.Bio) ? "No bio provided" : Model.Account.Profile.Bio)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="p-6">
|
||||
<!-- Profile Tab -->
|
||||
<div id="profile-tab" class="tab-content">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Profile
|
||||
Information</h2>
|
||||
<input type="radio" name="profile-tabs" role="tab" class="tab" aria-label="Security" />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 p-6">
|
||||
<h2 class="text-xl font-semibold mb-2">Security Settings</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Basic
|
||||
Information</h3>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Full
|
||||
Name
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
@($"{Model.Account.Profile.FirstName} {Model.Account.Profile.MiddleName} {Model.Account.Profile.LastName}".Trim())
|
||||
</dd>
|
||||
<div class="space-y-6">
|
||||
<div class="card bg-base-300 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Access Token</h3>
|
||||
<p>Use this token to authenticate with the API</p>
|
||||
<div class="form-control">
|
||||
<div class="join">
|
||||
<input type="password" id="accessToken" value="@Model.AccessToken" readonly class="input input-bordered join-item flex-grow" />
|
||||
<button onclick="copyAccessToken()" class="btn join-item">Copy</button>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Username
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">@Model.Account.Name</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Nickname
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">@Model.Account.Nick</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Gender
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">@Model.Account.Profile.Gender</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Additional
|
||||
Details</h3>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Location
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">@Model.Account.Profile.Location</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Birthday
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
@Model.Account.Profile.Birthday?.ToString("MMMM d, yyyy", System.Globalization.CultureInfo.InvariantCulture)
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Bio
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
@(string.IsNullOrEmpty(Model.Account.Profile.Bio) ? "No bio provided" : Model.Account.Profile.Bio)
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<button type="button"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Tab -->
|
||||
<div id="security-tab" class="tab-content hidden">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Security
|
||||
Settings</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Access Token</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">Use this
|
||||
token to authenticate with the API</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 px-4 py-5 sm:px-6">
|
||||
<div class="flex items-center">
|
||||
<input type="password" 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 py-2 px-4"/>
|
||||
<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>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Keep this token secure and do not share it with anyone.
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/70 mt-2">Keep this token secure and do not share it with anyone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Tab -->
|
||||
<div id="sessions-tab" class="tab-content hidden">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Active
|
||||
Sessions</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">This is a list of devices that have
|
||||
logged into your account. Revoke any sessions that you do not recognize.</p>
|
||||
<input type="radio" name="profile-tabs" role="tab" class="tab" aria-label="Sessions" />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 p-6">
|
||||
<h2 class="text-xl font-semibold">Active Sessions</h2>
|
||||
<p class="text-base-content/70 mb-3">This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.</p>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<li class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="flex-shrink-0 h-10 w-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-blue-600 dark:text-blue-400"
|
||||
fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6z"
|
||||
clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Current Session
|
||||
<div class="card bg-base-300 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar">
|
||||
<div class="mask mask-squircle w-12 h-12">
|
||||
<svg class="h-full w-full text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@($"{Request.Headers["User-Agent"]} • {DateTime.Now:MMMM d, yyyy 'at' h:mm tt}")
|
||||
<div>
|
||||
<div class="font-bold">Current Session</div>
|
||||
<div class="text-sm opacity-50">@($"{Request.Headers["User-Agent"]} • {DateTime.Now:MMMM d, yyyy 'at' h:mm tt}")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Active now
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="bg-gray-50 dark:bg-gray-800 px-4 py-4 sm:px-6 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Sign out all other sessions
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button type="button" class="btn btn-error">Sign out all other sessions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -245,10 +162,7 @@
|
||||
<!-- Logout Button -->
|
||||
<div class="mt-6 flex justify-end">
|
||||
<form method="post" asp-page-handler="Logout">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Sign out
|
||||
</button>
|
||||
<button type="submit" class="btn btn-error">Sign out</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -258,54 +172,22 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||
<div class="max-w-md w-full p-8 bg-white dark:bg-gray-800 rounded-lg shadow-md text-center">
|
||||
<div class="text-red-500 text-5xl mb-4">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<div class="text-error text-5xl mb-4">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold">Profile Not Found</h1>
|
||||
<p class="py-6">User profile not found. Please log in to continue.</p>
|
||||
<a href="/auth/login" class="btn btn-primary">Go to Login</a>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Profile Not Found</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">User profile not found. Please log in to continue.</p>
|
||||
<a href="/auth/login"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Go to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Tab functionality
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Get all tab buttons and content
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
// Add click event listeners to tab buttons
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
|
||||
// Update active tab button
|
||||
tabButtons.forEach(btn => btn.classList.remove('border-blue-500', 'text-blue-600', 'dark:text-blue-400'));
|
||||
this.classList.add('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
|
||||
|
||||
// Show corresponding tab content
|
||||
tabContents.forEach(content => {
|
||||
content.classList.add('hidden');
|
||||
if (content.id === `${tabId}-tab`) {
|
||||
content.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Show first tab by default
|
||||
if (tabButtons.length > 0) {
|
||||
tabButtons[0].click();
|
||||
}
|
||||
});
|
||||
|
||||
// Copy access token to clipboard
|
||||
function copyAccessToken() {
|
||||
const copyText = document.getElementById("accessToken");
|
||||
@ -340,4 +222,4 @@ else
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,10 @@
|
||||
ViewData["Title"] = "Authorize Application";
|
||||
}
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 transition-colors duration-200 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-8 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 transition-colors duration-200">
|
||||
<div class="text-center">
|
||||
<h2 class="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
<div class="h-full flex items-center justify-center bg-base-200 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
<div class="card-body px-8 py-7">
|
||||
<h2 class="card-title justify-center text-2xl font-bold">
|
||||
Authorize Application
|
||||
</h2>
|
||||
@if (!string.IsNullOrEmpty(Model.AppName))
|
||||
@ -16,21 +16,25 @@
|
||||
<div class="flex items-center justify-center">
|
||||
@if (!string.IsNullOrEmpty(Model.AppLogo))
|
||||
{
|
||||
<div class="relative h-16 w-16 flex-shrink-0">
|
||||
<img class="h-16 w-16 rounded-lg object-cover border border-gray-200 dark:border-gray-700"
|
||||
src="@Model.AppLogo"
|
||||
alt="@Model.AppName logo"
|
||||
onerror="this.onerror=null; this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzczz0idy02IGgtNiB0ZXh0LWdyYXktNDAwIj48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xOS42MDMgMy4wMDRjMS4xNTMgMCAyLjI2LjE4IDMuMzI5LjUxM2EuNzUuNzUgMCAwMS0uMzI4IDEuNDQ1IDE2LjkzIDE2LjkzIDAgMDAtNi4wMS0xuMTAzLjc1Ljc1IDAgMDEtLjY2OC0uNzQ4VjMuNzVBLjc1Ljc1IDAgMDExNi41IDNoMy4xMDN6TTMxLjUgMjEuMDA4Yy0uU0UuNzUgNzUuNzUgMCAwMTEuOTQ4LjA1MWMuMDgxLjkxOC4wNTIgMS44MzgtLjA4NiAyLjczNGEuNzUuNzUgMCAwMS0xLjQ5LS4xNTkgMjEuMzg2IDIxLjM4NiAwIDAwLjA2LTIuNTc1Ljc1Ljc1IDAgMDExLjU3OC0uMDQzem0tMy4wMi0xOC4wMmMuMDYgMS4wOTIuMDQgMi4xODctLjA1NiAzLjI3MmEuNzUuNzUgMCAwMS0xLjQ5Mi0uMTY0Yy4wOTItLjg3NC4xMDctMS43NTYuMDUxLTIuNjA3YS43NS43NSAwIDAxMS40OTctLjEwMXpNNS42MzcgNS42MzJjLS4zMzQuMS0uNjc2LjE4NS0xLjAyNi4yNTdhLjc1Ljc1IDAgMDEuMTQ5LTEuNDljLjQyNS0uMDg1Ljg1Mi0uMTg5IDEuMjY4LS4zMDRhLjc1Ljc1IDAgMDEuMzYgMS40Mzd6TTMuMzMgMTkuNjUzYy0uMTY1LS4zNS0uMzA4LS43MDctNDIuNjUzLjc1Ljc1IDAgMS4zODgtLjU0MiAxLjQ5LTEuMjg1LjA0Ni0uMzI2LjEwNi0uNjUyLjE4LS45NzZhLjc1Ljc1IDAgMTExLjQ2LS41M2MuMTA2LjQzNy4xODkuODgxLjI0NSAxLjM0OGEuNzUuNzUgMCAwMS0xLjQ5LjIzM3pNTEuMzUzIDIuNzY3YS43NS43NSAwIDAxLjc1LS4wMTdsLjAwMS4wMDFoLjAwMWwuMDAyLjAwMWwuMDA3LjAwM2wuMDI0LjAxM2MuMDIuMDEuMDQ1LjAyNS4wNzkuMDQ2LjA2Ny4wNDIuMTYxLjEwMi4yNzUuMTc4LjIzMi4xNTEuNTUuMzgzLjg1Ni42NjdsLjAyNy4wMjRjLjYxNi41NTggMS4yMTIgMS4xNzYgMS43MzMgMS44NDNhLjc1Ljc1IDAgMDEtMS4yNC44N2MtLjQ3LS42NzEtMS4wMzEtMS4yOTItMS42LFsxLjYxNi0xLjYxNmEzLjc1IDMuNzUgMCAwMC01LjMwNS01LjMwNGwtMS4wNi0xuMDZBNy4yNDMgNy4yNDMgMCAwMTUxLjM1MyAyLjc2N3pNNDQuMzc5IDkuNjRsLTEuNTYgMS41NmE2Ljk5IDYuOTkgMCAwMTIuMjMgNC4zMzcgNi45OSA2Ljk5IDAgMDEtMi4yMyA1LjE3NmwtMS41Ni0xLjU2QTguNDkgOC40OSAwIDAwNDUuNSAxNS41YzAtMi4yOTYtLjg3NC00LjQzLTIuMTIxLTYuMDF6bS0zLjUzLTMuNTNsLTEuMDYxLTEuMDZhNy4yNDMgNy4yNDMgMCAwMTkuMTkyIDkuE2x0LTEuMDYgMS4wNjFhNS43NDkgNS43NDkgMCAwMC04LjEzLTguMTN6TTM0LjUgMTUuNWE4Ljk5IDguOTkgMCAwMC0yLjYzMSA2LjM2OS43NS43NSAwIDExLTEuNDk0LS43MzlDNzIuMzkgMjAuMDYgMzMuNSAxNy41NzUgMzMuNSAxNS41YzAtMi4zNzYgMS4wOTktNC40MzggMi44MTEtNS44MTJsLS4zOTYtLjM5NmEuNzUuNzUgMCAwMTEuMDYtMS4wNkwzNy41IDkuNDRWMmgtL4wMDJhLjc1Ljc1IDAgMDEtLjc0OC0uNzVWMS41YS43NS43NSAwIDAxLjc1LS43NWg2YS43NS43NSAwIDAxLjc1Ljc1di4yNWEwIC43NS0uNzUuNzVoLS4wMDF2Ny40NGwzLjUzNy0zLjUzN2EuNzUuNzUgMCAwMTEuMDYgMS4wNmwtLjM5Ni4zOTZDMzUuNDAxIDExLjA2MiAzNC41IDEzLjEyNCAzNC41IDE1LjV6TTM5IDIuMjV2Ni4wMy0uMDAyYTEuNSAxLjUgMCAwMS0uNDQ0IDEuMDZsLTEuMDYxIDEuMDZBOC40OSA4LjQ5IDAgMDAzOSAxNS41YzAgMi4yOTYtLjg3NCA0LjQzLTIuMTIxIDYuMDFsMS41NiAxLjU2QTYuOTkgNi45OSAwIDAwNDIgMTUuNWE2Ljk5IDYuOTkgMCAwMC0yLjIzLTUuMTc2bC0xLjU2LTEuNTZhMS41IDEuNSAwIDAxLS40NC0xLjA2VjIuMjVoLTF6TTI0IDkuNzVhLjc1Ljc1IDAgMDEtLjc1Ljc1di0uNWMwLS40MTQuMzM2LS43NS43NS0uNzVoLjVjLjQxNCAwIC43NS4zMzYuNzUuNzV2LjVhLjc1Ljc1IDAgMDEtLjc1Ljc1aC0uNXpNMTkuNSAxM2EuNzUuNzUgMCAwMS0uNzUtLjc1di0uNWMwLS40MTQuMzM2LS43NS43NS0uNzVoLjVjLjQxNCAwIC43NS4zMzYuNzUuNzV2LjVhLjc1Ljc1IDAgMDEtLjc1Ljc1aC0uNXpNMTUgMTYuMjVhLjc1Ljc1IDAgMDEuNzUtLjc1aC41Yy40MTQgMCAuNzUuMzM2Ljc1Ljc1di41YS43NS43NSAwIDAxLS43NS43NWgtLjVhLjc1Ljc1IDAgMDEtLjc1LS43NXYtLjV6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiLz48L3N2Zz4=';">
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-300">@Model.AppName?[0]</span>
|
||||
<div class="avatar">
|
||||
<div class="w-12 rounded">
|
||||
<img src="@Model.AppLogo" alt="@Model.AppName logo" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="avatar avatar-placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-12">
|
||||
<span class="text-xl">@Model.AppName?[..1].ToUpper()</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="ml-4 text-left">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">@Model.AppName</h3>
|
||||
<h3 class="text-lg font-medium">@Model.AppName</h3>
|
||||
@if (!string.IsNullOrEmpty(Model.AppUri))
|
||||
{
|
||||
<a href="@Model.AppUri" class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 transition-colors duration-200" target="_blank" rel="noopener noreferrer">
|
||||
<a href="@Model.AppUri" class="text-sm link link-primary" target="_blank" rel="noopener noreferrer">
|
||||
@Model.AppUri
|
||||
</a>
|
||||
}
|
||||
@ -38,77 +42,59 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<p class="mt-6 text-sm text-gray-600 dark:text-gray-300">
|
||||
wants to access your account with the following permissions:
|
||||
<p class="mt-6 text-sm text-center">
|
||||
When you authorize this application, you consent to the following permissions:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<ul class="border border-gray-200 dark:border-gray-700 rounded-lg divide-y divide-gray-200 dark:divide-gray-700 overflow-hidden">
|
||||
@if (Model.Scope != null)
|
||||
{
|
||||
var scopeDescriptions = new Dictionary<string, (string Name, string Description)>
|
||||
<div class="mt-4">
|
||||
<ul class="menu bg-base-200 rounded-box w-full">
|
||||
@if (Model.Scope != null)
|
||||
{
|
||||
["openid"] = ("OpenID", "Read your basic profile information"),
|
||||
["profile"] = ("Profile", "View your basic profile information"),
|
||||
["email"] = ("Email", "View your email address"),
|
||||
["offline_access"] = ("Offline Access", "Access your data while you're not using the application")
|
||||
};
|
||||
var scopeDescriptions = new Dictionary<string, (string Name, string Description)>
|
||||
{
|
||||
["openid"] = ("OpenID", "Read your basic profile information"),
|
||||
["profile"] = ("Profile", "View your basic profile information"),
|
||||
["email"] = ("Email", "View your email address"),
|
||||
["offline_access"] = ("Offline Access", "Access your data while you're not using the application")
|
||||
};
|
||||
|
||||
foreach (var scope in Model.Scope.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)))
|
||||
{
|
||||
var scopeInfo = scopeDescriptions.GetValueOrDefault(scope, (scope, scope.Replace('_', ' ')));
|
||||
<li class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 pt-0.5">
|
||||
<svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
foreach (var scope in Model.Scope.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)))
|
||||
{
|
||||
var scopeInfo = scopeDescriptions.GetValueOrDefault(scope, (scope, scope.Replace('_', ' ')));
|
||||
<li>
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-success" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">@scopeInfo.Item1</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">@scopeInfo.Item2</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<div>
|
||||
<p class="font-medium">@scopeInfo.Item1</p>
|
||||
<p class="text-xs text-base-content/70">@scopeInfo.Item2</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p>By authorizing, you allow this application to access your information on your behalf.</p>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-8 space-y-4">
|
||||
<input type="hidden" asp-for="ClientIdString" />
|
||||
<input type="hidden" asp-for="ResponseType" name="response_type" />
|
||||
<input type="hidden" asp-for="RedirectUri" name="redirect_uri" />
|
||||
<input type="hidden" asp-for="Scope" name="scope" />
|
||||
<input type="hidden" asp-for="State" name="state" />
|
||||
<input type="hidden" asp-for="Nonce" name="nonce" />
|
||||
<input type="hidden" asp-for="ReturnUrl" name="returnUrl" />
|
||||
<input type="hidden" name="code_challenge" value="@HttpContext.Request.Query["code_challenge"]" />
|
||||
<input type="hidden" name="code_challenge_method" value="@HttpContext.Request.Query["code_challenge_method"]" />
|
||||
<input type="hidden" name="response_mode" value="@HttpContext.Request.Query["response_mode"]" />
|
||||
|
||||
<div class="flex flex-col space-y-3">
|
||||
<button type="submit" name="allow" value="true"
|
||||
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800 transition-colors duration-200">
|
||||
Allow
|
||||
</button>
|
||||
<button type="submit" name="allow" value="false"
|
||||
class="w-full flex justify-center py-2.5 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800 transition-colors duration-200">
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-xs text-center text-gray-500 dark:text-gray-400">
|
||||
You can change these permissions later in your account settings.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" class="mt-8 space-y-4">
|
||||
<input type="hidden" asp-for="ClientIdString" />
|
||||
<input type="hidden" asp-for="ResponseType" name="response_type" />
|
||||
<input type="hidden" asp-for="RedirectUri" name="redirect_uri" />
|
||||
<input type="hidden" asp-for="Scope" name="scope" />
|
||||
<input type="hidden" asp-for="State" name="state" />
|
||||
<input type="hidden" asp-for="Nonce" name="nonce" />
|
||||
<input type="hidden" asp-for="ReturnUrl" name="returnUrl" />
|
||||
<input type="hidden" asp-for="CodeChallenge" value="@HttpContext.Request.Query["code_challenge"]" />
|
||||
<input type="hidden" asp-for="CodeChallengeMethod" value="@HttpContext.Request.Query["code_challenge_method"]" />
|
||||
<input type="hidden" asp-for="ResponseMode" value="@HttpContext.Request.Query["response_mode"]" />
|
||||
|
||||
<div class="card-actions justify-center flex gap-4">
|
||||
<button type="submit" name="allow" value="true" class="btn btn-primary flex-1">Allow</button>
|
||||
<button type="submit" name="allow" value="false" class="btn btn-ghost flex-1">Deny</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -124,4 +110,4 @@
|
||||
_ => scope
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,12 @@
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="max-w-lg w-full mx-auto p-6 text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Authentication Successful</h1>
|
||||
<p class="mb-6 text-gray-900 dark:text-white">You can now close this window and return to the application.</p>
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">Authentication Successful</h1>
|
||||
<p class="py-6">You can now close this window and return to the application.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -44,4 +46,4 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
@page "/web/auth/challenge/{id:guid}"
|
||||
@page "//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");
|
||||
Response.Redirect($"//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 class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<p class="py-6">Redirecting to authentication page...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,36 +1,36 @@
|
||||
@page "/web/auth/login"
|
||||
@page "//auth/login"
|
||||
@model DysonNetwork.Sphere.Pages.Auth.LoginModel
|
||||
@{
|
||||
ViewData["Title"] = "Login";
|
||||
ViewData["Title"] = "Login | Solar Network";
|
||||
var returnUrl = Model.ReturnUrl ?? "";
|
||||
}
|
||||
|
||||
<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">
|
||||
<input type="hidden" asp-for="ReturnUrl" value="@returnUrl" />
|
||||
<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 class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content w-full max-w-md">
|
||||
<div class="card w-full bg-base-100 shadow-xl">
|
||||
<div class="card-body px-8 py-7">
|
||||
<h1 class="card-title justify-center text-2xl font-bold">Welcome back!</h1>
|
||||
<p class="text-center">Login to your Solar Network account to continue.</p>
|
||||
<form method="post" class="mt-4">
|
||||
<input type="hidden" asp-for="ReturnUrl" value="@returnUrl"/>
|
||||
<div class="form-control">
|
||||
<label class="label" asp-for="Username">
|
||||
<span class="label-text">Username</span>
|
||||
</label>
|
||||
<input asp-for="Username" class="input input-bordered w-full"/>
|
||||
<span asp-validation-for="Username" class="text-error text-sm mt-1"></span>
|
||||
</div>
|
||||
<div class="form-control mt-6">
|
||||
<button type="submit" class="btn btn-primary w-full">Next</button>
|
||||
</div>
|
||||
<div class="text-sm text-center mt-4">
|
||||
<span class="text-base-content/70">Have no account?</span> <br/>
|
||||
<a href="https://solian.app/#/auth/create-account" class="link link-primary">
|
||||
Create a new account →
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</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 class="mt-8 flex flex-col text-sm text-center">
|
||||
<span class="text-gray-900 dark:text-white opacity-80">Have no account?</span>
|
||||
|
||||
<a href="https://solian.app/#/auth/create-account"
|
||||
class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Create a new account →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,58 +1,104 @@
|
||||
@page "/web/auth/challenge/{id:guid}/select-factor"
|
||||
@page "//auth/challenge/{id:guid}/select-factor"
|
||||
@using DysonNetwork.Sphere.Account
|
||||
@model DysonNetwork.Sphere.Pages.Auth.SelectFactorModel
|
||||
@{
|
||||
ViewData["Title"] = "Select Authentication Method";
|
||||
ViewData["Title"] = "Select Authentication Method | Solar Network";
|
||||
}
|
||||
|
||||
<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>
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content w-full max-w-md">
|
||||
<div class="card w-full bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title justify-center text-2xl font-bold">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)
|
||||
@if (Model.AuthChallenge != null && Model.AuthChallenge.StepRemain > 0)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<form method="post" asp-page-handler="SelectFactor" class="w-full" id="factor-@factor.Id">
|
||||
<input type="hidden" name="SelectedFactorId" 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 class="text-center mt-4">
|
||||
<p class="text-sm text-info mb-2">Progress: @(Model.AuthChallenge.StepTotal - Model.AuthChallenge.StepRemain) of @Model.AuthChallenge.StepTotal steps completed</p>
|
||||
<progress class="progress progress-info w-full" value="@(Model.AuthChallenge.StepTotal - Model.AuthChallenge.StepRemain)" max="@Model.AuthChallenge.StepTotal"></progress>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.AuthChallenge == null)
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Challenge not found or expired.</span>
|
||||
</div>
|
||||
}
|
||||
else if (Model.AuthChallenge.StepRemain == 0)
|
||||
{
|
||||
<div class="alert alert-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Challenge completed. Redirecting...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-center">Please select an authentication method:</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach (var factor in Model.AuthFactors)
|
||||
{
|
||||
<form method="post" asp-page-handler="SelectFactor" class="w-full" id="factor-@factor.Id">
|
||||
<input type="hidden" name="SelectedFactorId" value="@factor.Id"/>
|
||||
|
||||
@if (factor.Type == AccountAuthFactorType.EmailCode)
|
||||
{
|
||||
<div class="card w-full bg-base-200 card-sm shadow-sm rounded-md">
|
||||
<div class="py-4 px-5 align-items-center">
|
||||
<div>
|
||||
<h2 class="card-title">@GetFactorDisplayName(factor.Type)</h2>
|
||||
<p>@GetFactorDescription(factor.Type)</p>
|
||||
</div>
|
||||
<div class="join w-full mt-2">
|
||||
<div class="flex-1">
|
||||
<label class="input join-item input-sm">
|
||||
<input id="hint-@factor.Id" type="email"
|
||||
placeholder="mail@site.com" required/>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary join-item btn-sm">
|
||||
<span class="material-symbols-outlined">
|
||||
arrow_right_alt
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card w-full bg-base-200 card-sm shadow-sm rounded-md">
|
||||
<div class="flex py-4 px-5 align-items-center">
|
||||
<div class="flex-1">
|
||||
<h2 class="card-title">@GetFactorDisplayName(factor.Type)</h2>
|
||||
<p>@GetFactorDescription(factor.Type)</p>
|
||||
</div>
|
||||
<div class="justify-end card-actions">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<span class="material-symbols-outlined">
|
||||
arrow_right_alt
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -78,4 +124,4 @@
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
}
|
||||
}
|
@ -1,77 +1,99 @@
|
||||
@page "/web/auth/challenge/{id:guid}/verify/{factorId:guid}"
|
||||
@page "//auth/challenge/{id:guid}/verify/{factorId:guid}"
|
||||
@using DysonNetwork.Sphere.Account
|
||||
@model DysonNetwork.Sphere.Pages.Auth.VerifyFactorModel
|
||||
@{
|
||||
ViewData["Title"] = "Verify Your Identity";
|
||||
ViewData["Title"] = "Verify Your Identity | Solar Network";
|
||||
}
|
||||
|
||||
<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>
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content w-full max-w-md">
|
||||
<div class="card w-full bg-base-100 shadow-xl">
|
||||
<div class="card-body px-8 py-7">
|
||||
<h1 class="card-title justify-center text-2xl font-bold">Verify Your Identity</h1>
|
||||
<p class="text-center">
|
||||
@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"
|
||||
type="password"
|
||||
autofocus />
|
||||
<span asp-validation-for="Code" class="text-red-500 text-sm mt-1"></span>
|
||||
</div>
|
||||
@if (Model.AuthChallenge != null && Model.AuthChallenge.StepRemain > 0)
|
||||
{
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-sm text-info mb-2">Progress: @(Model.AuthChallenge.StepTotal - Model.AuthChallenge.StepRemain) of @Model.AuthChallenge.StepTotal steps completed</p>
|
||||
<progress class="progress progress-info w-full" value="@(Model.AuthChallenge.StepTotal - Model.AuthChallenge.StepRemain)" max="@Model.AuthChallenge.StepTotal"></progress>
|
||||
</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>
|
||||
@if (Model.AuthChallenge == null)
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>Challenge not found or expired.</span>
|
||||
</div>
|
||||
}
|
||||
else if (Model.AuthChallenge.StepRemain == 0)
|
||||
{
|
||||
<div class="alert alert-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>Verification successful. Redirecting...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" class="space-y-4">
|
||||
@if (!ViewData.ModelState.IsValid && ViewData.ModelState.Any(m => m.Value.Errors.Any()))
|
||||
{
|
||||
<div role="alert" class="alert alert-error mb-4">
|
||||
<span>@Html.ValidationSummary(true)</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-control">
|
||||
<label asp-for="Code" class="label">
|
||||
<span class="label-text">@(Model.FactorType == AccountAuthFactorType.Password ? "Use your password" : "Verification Code")</span>
|
||||
</label>
|
||||
<input asp-for="Code"
|
||||
class="input input-bordered w-full"
|
||||
autocomplete="one-time-code"
|
||||
type="password"
|
||||
autofocus />
|
||||
<span asp-validation-for="Code" class="text-error text-sm mt-1"></span>
|
||||
</div>
|
||||
|
||||
<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 class="form-control mt-6">
|
||||
<button type="submit" class="btn btn-primary w-full">Verify</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a asp-page="SelectFactor" asp-route-id="@Model.Id" class="link link-primary text-sm">
|
||||
← Back to authentication methods
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||
}
|
||||
}
|
@ -32,16 +32,24 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ReturnUrl))
|
||||
{
|
||||
TempData["ReturnUrl"] = ReturnUrl;
|
||||
}
|
||||
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();
|
||||
if (AuthChallenge.StepRemain == 0) return await ExchangeTokenAndRedirect(AuthChallenge);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ReturnUrl))
|
||||
{
|
||||
TempData["ReturnUrl"] = ReturnUrl;
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadChallengeAndFactor();
|
||||
@ -52,6 +60,12 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
if (AuthChallenge == null) return NotFound("Challenge not found or expired.");
|
||||
if (Factor == null) return NotFound("Authentication method not found.");
|
||||
|
||||
if (AuthChallenge.BlacklistFactors.Contains(Factor.Id))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "This authentication method has already been used for this challenge.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await accounts.VerifyFactorCode(Factor, Code))
|
||||
@ -79,7 +93,7 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
{ "account_id", AuthChallenge.AccountId }
|
||||
}, Request, AuthChallenge.Account);
|
||||
|
||||
return await ExchangeTokenAndRedirect();
|
||||
return await ExchangeTokenAndRedirect(AuthChallenge);
|
||||
}
|
||||
|
||||
else
|
||||
@ -131,14 +145,10 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ExchangeTokenAndRedirect()
|
||||
private async Task<IActionResult> ExchangeTokenAndRedirect(Challenge challenge)
|
||||
{
|
||||
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.");
|
||||
await db.Entry(challenge).ReloadAsync();
|
||||
if (challenge.StepRemain != 0) return BadRequest($"Challenge not yet completed. Remaining steps: {challenge.StepRemain}");
|
||||
|
||||
var session = await db.AuthSessions
|
||||
.FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id);
|
||||
@ -160,7 +170,7 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
Response.Cookies.Append(AuthConstants.CookieTokenName, token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = !configuration.GetValue<bool>("Debug"),
|
||||
Secure = Request.IsHttps,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Path = "/"
|
||||
});
|
||||
|
@ -38,73 +38,73 @@
|
||||
</script>
|
||||
}
|
||||
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="max-w-lg w-full mx-auto p-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Security Check</h1>
|
||||
<p class="mb-6 text-gray-900 dark:text-white">Please complete the contest below to confirm you're not a robot</p>
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Security Check</h1>
|
||||
<p>Please complete the contest below to confirm you're not a robot</p>
|
||||
|
||||
<div class="flex justify-center mb-8">
|
||||
@switch (provider)
|
||||
{
|
||||
case "cloudflare":
|
||||
<div class="cf-turnstile"
|
||||
data-sitekey="@apiKey"
|
||||
data-callback="onSuccess">
|
||||
<div class="flex justify-center my-8">
|
||||
@switch (provider)
|
||||
{
|
||||
case "cloudflare":
|
||||
<div class="cf-turnstile"
|
||||
data-sitekey="@apiKey"
|
||||
data-callback="onSuccess">
|
||||
</div>
|
||||
break;
|
||||
case "recaptcha":
|
||||
<div class="g-recaptcha"
|
||||
data-sitekey="@apiKey"
|
||||
data-callback="onSuccess">
|
||||
</div>
|
||||
break;
|
||||
case "hcaptcha":
|
||||
<div class="h-captcha"
|
||||
data-sitekey="@apiKey"
|
||||
data-callback="onSuccess">
|
||||
</div>
|
||||
break;
|
||||
default:
|
||||
<div class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span>Captcha provider not configured correctly.</span>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm">
|
||||
<div class="font-semibold mb-1">Solar Network Anti-Robot</div>
|
||||
<div class="text-base-content/70">
|
||||
Powered by
|
||||
@switch (provider)
|
||||
{
|
||||
case "cloudflare":
|
||||
<a href="https://www.cloudflare.com/turnstile/" class="link link-hover">
|
||||
Cloudflare Turnstile
|
||||
</a>
|
||||
break;
|
||||
case "recaptcha":
|
||||
<a href="https://www.google.com/recaptcha/" class="link link-hover">
|
||||
Google reCaptcha
|
||||
</a>
|
||||
break;
|
||||
default:
|
||||
<span>Nothing</span>
|
||||
break;
|
||||
}
|
||||
<br/>
|
||||
Hosted by
|
||||
<a href="https://github.com/Solsynth/DysonNetwork" class="link link-hover">
|
||||
DysonNetwork.Sphere
|
||||
</a>
|
||||
</div>
|
||||
break;
|
||||
case "recaptcha":
|
||||
<div class="g-recaptcha"
|
||||
data-sitekey="@apiKey"
|
||||
data-callback="onSuccess">
|
||||
</div>
|
||||
break;
|
||||
case "hcaptcha":
|
||||
<div class="h-captcha"
|
||||
data-sitekey="@apiKey"
|
||||
data-callback="onSuccess">
|
||||
</div>
|
||||
break;
|
||||
default:
|
||||
<div class="p-4 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
||||
<p class="text-yellow-800 dark:text-yellow-200">
|
||||
Captcha provider not configured correctly.
|
||||
</p>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center text-sm">
|
||||
<div class="font-semibold text-gray-700 dark:text-gray-300 mb-1">Solar Network Anti-Robot</div>
|
||||
<div class="text-gray-600 dark:text-gray-400">
|
||||
Powered by
|
||||
@switch (provider)
|
||||
{
|
||||
case "cloudflare":
|
||||
<a href="https://www.cloudflare.com/turnstile/"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
Cloudflare Turnstile
|
||||
</a>
|
||||
break;
|
||||
case "recaptcha":
|
||||
<a href="https://www.google.com/recaptcha/"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
Google reCaptcha
|
||||
</a>
|
||||
break;
|
||||
default:
|
||||
<span>Nothing</span>
|
||||
break;
|
||||
}
|
||||
<br/>
|
||||
Hosted by
|
||||
<a href="https://github.com/Solsynth/DysonNetwork"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
DysonNetwork.Sphere
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,29 +1,23 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "The Solar Network";
|
||||
ViewData["Title"] = "The Solar Network | Solar Network";
|
||||
}
|
||||
|
||||
<div class="container-default h-full text-center flex flex-col justify-center items-center">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl">
|
||||
Solar Network
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300">
|
||||
This Solar Network instance is up and running.
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="https://sn.solsynth.dev" target="_blank" class="btn-primary">
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-x-6 mt-6">
|
||||
<a href="/swagger" target="_blank" class="btn-text">
|
||||
<span aria-hidden="true">λ </span> API Docs
|
||||
</a>
|
||||
<a href="https://kb.solsynth.dev" target="_blank" class="btn-text">
|
||||
Learn more <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">Solar Network</h1>
|
||||
<p class="py-6">This Solar Network instance is up and running.</p>
|
||||
<a href="https://sn.solsynth.dev" target="_blank" class="btn btn-primary">Get started</a>
|
||||
<div class="flex items-center justify-center gap-x-6 mt-6">
|
||||
<a href="/swagger" target="_blank" class="btn btn-ghost">
|
||||
<span aria-hidden="true">λ </span> API Docs
|
||||
</a>
|
||||
<a href="https://kb.solsynth.dev" target="_blank" class="btn btn-ghost">
|
||||
Learn more <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,5 @@ public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
// Add any page initialization logic here
|
||||
}
|
||||
}
|
67
DysonNetwork.Sphere/Pages/Posts/PostDetail.cshtml
Normal file
67
DysonNetwork.Sphere/Pages/Posts/PostDetail.cshtml
Normal file
@ -0,0 +1,67 @@
|
||||
@page "/posts/{PostId:guid}"
|
||||
@model DysonNetwork.Sphere.Pages.Posts.PostDetailModel
|
||||
@using Markdig
|
||||
@{
|
||||
ViewData["Title"] = Model.Post?.Title + " | Solar Network";
|
||||
var imageUrl = Model.Post?.Attachments?.FirstOrDefault(a => a.MimeType.StartsWith("image/"))?.Id;
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<meta property="og:title" content="@Model.Post?.Title" />
|
||||
<meta property="og:type" content="article" />
|
||||
@if (imageUrl != null)
|
||||
{
|
||||
<meta property="og:image" content="/api/files/@imageUrl" />
|
||||
}
|
||||
<meta property="og:url" content="@Request.Scheme://@Request.Host@Request.Path" />
|
||||
<meta property="og:description" content="@Model.Post?.Description" />
|
||||
}
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
@if (Model.Post != null)
|
||||
{
|
||||
<h1 class="text-3xl font-bold mb-4">@Model.Post.Title</h1>
|
||||
<p class="text-gray-600 mb-2">
|
||||
Created at: @Model.Post.CreatedAt
|
||||
@if (Model.Post.Publisher?.Account != null)
|
||||
{
|
||||
<span>by <a href="#" class="text-blue-500">@@@Model.Post.Publisher.Name</a></span>
|
||||
}
|
||||
</p>
|
||||
<div class="prose lg:prose-xl mb-4">
|
||||
@Html.Raw(Markdown.ToHtml(Model.Post.Content ?? string.Empty))
|
||||
</div>
|
||||
|
||||
@if (Model.Post.Attachments != null && Model.Post.Attachments.Any())
|
||||
{
|
||||
<h2 class="text-2xl font-bold mb-2">Attachments</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach (var attachment in Model.Post.Attachments)
|
||||
{
|
||||
<div class="border p-2 rounded-md">
|
||||
@if (attachment.MimeType != null && attachment.MimeType.StartsWith("image/"))
|
||||
{
|
||||
<img src="/api/files/@attachment.Id" alt="@attachment.Name" class="w-full h-auto object-cover mb-2" />
|
||||
}
|
||||
else if (attachment.MimeType != null && attachment.MimeType.StartsWith("video/"))
|
||||
{
|
||||
<video controls class="w-full h-auto object-cover mb-2">
|
||||
<source src="/api/files/@attachment.Id" type="@attachment.MimeType">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
}
|
||||
<a href="/api/files/@attachment.Id" target="_blank" class="text-blue-500 hover:underline">
|
||||
@attachment.Name
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
<span>Post not found.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
45
DysonNetwork.Sphere/Pages/Posts/PostDetail.cshtml.cs
Normal file
45
DysonNetwork.Sphere/Pages/Posts/PostDetail.cshtml.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Pages.Posts;
|
||||
|
||||
public class PostDetailModel(
|
||||
AppDatabase db,
|
||||
PublisherService pub,
|
||||
RelationshipService rels
|
||||
) : PageModel
|
||||
{
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public Guid PostId { get; set; }
|
||||
|
||||
public Post.Post? Post { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
if (PostId == Guid.Empty)
|
||||
return NotFound();
|
||||
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Sphere.Account.Account;
|
||||
var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser);
|
||||
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id);
|
||||
|
||||
Post = await db.Posts
|
||||
.Where(e => e.Id == PostId)
|
||||
.Include(e => e.Publisher)
|
||||
.ThenInclude(p => p.Account)
|
||||
.Include(e => e.Tags)
|
||||
.Include(e => e.Categories)
|
||||
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (Post == null)
|
||||
return NotFound();
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
@ -5,34 +5,54 @@
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>@ViewData["Title"]</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap"
|
||||
rel="stylesheet">
|
||||
<link rel="stylesheet" href="~/css/styles.css" asp-append-version="true"/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
@await RenderSectionAsync("Head", required: false)
|
||||
</head>
|
||||
<body class="h-[calc(100dvh-118px)] mt-[64px] bg-white dark:bg-gray-900">
|
||||
<header class="bg-white dark:bg-gray-800 shadow-sm fixed left-0 right-0 top-0 z-50">
|
||||
<nav class="container-default">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<div class="flex">
|
||||
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white">Solar Network</a>
|
||||
</div>
|
||||
<div class="flex items-center ml-auto">
|
||||
@if (Context.Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out _))
|
||||
{
|
||||
<a href="/web/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>
|
||||
<body class="h-full bg-base-200">
|
||||
<header class="navbar bg-base-100/35 backdrop-blur-md shadow-xl fixed left-0 right-0 top-0 z-50 px-5">
|
||||
<div class="flex-1">
|
||||
<a class="btn btn-ghost text-xl">Solar Network</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<ul class="menu menu-horizontal menu-sm px-1">
|
||||
@if (Context.Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out _))
|
||||
{
|
||||
<li class="tooltip tooltip-bottom" data-tip="Profile">
|
||||
<a href="//account/profile">
|
||||
<span class="material-symbols-outlined">account_circle</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tooltip tooltip-bottom" data-tip="Logout">
|
||||
<form method="post" asp-page="/Account/Profile" asp-page-handler="Logout">
|
||||
<button type="submit">
|
||||
<span class="material-symbols-outlined">
|
||||
logout
|
||||
</span>
|
||||
</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>
|
||||
</nav>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="tooltip tooltip-bottom" data-tip="Login">
|
||||
<a href="//auth/login"><span class="material-symbols-outlined">login</span></a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@* The header 64px *@
|
||||
<main class="h-full">
|
||||
<main class="h-full pt-16">
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
||||
|
@ -6,96 +6,86 @@
|
||||
ViewData["Title"] = "Magic Spell";
|
||||
}
|
||||
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="max-w-lg w-full mx-auto p-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Magic Spell</h1>
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold mb-4">Magic Spell</h1>
|
||||
|
||||
@if (Model.IsSuccess)
|
||||
{
|
||||
<div class="p-4 bg-green-100 dark:bg-green-900 rounded-lg mb-6">
|
||||
<p class="text-green-800 dark:text-green-200">The spell was applied successfully!</p>
|
||||
<p class="text-green-800 dark:text-green-200 opacity-80">Now you can close this page.</p>
|
||||
<div class="alert alert-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>The spell was applied successfully!</span>
|
||||
<p>Now you can close this page.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.CurrentSpell == null)
|
||||
{
|
||||
<div class="p-4 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
||||
<p class="text-yellow-800 dark:text-yellow-200">The spell was expired or does not exist.</p>
|
||||
<div class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span>The spell was expired or does not exist.</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div
|
||||
class="px-4 py-12 bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-lg rounded-lg mb-6">
|
||||
<div class="mb-2">
|
||||
<p>
|
||||
<span class="font-medium">The spell is for </span>
|
||||
<span
|
||||
class="font-bold">@System.Text.RegularExpressions.Regex.Replace(Model.CurrentSpell!.Type.ToString(), "([a-z])([A-Z])", "$1 $2")</span>
|
||||
</p>
|
||||
<p><span class="font-medium">for @@</span>@Model.CurrentSpell.Account?.Name</p>
|
||||
</div>
|
||||
<div class="text-sm opacity-80">
|
||||
@if (Model.CurrentSpell.ExpiresAt.HasValue)
|
||||
{
|
||||
<p>Available until @Model.CurrentSpell.ExpiresAt.Value.ToDateTimeUtc().ToString("g")</p>
|
||||
}
|
||||
@if (Model.CurrentSpell.AffectedAt.HasValue)
|
||||
{
|
||||
<p>Available after @Model.CurrentSpell.AffectedAt.Value.ToDateTimeUtc().ToString("g")</p>
|
||||
}
|
||||
</div>
|
||||
<p class="text-sm opacity-80">Would you like to apply this spell?</p>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
<input type="hidden" asp-for="CurrentSpell!.Id"/>
|
||||
|
||||
@if (Model.CurrentSpell?.Type == MagicSpellType.AuthPasswordReset)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<label
|
||||
asp-for="NewPassword"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<input type="password"
|
||||
asp-for="NewPassword"
|
||||
required
|
||||
minlength="8"
|
||||
style="padding: 0.5rem 1rem"
|
||||
placeholder="Your new password"
|
||||
class="w-full border-2 border-gray-300 dark:border-gray-600 rounded-lg
|
||||
focus:ring-2 focus:ring-blue-400
|
||||
dark:text-white bg-gray-100 dark:bg-gray-800"/>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
@System.Text.RegularExpressions.Regex.Replace(Model.CurrentSpell!.Type.ToString(), "([a-z])([A-Z])", "$1 $2")
|
||||
</h2>
|
||||
<p>for @@ @Model.CurrentSpell.Account?.Name</p>
|
||||
<div class="text-sm opacity-80">
|
||||
@if (Model.CurrentSpell.ExpiresAt.HasValue)
|
||||
{
|
||||
<p>Available until @Model.CurrentSpell.ExpiresAt.Value.ToDateTimeUtc().ToString("g")</p>
|
||||
}
|
||||
@if (Model.CurrentSpell.AffectedAt.HasValue)
|
||||
{
|
||||
<p>Available after @Model.CurrentSpell.AffectedAt.Value.ToDateTimeUtc().ToString("g")</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="text-sm opacity-80">Would you like to apply this spell?</p>
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
<input type="hidden" asp-for="CurrentSpell!.Id"/>
|
||||
|
||||
<button type="submit"
|
||||
class="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors
|
||||
transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||
Apply
|
||||
</button>
|
||||
</form>
|
||||
@if (Model.CurrentSpell?.Type == MagicSpellType.AuthPasswordReset)
|
||||
{
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label" asp-for="NewPassword">
|
||||
<span class="label-text">New Password</span>
|
||||
</label>
|
||||
<input type="password"
|
||||
asp-for="NewPassword"
|
||||
required
|
||||
minlength="8"
|
||||
placeholder="Your new password"
|
||||
class="input input-bordered w-full max-w-xs"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button type="submit" class="btn btn-primary">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center text-sm">
|
||||
<div class="font-semibold text-gray-700 dark:text-gray-300 mb-1">Solar Network</div>
|
||||
<div class="text-gray-600 dark:text-gray-400">
|
||||
<a href="https://solsynth.dev" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
Solsynth LLC
|
||||
</a>
|
||||
© @DateTime.Now.Year
|
||||
<br/>
|
||||
Powered by
|
||||
<a href="https://github.com/Solsynth/DysonNetwork"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
DysonNetwork.Sphere
|
||||
</a>
|
||||
|
||||
<div class="mt-8 text-center text-sm">
|
||||
<div class="font-semibold mb-1">Solar Network</div>
|
||||
<div class="text-base-content/70">
|
||||
<a href="https://solsynth.dev" class="link link-hover">
|
||||
Solsynth LLC
|
||||
</a>
|
||||
© @DateTime.Now.Year
|
||||
<br/>
|
||||
Powered by
|
||||
<a href="https://github.com/Solsynth/DysonNetwork" class="link link-hover">
|
||||
DysonNetwork.Sphere
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Pages.Posts;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
@ -13,7 +14,7 @@ using NpgsqlTypes;
|
||||
namespace DysonNetwork.Sphere.Post;
|
||||
|
||||
[ApiController]
|
||||
[Route("/posts")]
|
||||
[Route("/api/posts")]
|
||||
public class PostController(
|
||||
AppDatabase db,
|
||||
PostService ps,
|
||||
@ -65,6 +66,9 @@ public class PostController(
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<Post>> GetPost(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["IsWebPage"] as bool? ?? true)
|
||||
return RedirectToPage("/Posts/PostDetail", new { PostId = id });
|
||||
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Account.Account;
|
||||
var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser);
|
||||
|
@ -11,7 +11,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Publisher;
|
||||
|
||||
[ApiController]
|
||||
[Route("/publishers")]
|
||||
[Route("/api/publishers")]
|
||||
public class PublisherController(
|
||||
AppDatabase db,
|
||||
PublisherService ps,
|
||||
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Publisher;
|
||||
|
||||
[ApiController]
|
||||
[Route("/publishers")]
|
||||
[Route("/api/publishers")]
|
||||
public class PublisherSubscriptionController(
|
||||
PublisherSubscriptionService subs,
|
||||
AppDatabase db,
|
||||
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Realm;
|
||||
|
||||
[ApiController]
|
||||
[Route("/realms/{slug}")]
|
||||
[Route("/api/realms/{slug}")]
|
||||
public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBase
|
||||
{
|
||||
[HttpGet("chat")]
|
||||
|
@ -9,7 +9,7 @@ using NodaTime;
|
||||
namespace DysonNetwork.Sphere.Realm;
|
||||
|
||||
[ApiController]
|
||||
[Route("/realms")]
|
||||
[Route("/api/realms")]
|
||||
public class RealmController(
|
||||
AppDatabase db,
|
||||
RealmService rs,
|
||||
|
@ -11,88 +11,228 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FortuneTipPositiveTitle_1" xml:space="preserve">
|
||||
<value>抽卡</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_1" xml:space="preserve">
|
||||
<value>次次出金</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_1" xml:space="preserve">
|
||||
<value>吃大保底</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_2" xml:space="preserve">
|
||||
<value>升段如破竹</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_2" xml:space="preserve">
|
||||
<value>掉分如山崩</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_3" xml:space="preserve">
|
||||
<value>欧气加身</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_3" xml:space="preserve">
|
||||
<value>十连皆寂</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_4" xml:space="preserve">
|
||||
<value>妙语连珠</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_4" xml:space="preserve">
|
||||
<value>谨言慎行</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_5" xml:space="preserve">
|
||||
<value>灵感如泉涌</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_5" xml:space="preserve">
|
||||
<value>下笔如千斤</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_6" xml:space="preserve">
|
||||
<value>0 error(s), 0 warning(s)</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_6" xml:space="preserve">
|
||||
<value>114 error(s), 514 warning(s)</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_7" xml:space="preserve">
|
||||
<value>购物</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_7" xml:space="preserve">
|
||||
<value>汇率低谷</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_7" xml:space="preserve">
|
||||
<value>开箱</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_7" xml:space="preserve">
|
||||
<value>225% 关税</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_1" xml:space="preserve">
|
||||
<value>抽卡</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_1" xml:space="preserve">
|
||||
<value>抽卡</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_1" xml:space="preserve">
|
||||
<value>次次出金</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_1" xml:space="preserve">
|
||||
<value>吃大保底</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_2" xml:space="preserve">
|
||||
<value>升段如破竹</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_2" xml:space="preserve">
|
||||
<value>掉分如山崩</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_3" xml:space="preserve">
|
||||
<value>欧气加身</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_3" xml:space="preserve">
|
||||
<value>十连皆寂</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_4" xml:space="preserve">
|
||||
<value>妙语连珠</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_4" xml:space="preserve">
|
||||
<value>谨言慎行</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_5" xml:space="preserve">
|
||||
<value>灵感如泉涌</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_5" xml:space="preserve">
|
||||
<value>下笔如千斤</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_6" xml:space="preserve">
|
||||
<value>0 error(s), 0 warning(s)</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_6" xml:space="preserve">
|
||||
<value>114 error(s), 514 warning(s)</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_7" xml:space="preserve">
|
||||
<value>购物</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_7" xml:space="preserve">
|
||||
<value>汇率低谷</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_7" xml:space="preserve">
|
||||
<value>开箱</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_7" xml:space="preserve">
|
||||
<value>225% 关税</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_1" xml:space="preserve">
|
||||
<value>抽卡</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_8" xml:space="preserve">
|
||||
<value>学习</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_8" xml:space="preserve">
|
||||
<value>效率200%</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_8" xml:space="preserve">
|
||||
<value>学习</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_8" xml:space="preserve">
|
||||
<value>效率50%</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_9" xml:space="preserve">
|
||||
<value>编曲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_9" xml:space="preserve">
|
||||
<value>灵感爆棚</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_9" xml:space="preserve">
|
||||
<value>编曲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_9" xml:space="preserve">
|
||||
<value>FL Studio未响应</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_10" xml:space="preserve">
|
||||
<value>摄影</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_10" xml:space="preserve">
|
||||
<value>刀锐奶化</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_10" xml:space="preserve">
|
||||
<value>摄影</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_10" xml:space="preserve">
|
||||
<value>"NO CARD"</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_11" xml:space="preserve">
|
||||
<value>焊 PCB</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_11" xml:space="preserve">
|
||||
<value>上电,启动,好耶!</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_11" xml:space="preserve">
|
||||
<value>焊 PCB</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_11" xml:space="preserve">
|
||||
<value>斯~不烫</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_12" xml:space="preserve">
|
||||
<value>AE 启动</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_12" xml:space="preserve">
|
||||
<value>帧渲染时间 20ms</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_12" xml:space="preserve">
|
||||
<value>AE 启动</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_12" xml:space="preserve">
|
||||
<value>咩~</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_13" xml:space="preserve">
|
||||
<value>航拍</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_13" xml:space="preserve">
|
||||
<value>”可以起飞“</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_13" xml:space="preserve">
|
||||
<value>航拍</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_13" xml:space="preserve">
|
||||
<value>”信号弱,请调整天线/飞机遭遇强风/飞机 GPS 受干扰“</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_14" xml:space="preserve">
|
||||
<value>调色</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_14" xml:space="preserve">
|
||||
<value>色彩准确强如怪,拼尽全力无法战胜</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_14" xml:space="preserve">
|
||||
<value>调色</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_14" xml:space="preserve">
|
||||
<value>甲:我要五彩斑斓的黑</value>
|
||||
<comment/>
|
||||
</data>
|
||||
</root>
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace DysonNetwork.Sphere.Safety;
|
||||
|
||||
[ApiController]
|
||||
[Route("/safety/reports")]
|
||||
[Route("/api/safety/reports")]
|
||||
public class AbuseReportController(
|
||||
SafetyService safety
|
||||
) : ControllerBase
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
@ -14,6 +15,7 @@ public static class ApplicationConfiguration
|
||||
{
|
||||
app.MapMetrics();
|
||||
app.MapOpenApi();
|
||||
app.UseMiddleware<ClientTypeMiddleware>();
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
@ -34,6 +36,7 @@ public static class ApplicationConfiguration
|
||||
app.UseWebSockets();
|
||||
app.UseRateLimiter();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<PermissionMiddleware>();
|
||||
|
||||
|
@ -229,7 +229,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<WalletService>();
|
||||
services.AddScoped<SubscriptionService>();
|
||||
services.AddScoped<PaymentService>();
|
||||
services.AddScoped<IRealtimeService, LivekitRealtimeService>();
|
||||
services.AddScoped<RealtimeStatusService>();
|
||||
services.AddRealtimeService(configuration);
|
||||
services.AddScoped<WebReaderService>();
|
||||
services.AddScoped<WebFeedService>();
|
||||
services.AddScoped<AfdianPaymentHandler>();
|
||||
@ -242,4 +243,22 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRealtimeService(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var provider = configuration["Realtime:Provider"];
|
||||
switch (provider)
|
||||
{
|
||||
case "Cloudflare":
|
||||
services.AddHttpClient<IRealtimeService, CloudflareRealtimeService>();
|
||||
break;
|
||||
case "LiveKit":
|
||||
services.AddScoped<IRealtimeService, LiveKitRealtimeService>();
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Realtime provider '{provider}' is not supported.");
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Sticker;
|
||||
|
||||
[ApiController]
|
||||
[Route("/stickers")]
|
||||
[Route("/api/stickers")]
|
||||
public class StickerController(AppDatabase db, StickerService st) : ControllerBase
|
||||
{
|
||||
private async Task<IActionResult> _CheckStickerPackPermissions(Guid packId, Account.Account currentUser,
|
||||
|
@ -7,7 +7,7 @@ using Minio.DataModel.Args;
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
|
||||
[ApiController]
|
||||
[Route("/files")]
|
||||
[Route("/api/files")]
|
||||
public class FileController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
@ -17,13 +17,27 @@ public class FileController(
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult> OpenFile(string id, [FromQuery] bool original = false)
|
||||
public async Task<ActionResult> OpenFile(
|
||||
string id,
|
||||
[FromQuery] bool download = false,
|
||||
[FromQuery] bool original = false,
|
||||
[FromQuery] string? overrideMimeType = null
|
||||
)
|
||||
{
|
||||
// Support the file extension for client side data recognize
|
||||
string? fileExtension = null;
|
||||
if (id.Contains("."))
|
||||
{
|
||||
var splitedId = id.Split('.');
|
||||
id = splitedId.First();
|
||||
fileExtension = splitedId.Last();
|
||||
}
|
||||
|
||||
var file = await fs.GetFileAsync(id);
|
||||
if (file is null) return NotFound();
|
||||
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(file.StorageUrl)) return Redirect(file.StorageUrl);
|
||||
|
||||
|
||||
if (file.UploadedTo is null)
|
||||
{
|
||||
var tusStorePath = configuration.GetValue<string>("Tus:StorePath")!;
|
||||
@ -61,12 +75,33 @@ public class FileController(
|
||||
return BadRequest(
|
||||
"Failed to configure client for remote destination, file got an invalid storage remote.");
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
if (fileExtension is not null)
|
||||
{
|
||||
if (MimeTypes.TryGetMimeType(fileExtension, out var mimeType))
|
||||
headers.Add("Response-Content-Type", mimeType);
|
||||
}
|
||||
else if (overrideMimeType is not null)
|
||||
{
|
||||
headers.Add("Response-Content-Type", overrideMimeType);
|
||||
}
|
||||
else if (file.MimeType is not null && !file.MimeType!.EndsWith("unknown"))
|
||||
{
|
||||
headers.Add("Response-Content-Type", file.MimeType);
|
||||
}
|
||||
|
||||
if (download)
|
||||
{
|
||||
headers.Add("Response-Content-Disposition", $"attachment; filename=\"{file.Name}\"");
|
||||
}
|
||||
|
||||
var bucket = dest.Bucket;
|
||||
var openUrl = await client.PresignedGetObjectAsync(
|
||||
new PresignedGetObjectArgs()
|
||||
.WithBucket(bucket)
|
||||
.WithObject(fileName)
|
||||
.WithExpiry(3600)
|
||||
.WithHeaders(headers)
|
||||
);
|
||||
|
||||
return Redirect(openUrl);
|
||||
@ -107,7 +142,7 @@ public class FileController(
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("/maintenance/migrateReferences")]
|
||||
[Authorize]
|
||||
[RequiredPermission("maintenance", "files.references")]
|
||||
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
[ApiController]
|
||||
[Route("/orders")]
|
||||
[Route("/api/orders")]
|
||||
public class OrderController(PaymentService payment, AuthService auth, AppDatabase db) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
|
@ -2,6 +2,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||
@ -51,7 +52,7 @@ public class AfdianPaymentHandler(
|
||||
var sign = CalculateSign(token, userId, paramsJson, ts);
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/open/query-order")
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/open/query-order")
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(new
|
||||
{
|
||||
@ -107,7 +108,7 @@ public class AfdianPaymentHandler(
|
||||
var sign = CalculateSign(token, userId, paramsJson, ts);
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/open/query-order")
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/open/query-order")
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(new
|
||||
{
|
||||
@ -176,7 +177,7 @@ public class AfdianPaymentHandler(
|
||||
var sign = CalculateSign(token, userId, paramsJson, ts);
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/open/query-order")
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://afdian.com/api/open/query-order")
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(new
|
||||
{
|
||||
@ -442,4 +443,4 @@ public class SkuDetailItem
|
||||
[JsonPropertyName("album_id")] public string AlbumId { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("pic")] public string Picture { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
[ApiController]
|
||||
[Route("/subscriptions")]
|
||||
[Route("/api/subscriptions")]
|
||||
public class SubscriptionController(SubscriptionService subscriptions, AfdianPaymentHandler afdian, AppDatabase db) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
@ -180,11 +180,12 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
}
|
||||
|
||||
[HttpPost("order/restore/afdian")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> RestorePurchaseFromAfdian([FromBody] RestorePurchaseRequest request)
|
||||
{
|
||||
var order = await afdian.GetOrderAsync(request.OrderId);
|
||||
if (order is null) return NotFound($"Order with ID {request.OrderId} was not found.");
|
||||
|
||||
|
||||
var subscription = await subscriptions.CreateSubscriptionFromOrder(order);
|
||||
return Ok(subscription);
|
||||
}
|
||||
@ -200,4 +201,4 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
[ApiController]
|
||||
[Route("/wallets")]
|
||||
[Route("/api/wallets")]
|
||||
public class WalletController(AppDatabase db, WalletService ws, PaymentService payment) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
|
@ -85,10 +85,18 @@
|
||||
"FromName": "Alphabot",
|
||||
"SubjectPrefix": "Solar Network"
|
||||
},
|
||||
"RealtimeChat": {
|
||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
||||
"ApiKey": "APIs6TiL8wj3A4j",
|
||||
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
||||
"Realtime": {
|
||||
"Provider": "Cloudflare",
|
||||
"LiveKit": {
|
||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
||||
"ApiKey": "APIs6TiL8wj3A4j",
|
||||
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
||||
},
|
||||
"Cloudflare": {
|
||||
"ApiKey": "",
|
||||
"ApiSecret": "",
|
||||
"PreferredRegion": "us-east-1"
|
||||
}
|
||||
},
|
||||
"GeoIp": {
|
||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||
|
@ -6,7 +6,8 @@
|
||||
"css:build": "npx @tailwindcss/cli -i ./wwwroot/css/site.css -o ./wwwroot/css/styles.css"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.7",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/cli": "^4.1.7"
|
||||
"daisyui": "^5.0.46"
|
||||
}
|
||||
}
|
@ -1,10 +1,88 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "daisyui";
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/preflight.css" layer(base);
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
|
||||
@theme {
|
||||
--font-sans: "Nunito", sans-serif;
|
||||
--font-mono: "Noto Sans Mono", monospace;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(100% 0 0);
|
||||
--color-base-200: oklch(98% 0 0);
|
||||
--color-base-300: oklch(95% 0 0);
|
||||
--color-base-content: oklch(21% 0.006 285.885);
|
||||
--color-primary: oklch(62% 0.0873 281deg);
|
||||
--color-primary-content: oklch(93% 0.034 272.788);
|
||||
--color-secondary: oklch(62% 0.214 259.815);
|
||||
--color-secondary-content: oklch(94% 0.028 342.258);
|
||||
--color-accent: oklch(77% 0.152 181.912);
|
||||
--color-accent-content: oklch(38% 0.063 188.416);
|
||||
--color-neutral: oklch(14% 0.005 285.823);
|
||||
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||
--color-info: oklch(82% 0.111 230.318);
|
||||
--color-info-content: oklch(29% 0.066 243.157);
|
||||
--color-success: oklch(79% 0.209 151.711);
|
||||
--color-success-content: oklch(37% 0.077 168.94);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(41% 0.112 45.904);
|
||||
--color-error: oklch(71% 0.194 13.428);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 1rem;
|
||||
--size-selector: 0.28125rem;
|
||||
--size-field: 0.28125rem;
|
||||
--border: 1px;
|
||||
--depth: 1;
|
||||
--noise: 1;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dark";
|
||||
default: false;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(0% 0 0);
|
||||
--color-base-200: oklch(20% 0.016 285.938);
|
||||
--color-base-300: oklch(25% 0.013 285.805);
|
||||
--color-base-content: oklch(97.807% 0.029 256.847);
|
||||
--color-primary: oklch(50% 0.0873 281deg);
|
||||
--color-primary-content: oklch(96% 0.018 272.314);
|
||||
--color-secondary: oklch(62% 0.214 259.815);
|
||||
--color-secondary-content: oklch(94% 0.028 342.258);
|
||||
--color-accent: oklch(77% 0.152 181.912);
|
||||
--color-accent-content: oklch(38% 0.063 188.416);
|
||||
--color-neutral: oklch(21% 0.006 285.885);
|
||||
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||
--color-info: oklch(82% 0.111 230.318);
|
||||
--color-info-content: oklch(29% 0.066 243.157);
|
||||
--color-success: oklch(79% 0.209 151.711);
|
||||
--color-success-content: oklch(37% 0.077 168.94);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(41% 0.112 45.904);
|
||||
--color-error: oklch(64% 0.246 16.439);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 1rem;
|
||||
--size-selector: 0.28125rem;
|
||||
--size-field: 0.28125rem;
|
||||
--border: 1px;
|
||||
--depth: 1;
|
||||
--noise: 1;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html, body {
|
||||
padding: 0;
|
||||
@ -12,6 +90,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
}
|
||||
|
||||
/* For Firefox. */
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
@ -23,14 +105,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors dark:bg-blue-600 dark:hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
@apply text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300;
|
||||
}
|
||||
|
||||
.container-default {
|
||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -8,10 +8,12 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABlurHashEncoder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fb87f853683828cb934127af9a42b22cf516412af1e61ae2ff4935ae82aff_003FBlurHashEncoder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABodyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc5c8aba04a29d49c65d772c9ffcd93ac7eb38ccbb49a5f506518a0b9bdcaa75_003FBodyBuilder_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_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_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_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_003AController_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb320290c1b964c3e88434ff5505d9086c9a00_003Fdf_003F95b535f9_003FController_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_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>
|
||||
@ -54,6 +56,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtSecurityTokenHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F477051138f1f40de9077b7b1cdc55c6215fb0_003Ff5_003Fd716e016_003FJwtSecurityTokenHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKestrelServerLimits_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e2e5dfcafad4407b569dd5df56a2fbf274e00_003Fa4_003F39445f62_003FKestrelServerLimits_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKnownResamplers_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003Fb3_003Fcdb3e080_003FKnownResamplers_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALivekitModels_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Ff013807925809b234b3ca1be31567576bb8bda08f2c4fa5d290d4d14d8134_003FLivekitModels_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALivekitRoom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F82666257d5ad47354add7af860f66dd85df55ec93e92e8a45891b9bff7bf80ac_003FLivekitRoom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMailboxAddress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8e03e47c46b7469f97abc40667cbcf9b133000_003Fa6_003F83324248_003FMailboxAddress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -70,6 +73,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6efe388c7585d5dd5587416a55298550b030c2a107edf45f988791297c3ffa_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcbafb95b4df34952928f87356db00c8f2fe00_003F9b_003F8ba036bb_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARazorPage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F81d2924a2bbd4b0c864a1d23cbf5f0893d200_003F5f_003Fc110be1c_003FRazorPage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResizeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F48_003F0209e410_003FResizeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManagerStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb62f365d06c44ad695ff75960cdf97a2a800_003Fe4_003Ff6ba93b7_003FResourceManagerStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARSA_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee4f989f6b8042b59b2654fdc188e287243600_003F8b_003F44e5f855_003FRSA_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -81,6 +85,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASourceCustom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdaa8d9c408cd4b4286bbef7e35f1a42e31c00_003F45_003F5839ca6c_003FSourceCustom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3bef61b8a21d4c8e96872ecdd7782fa0e55000_003F7a_003F870020d0_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe6898c1ddf974e16b95b114722270029e55000_003F6b_003F7530575d_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationFeed_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fad_003Fd26b4d73_003FSyndicationFeed_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationItem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fe1_003Fb136d7be_003FSyndicationItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Reference in New Issue
Block a user