:drunk: Write shit code trying to split up the Auth (WIP)

This commit is contained in:
2025-07-06 12:58:18 +08:00
parent 5757526ea5
commit 6a3d04af3d
224 changed files with 1889 additions and 36885 deletions

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,6 @@
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Storage; namespace DysonNetwork.Common.Interfaces;
/// <summary> /// <summary>
/// Common interface for cloud file entities that can be used in file operations. /// Common interface for cloud file entities that can be used in file operations.

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Common.Interfaces;
public interface IIdentifiedResource
{
public string ResourceIdentifier { get; }
}

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public enum AbuseReportType public enum AbuseReportType
{ {

View File

@ -1,14 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Sphere.Wallet;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using OtpNet; using OtpNet;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
[Index(nameof(Name), IsUnique = true)] [Index(nameof(Name), IsUnique = true)]
public class Account : ModelBase public class Account : ModelBase
@ -26,8 +23,8 @@ public class Account : ModelBase
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>(); [JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>();
[JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>(); [JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>();
[JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>(); [JsonIgnore] public ICollection<AuthSession> Sessions { get; set; } = new List<AuthSession>();
[JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>(); [JsonIgnore] public ICollection<AuthChallenge> Challenges { get; set; } = new List<AuthChallenge>();
[JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>(); [JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
[JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>(); [JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>();
@ -193,4 +190,4 @@ public class AccountConnection : ModelBase
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
} }

View File

@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Point = NetTopologySuite.Geometries.Point; using NetTopologySuite.Geometries;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public abstract class ActionLogType public abstract class ActionLogType
{ {

View File

@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Activity; namespace DysonNetwork.Common.Models;
public interface IActivity public interface IActivity
{ {

View File

@ -1,13 +1,12 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Drawing;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Developer;
using NodaTime; using NodaTime;
using Point = NetTopologySuite.Geometries.Point;
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Common.Models;
public class Session : ModelBase public class AuthSession : ModelBase
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string? Label { get; set; } [MaxLength(1024)] public string? Label { get; set; }
@ -15,9 +14,9 @@ public class Session : ModelBase
public Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!; [JsonIgnore] public Models.Account Account { get; set; } = null!;
public Guid ChallengeId { get; set; } public Guid ChallengeId { get; set; }
public Challenge Challenge { get; set; } = null!; public AuthChallenge AuthChallenge { get; set; } = null!;
public Guid? AppId { get; set; } public Guid? AppId { get; set; }
public CustomApp? App { get; set; } public CustomApp? App { get; set; }
} }
@ -40,7 +39,7 @@ public enum ChallengePlatform
Linux Linux
} }
public class Challenge : ModelBase public class AuthChallenge : ModelBase
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }
@ -49,9 +48,9 @@ public class Challenge : ModelBase
public int FailedAttempts { get; set; } public int FailedAttempts { get; set; }
public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified; public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
public ChallengeType Type { get; set; } = ChallengeType.Login; public ChallengeType Type { get; set; } = ChallengeType.Login;
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new(); [Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = [];
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new(); [Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new(); [Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
[MaxLength(128)] public string? IpAddress { get; set; } [MaxLength(128)] public string? IpAddress { get; set; }
[MaxLength(512)] public string? UserAgent { get; set; } [MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(256)] public string? DeviceId { get; set; } [MaxLength(256)] public string? DeviceId { get; set; }
@ -59,9 +58,9 @@ public class Challenge : ModelBase
public Point? Location { get; set; } public Point? Location { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!; [JsonIgnore] public Models.Account Account { get; set; } = null!;
public Challenge Normalize() public AuthChallenge Normalize()
{ {
if (StepRemain == 0 && BlacklistFactors.Count == 0) StepRemain = StepTotal; if (StepRemain == 0 && BlacklistFactors.Count == 0) StepRemain = StepTotal;
return this; return this;

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public class Badge : ModelBase public class Badge : ModelBase
{ {
@ -16,7 +16,7 @@ public class Badge : ModelBase
public Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!; [JsonIgnore] public Models.Account Account { get; set; } = null!;
public BadgeReferenceObject ToReference() public BadgeReferenceObject ToReference()
{ {

View File

@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Common.Interfaces;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Chat; namespace DysonNetwork.Common.Models;
public enum ChatRoomType public enum ChatRoomType
{ {
@ -31,7 +31,7 @@ public class ChatRoom : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>(); [JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>();
public Guid? RealmId { get; set; } public Guid? RealmId { get; set; }
public Realm.Realm? Realm { get; set; } public Common.Models.Realm? Realm { get; set; }
[NotMapped] [NotMapped]
[JsonPropertyName("members")] [JsonPropertyName("members")]
@ -73,7 +73,7 @@ public class ChatMember : ModelBase
public Guid ChatRoomId { get; set; } public Guid ChatRoomId { get; set; }
public ChatRoom ChatRoom { get; set; } = null!; public ChatRoom ChatRoom { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
[MaxLength(1024)] public string? Nick { get; set; } [MaxLength(1024)] public string? Nick { get; set; }
@ -105,7 +105,7 @@ public class ChatMemberTransmissionObject : ModelBase
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid ChatRoomId { get; set; } public Guid ChatRoomId { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
[MaxLength(1024)] public string? Nick { get; set; } [MaxLength(1024)] public string? Nick { get; set; }

View File

@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using DysonNetwork.Common.Interfaces;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Storage; namespace DysonNetwork.Common.Models;
public class RemoteStorageConfig public class RemoteStorageConfig
{ {
@ -74,7 +74,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource
[MaxLength(4096)] [MaxLength(4096)]
public string? StorageUrl { get; set; } public string? StorageUrl { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public CloudFileReferenceObject ToReferenceObject() public CloudFileReferenceObject ToReferenceObject()

View File

@ -1,11 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Account; using DysonNetwork.Common.Interfaces;
using DysonNetwork.Sphere.Storage;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Developer; namespace DysonNetwork.Common.Models;
public enum CustomAppStatus public enum CustomAppStatus
{ {
@ -33,7 +32,7 @@ public class CustomApp : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>(); [JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
public Guid PublisherId { get; set; } public Guid PublisherId { get; set; }
public Publisher.Publisher Developer { get; set; } = null!; public Publisher Developer { get; set; } = null!;
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id; [NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
} }

View File

@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public enum StatusAttitude public enum StatusAttitude
{ {
@ -23,7 +23,7 @@ public class Status : ModelBase
public Instant? ClearedAt { get; set; } public Instant? ClearedAt { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account Account { get; set; } = null!; public Models.Account Account { get; set; } = null!;
} }
public enum CheckInResultLevel public enum CheckInResultLevel
@ -44,7 +44,7 @@ public class CheckInResult : ModelBase
[Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>(); [Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>();
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account Account { get; set; } = null!; public Models.Account Account { get; set; } = null!;
} }
public class FortuneTip public class FortuneTip

View File

@ -0,0 +1,14 @@
namespace DysonNetwork.Sphere.Models
{
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class LoginResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
}

View File

@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public enum MagicSpellType public enum MagicSpellType
{ {
@ -26,5 +26,5 @@ public class MagicSpell : ModelBase
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
public Guid? AccountId { get; set; } public Guid? AccountId { get; set; }
public Account? Account { get; set; } public Models.Account? Account { get; set; }
} }

View File

@ -1,11 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Common.Interfaces;
using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Chat; namespace DysonNetwork.Common.Models;
public class Message : ModelBase, IIdentifiedResource public class Message : ModelBase, IIdentifiedResource
{ {

View File

@ -0,0 +1,10 @@
using NodaTime;
namespace DysonNetwork.Common.Models;
public abstract class ModelBase
{
public Instant CreatedAt { get; set; }
public Instant UpdatedAt { get; set; }
public Instant? DeletedAt { get; set; }
}

View File

@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public class Notification : ModelBase public class Notification : ModelBase
{ {
@ -18,7 +18,7 @@ public class Notification : ModelBase
public Instant? ViewedAt { get; set; } public Instant? ViewedAt { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!; [JsonIgnore] public Models.Account Account { get; set; } = null!;
} }
public enum NotificationPushProvider public enum NotificationPushProvider
@ -37,5 +37,5 @@ public class NotificationPushSubscription : ModelBase
public Instant? LastUsedAt { get; set; } public Instant? LastUsedAt { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!; [JsonIgnore] public Models.Account Account { get; set; } = null!;
} }

View File

@ -1,9 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Sphere.Developer;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Wallet; namespace DysonNetwork.Common.Models;
public class WalletCurrency public class WalletCurrency
{ {

View File

@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Permission; namespace DysonNetwork.Common.Models;
/// The permission node model provides the infrastructure of permission control in Dyson Network. /// The permission node model provides the infrastructure of permission control in Dyson Network.
/// It based on the ABAC permission model. /// It based on the ABAC permission model.

View File

@ -1,12 +1,12 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Activity; using DysonNetwork.Common.Interfaces;
using DysonNetwork.Sphere.Storage;
using NodaTime; using NodaTime;
using NpgsqlTypes; using NpgsqlTypes;
namespace DysonNetwork.Sphere.Post; namespace DysonNetwork.Common.Models;
public enum PostType public enum PostType
{ {
@ -59,7 +59,7 @@ public class Post : ModelBase, IIdentifiedResource, IActivity
[JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!; [JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!;
public Guid PublisherId { get; set; } public Guid PublisherId { get; set; }
public Publisher.Publisher Publisher { get; set; } = null!; public Publisher Publisher { get; set; } = null!;
public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>(); public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>();
public ICollection<PostTag> Tags { get; set; } = new List<PostTag>(); public ICollection<PostTag> Tags { get; set; } = new List<PostTag>();
@ -71,9 +71,9 @@ public class Post : ModelBase, IIdentifiedResource, IActivity
public string ResourceIdentifier => $"post/{Id}"; public string ResourceIdentifier => $"post/{Id}";
public Activity.Activity ToActivity() public Activity ToActivity()
{ {
return new Activity.Activity() return new Activity()
{ {
CreatedAt = PublishedAt ?? CreatedAt, CreatedAt = PublishedAt ?? CreatedAt,
UpdatedAt = UpdatedAt, UpdatedAt = UpdatedAt,
@ -109,7 +109,7 @@ public class PostCollection : ModelBase
[MaxLength(256)] public string? Name { get; set; } [MaxLength(256)] public string? Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; } [MaxLength(4096)] public string? Description { get; set; }
public Publisher.Publisher Publisher { get; set; } = null!; public Publisher Publisher { get; set; } = null!;
public ICollection<Post> Posts { get; set; } = new List<Post>(); public ICollection<Post> Posts { get; set; } = new List<Post>();
} }
@ -130,5 +130,5 @@ public class PostReaction : ModelBase
public Guid PostId { get; set; } public Guid PostId { get; set; }
[JsonIgnore] public Post Post { get; set; } = null!; [JsonIgnore] public Post Post { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
} }

View File

@ -1,12 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Post; using DysonNetwork.Common.Interfaces;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Publisher; namespace DysonNetwork.Common.Models;
public enum PublisherType public enum PublisherType
{ {
@ -30,9 +29,9 @@ public class Publisher : ModelBase, IIdentifiedResource
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; } [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<Post.Post> Posts { get; set; } = new List<Post.Post>(); [JsonIgnore] public ICollection<Post> Posts { get; set; } = new List<Post>();
[JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>(); [JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>();
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>(); [JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
[JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = new List<PublisherFeature>(); [JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = new List<PublisherFeature>();
@ -41,9 +40,9 @@ public class Publisher : ModelBase, IIdentifiedResource
public ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>(); public ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>();
public Guid? AccountId { get; set; } public Guid? AccountId { get; set; }
public Account.Account? Account { get; set; } public Account? Account { get; set; }
public Guid? RealmId { get; set; } public Guid? RealmId { get; set; }
[JsonIgnore] public Realm.Realm? Realm { get; set; } [JsonIgnore] public Realm? Realm { get; set; }
public string ResourceIdentifier => $"publisher/{Id}"; public string ResourceIdentifier => $"publisher/{Id}";
} }
@ -61,7 +60,7 @@ public class PublisherMember : ModelBase
public Guid PublisherId { get; set; } public Guid PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!; [JsonIgnore] public Publisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer; public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
public Instant? JoinedAt { get; set; } public Instant? JoinedAt { get; set; }
@ -81,7 +80,7 @@ public class PublisherSubscription : ModelBase
public Guid PublisherId { get; set; } public Guid PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!; [JsonIgnore] public Publisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!; [JsonIgnore] public Account Account { get; set; } = null!;
public PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active; public PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active;
public int Tier { get; set; } = 0; public int Tier { get; set; } = 0;

View File

@ -1,12 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Chat; using DysonNetwork.Common.Interfaces;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Realm; namespace DysonNetwork.Common.Models;
[Index(nameof(Slug), IsUnique = true)] [Index(nameof(Slug), IsUnique = true)]
public class Realm : ModelBase, IIdentifiedResource public class Realm : ModelBase, IIdentifiedResource
@ -25,14 +24,13 @@ public class Realm : ModelBase, IIdentifiedResource
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; } [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>(); [JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>(); [JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
[JsonIgnore] public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!; [JsonIgnore] public Account Account { get; set; } = null!;
public string ResourceIdentifier => $"realm/{Id}"; public string ResourceIdentifier => $"realm/{Id}";
} }
@ -49,7 +47,7 @@ public class RealmMember : ModelBase
public Guid RealmId { get; set; } public Guid RealmId { get; set; }
public Realm Realm { get; set; } = null!; public Realm Realm { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
public int Role { get; set; } = RealmMemberRole.Normal; public int Role { get; set; } = RealmMemberRole.Normal;
public Instant? JoinedAt { get; set; } public Instant? JoinedAt { get; set; }

View File

@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Chat.Realtime;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Chat; namespace DysonNetwork.Common.Models;
public class RealtimeCall : ModelBase public class RealtimeCall : ModelBase
{ {

View File

@ -1,6 +1,6 @@
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
public enum RelationshipStatus : short public enum RelationshipStatus : short
{ {
@ -12,9 +12,9 @@ public enum RelationshipStatus : short
public class Relationship : ModelBase public class Relationship : ModelBase
{ {
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account Account { get; set; } = null!; public Models.Account Account { get; set; } = null!;
public Guid RelatedId { get; set; } public Guid RelatedId { get; set; }
public Account Related { get; set; } = null!; public Models.Account Related { get; set; } = null!;
public Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Wallet; namespace DysonNetwork.Common.Models;
public record class SubscriptionTypeData( public record class SubscriptionTypeData(
string Identifier, string Identifier,
@ -138,7 +138,7 @@ public class Subscription : ModelBase
public Instant? RenewalAt { get; set; } public Instant? RenewalAt { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!; public Account Account { get; set; } = null!;
[NotMapped] [NotMapped]
public bool IsAvailable public bool IsAvailable

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Common.Models;
/// <summary> /// <summary>
/// The verification info of a resource /// The verification info of a resource

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Wallet; namespace DysonNetwork.Common.Models;
public class Wallet : ModelBase public class Wallet : ModelBase
{ {
@ -10,7 +10,7 @@ public class Wallet : ModelBase
public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>(); public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>();
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
} }
public class WalletPocket : ModelBase public class WalletPocket : ModelBase

View File

@ -4,7 +4,7 @@ using NodaTime;
using NodaTime.Serialization.JsonNet; using NodaTime.Serialization.JsonNet;
using StackExchange.Redis; using StackExchange.Redis;
namespace DysonNetwork.Sphere.Storage; namespace DysonNetwork.Common.Services;
/// <summary> /// <summary>
/// Represents a distributed lock that can be used to synchronize access across multiple processes /// Represents a distributed lock that can be used to synchronize access across multiple processes

View File

@ -0,0 +1,17 @@
namespace DysonNetwork.Pass.Connection;
public class GeoIpService
{
// Dummy class
}
public class WebSocketService
{
// Dummy class
}
public class WebSocketPacket
{
public string Type { get; set; } = null!;
public object Data { get; set; } = null!;
}

View File

@ -0,0 +1,196 @@
using System.Linq.Expressions;
using System.Reflection;
using DysonNetwork.Common.Models;
using DysonNetwork.Pass.Permission;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using NodaTime;
using Quartz;
namespace DysonNetwork.Pass.Data;
public class PassDatabase(
DbContextOptions<PassDatabase> options,
IConfiguration configuration
) : DbContext(options)
{
public DbSet<PermissionNode> PermissionNodes { get; set; }
public DbSet<PermissionGroup> PermissionGroups { get; set; }
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
public DbSet<MagicSpell> MagicSpells { get; set; }
public DbSet<Account> Accounts { get; set; }
public DbSet<AccountConnection> AccountConnections { get; set; }
public DbSet<Profile> AccountProfiles { get; set; }
public DbSet<AccountContact> AccountContacts { get; set; }
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; }
public DbSet<Relationship> AccountRelationships { get; set; }
public DbSet<Notification> Notifications { get; set; }
public DbSet<Badge> Badges { get; set; }
public DbSet<ActionLog> ActionLogs { get; set; }
public DbSet<AbuseReport> AbuseReports { get; set; }
public DbSet<AuthSession> AuthSessions { get; set; }
public DbSet<AuthChallenge> AuthChallenges { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(
configuration.GetConnectionString("App"),
opt => opt
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
.UseNetTopologySuite()
.UseNodaTime()
).UseSnakeCaseNamingConvention();
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
// Add any initial seeding logic here if needed for PassDatabase
});
optionsBuilder.UseSeeding((context, _) => {});
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PermissionGroupMember>()
.HasKey(pg => new { pg.GroupId, pg.Actor });
modelBuilder.Entity<PermissionGroupMember>()
.HasOne(pg => pg.Group)
.WithMany(g => g.Members)
.HasForeignKey(pg => pg.GroupId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Relationship>()
.HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId });
modelBuilder.Entity<Relationship>()
.HasOne(r => r.Account)
.WithMany(a => a.OutgoingRelationships)
.HasForeignKey(r => r.AccountId);
modelBuilder.Entity<Relationship>()
.HasOne(r => r.Related)
.WithMany(a => a.IncomingRelationships)
.HasForeignKey(r => r.RelatedId);
// Automatically apply soft-delete filter to all entities inheriting BaseModel
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue;
var method = typeof(PassDatabase)
.GetMethod(nameof(SetSoftDeleteFilter),
BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(entityType.ClrType);
method.Invoke(null, [modelBuilder]);
}
}
private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
where TEntity : ModelBase
{
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var now = SystemClock.Instance.GetCurrentInstant();
foreach (var entry in ChangeTracker.Entries<ModelBase>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.UpdatedAt = now;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = now;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.Entity.DeletedAt = now;
break;
case EntityState.Detached:
case EntityState.Unchanged:
default:
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
}
public class PassDatabaseFactory : IDesignTimeDbContextFactory<PassDatabase>
{
public PassDatabase CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var optionsBuilder = new DbContextOptionsBuilder<PassDatabase>();
return new PassDatabase(optionsBuilder.Options, configuration);
}
}
public class PassDatabaseRecyclingJob(PassDatabase db, ILogger<PassDatabaseRecyclingJob> logger) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var now = SystemClock.Instance.GetCurrentInstant();
logger.LogInformation("Cleaning up expired records...");
// Expired relationships
var affectedRows = await db.AccountRelationships
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
.ExecuteDeleteAsync();
logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows);
logger.LogInformation("Deleting soft-deleted records...");
var threshold = now - Duration.FromDays(7);
var entityTypes = db.Model.GetEntityTypes()
.Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase))
.Select(t => t.ClrType);
foreach (var entityType in entityTypes)
{
var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
.MakeGenericMethod(entityType).Invoke(db, null)!;
var parameter = Expression.Parameter(entityType, "e");
var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt));
var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?)));
var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?)));
var finalCondition = Expression.AndAlso(notNull, condition);
var lambda = Expression.Lambda(finalCondition, parameter);
var queryable = set.Provider.CreateQuery(
Expression.Call(
typeof(Queryable),
"Where",
[entityType],
set.Expression,
Expression.Quote(lambda)
)
);
var toListAsync = typeof(EntityFrameworkQueryableExtensions)
.GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))!
.MakeGenericMethod(entityType);
var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!;
db.RemoveRange(items);
}
await db.SaveChangesAsync();
}
}

View File

@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace DysonNetwork.Pass.Developer;
public class CustomApp
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(256)] public string Name { get; set; } = null!;
[MaxLength(4096)] public string Description { get; set; } = null!;
[MaxLength(1024)] public string Homepage { get; set; } = null!;
[MaxLength(1024)] public string CallbackUrl { get; set; } = null!;
[Column(TypeName = "jsonb")] public OauthConfig? OauthConfig { get; set; }
public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
}
public class CustomAppSecret
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(4096)] public string Secret { get; set; } = null!;
public bool IsOidc { get; set; } = false;
public DateTime? ExpiredAt { get; set; }
public Guid AppId { get; set; }
[JsonIgnore] public CustomApp App { get; set; } = null!;
}
public class OauthConfig
{
public List<string>? AllowedScopes { get; set; }
}

View File

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Features\Account\Controllers\" />
<Folder Include="Features\Account\Services\" />
<Folder Include="Features\Auth\Controllers\" />
<Folder Include="Features\Auth\Models\" />
<Folder Include="Features\Auth\Services\" />
<Folder Include="Data\" />
<Folder Include="Email\" />
<Folder Include="Developer\" />
<Folder Include="Localization\" />
<Folder Include="Storage\" />
<Folder Include="Storage\Handlers\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@DysonNetwork.Pass_HostAddress = http://localhost:5048
GET {{DysonNetwork.Pass_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Email;
public class EmailModels
{
// Dummy class for EmailModels
}

View File

@ -0,0 +1,10 @@
namespace DysonNetwork.Pass.Email;
public class EmailService
{
public Task SendTemplatedEmailAsync<TTemplate, TModel>(string recipientName, string recipientEmail, string subject, TModel model) where TTemplate : class where TModel : class
{
// Dummy implementation
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Email;
public class RazorViewRenderer
{
// Dummy class for RazorViewRenderer
}

View File

@ -0,0 +1,45 @@
using System.Collections;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
namespace DysonNetwork.Pass.Extensions;
public static class OptionalQueryExtensions
{
public static IQueryable<T> If<
T
>(
this IQueryable<T> source,
bool condition,
Func<IQueryable<T>, IQueryable<T>> transform
)
{
return condition ? transform(source) : source;
}
public static IQueryable<T> If<
T,
TP
>(
this IIncludableQueryable<T, TP> source,
bool condition,
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
)
where T : class
{
return condition ? transform(source) : source;
}
public static IQueryable<T> If<
T,
TP
>(
this IIncludableQueryable<T, IEnumerable<TP>> source,
bool condition,
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
)
where T : class
{
return condition ? transform(source) : source;
}
}

View File

@ -1,28 +1,27 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Auth; using DysonNetwork.Pass.Data;
using DysonNetwork.Sphere.Permission; using DysonNetwork.Pass.Features.Auth;
using Microsoft.AspNetCore.Authorization; using DysonNetwork.Common.Models;
using DysonNetwork.Pass.Features.Account.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using NodaTime.Extensions;
using System.Collections.Generic;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account.Controllers;
[ApiController] [ApiController]
[Route("/accounts")] [Route("/accounts")]
public class AccountController( public class AccountController(
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
AccountService accounts, AccountService accounts,
AccountEventService events AccountEventService events
) : ControllerBase ) : ControllerBase
{ {
[HttpGet("{name}")] [HttpGet("{name}")]
[ProducesResponseType<Account>(StatusCodes.Status200OK)] [ProducesResponseType<Common.Models.Account>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Account?>> GetByName(string name) public async Task<ActionResult<Common.Models.Account?>> GetByName(string name)
{ {
var account = await db.Accounts var account = await db.Accounts
.Include(e => e.Badges) .Include(e => e.Badges)
@ -73,9 +72,9 @@ public class AccountController(
} }
[HttpPost] [HttpPost]
[ProducesResponseType<Account>(StatusCodes.Status200OK)] [ProducesResponseType<Common.Models.Account>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Account>> CreateAccount([FromBody] AccountCreateRequest request) public async Task<ActionResult<Common.Models.Account>> CreateAccount([FromBody] AccountCreateRequest request)
{ {
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token."); if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
@ -163,7 +162,7 @@ public class AccountController(
} }
[HttpGet("search")] [HttpGet("search")]
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20) public async Task<List<Common.Models.Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
{ {
if (string.IsNullOrWhiteSpace(query)) if (string.IsNullOrWhiteSpace(query))
return []; return [];

View File

@ -1,20 +1,20 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Auth; using DysonNetwork.Common.Models;
using DysonNetwork.Sphere.Permission; using DysonNetwork.Pass.Data;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Features.Account.Services;
using DysonNetwork.Pass.Features.Auth;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using Org.BouncyCastle.Utilities;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account.Controllers;
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("/accounts/me")] [Route("/accounts/me")]
public class AccountCurrentController( public class AccountCurrentController(
AppDatabase db, PassDatabase db,
AccountService accounts, AccountService accounts,
FileReferenceService fileRefService, FileReferenceService fileRefService,
AccountEventService events, AccountEventService events,
@ -22,10 +22,10 @@ public class AccountCurrentController(
) : ControllerBase ) : ControllerBase
{ {
[HttpGet] [HttpGet]
[ProducesResponseType<Account>(StatusCodes.Status200OK)] [ProducesResponseType<Common.Models.Account>(StatusCodes.Status200OK)]
public async Task<ActionResult<Account>> GetCurrentIdentity() public async Task<ActionResult<Common.Models.Account>> GetCurrentIdentity()
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id; var userId = currentUser.Id;
var account = await db.Accounts var account = await db.Accounts

View File

@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Account; using DysonNetwork.Pass.Data;
namespace DysonNetwork.Pass.Features.Account;
[ApiController] [ApiController]
[Route("/spells")] [Route("/spells")]
public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase public class MagicSpellController(PassDatabase db, MagicSpellService sp) : ControllerBase
{ {
[HttpPost("{spellId:guid}/resend")] [HttpPost("{spellId:guid}/resend")]
public async Task<ActionResult> ResendMagicSpell(Guid spellId) public async Task<ActionResult> ResendMagicSpell(Guid spellId)

View File

@ -1,23 +1,23 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Auth; using DysonNetwork.Pass.Features.Auth;
using DysonNetwork.Sphere.Permission; using DysonNetwork.Common.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
[ApiController] [ApiController]
[Route("/notifications")] [Route("/notifications")]
public class NotificationController(AppDatabase db, NotificationService nty) : ControllerBase public class NotificationController(PassDatabase db, NotificationService nty) : ControllerBase
{ {
[HttpGet("count")] [HttpGet("count")]
[Authorize] [Authorize]
public async Task<ActionResult<int>> CountUnreadNotifications() public async Task<ActionResult<int>> CountUnreadNotifications()
{ {
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
if (currentUserValue is not Account currentUser) return Unauthorized(); if (currentUserValue is not Common.Models.Account currentUser) return Unauthorized();
var count = await db.Notifications var count = await db.Notifications
.Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null) .Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null)
@ -35,7 +35,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
) )
{ {
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
if (currentUserValue is not Account currentUser) return Unauthorized(); if (currentUserValue is not Common.Models.Account currentUser) return Unauthorized();
var totalCount = await db.Notifications var totalCount = await db.Notifications
.Where(s => s.AccountId == currentUser.Id) .Where(s => s.AccountId == currentUser.Id)
@ -67,7 +67,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
{ {
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account; var currentUser = currentUserValue as Common.Models.Account;
if (currentUser == null) return Unauthorized(); if (currentUser == null) return Unauthorized();
var currentSession = currentSessionValue as Session; var currentSession = currentSessionValue as Session;
if (currentSession == null) return Unauthorized(); if (currentSession == null) return Unauthorized();
@ -85,7 +85,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
{ {
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account; var currentUser = currentUserValue as Common.Models.Account;
if (currentUser == null) return Unauthorized(); if (currentUser == null) return Unauthorized();
var currentSession = currentSessionValue as Session; var currentSession = currentSessionValue as Session;
if (currentSession == null) return Unauthorized(); if (currentSession == null) return Unauthorized();

View File

@ -1,21 +1,23 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Data;
using DysonNetwork.Common.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
[ApiController] [ApiController]
[Route("/relationships")] [Route("/relationships")]
public class RelationshipController(AppDatabase db, RelationshipService rels) : ControllerBase public class RelationshipController(PassDatabase db, RelationshipService rels) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize] [Authorize]
public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0, public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0,
[FromQuery] int take = 20) [FromQuery] int take = 20)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id; var userId = currentUser.Id;
var query = db.AccountRelationships.AsQueryable() var query = db.AccountRelationships.AsQueryable()
@ -46,7 +48,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<List<Relationship>>> ListSentRequests() public async Task<ActionResult<List<Relationship>>> ListSentRequests()
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relationships = await db.AccountRelationships var relationships = await db.AccountRelationships
.Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending) .Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending)
@ -69,7 +71,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId, public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId,
[FromBody] RelationshipRequest request) [FromBody] RelationshipRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId); var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found."); if (relatedUser is null) return NotFound("Account was not found.");
@ -92,7 +94,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId, public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId,
[FromBody] RelationshipRequest request) [FromBody] RelationshipRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
try try
{ {
@ -113,7 +115,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<Relationship>> GetRelationship(Guid userId) public async Task<ActionResult<Relationship>> GetRelationship(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var now = Instant.FromDateTimeUtc(DateTime.UtcNow); var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
var queries = db.AccountRelationships.AsQueryable() var queries = db.AccountRelationships.AsQueryable()
@ -133,7 +135,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId) public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId); var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found."); if (relatedUser is null) return NotFound("Account was not found.");
@ -158,7 +160,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult> DeleteFriendRequest(Guid userId) public async Task<ActionResult> DeleteFriendRequest(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
try try
{ {
@ -175,7 +177,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId) public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
if (relationship is null) return NotFound("Friend request was not found."); if (relationship is null) return NotFound("Friend request was not found.");
@ -195,7 +197,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId) public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
if (relationship is null) return NotFound("Friend request was not found."); if (relationship is null) return NotFound("Friend request was not found.");
@ -215,7 +217,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<Relationship>> BlockUser(Guid userId) public async Task<ActionResult<Relationship>> BlockUser(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId); var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found."); if (relatedUser is null) return NotFound("Account was not found.");
@ -235,7 +237,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize] [Authorize]
public async Task<ActionResult<Relationship>> UnblockUser(Guid userId) public async Task<ActionResult<Relationship>> UnblockUser(Guid userId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId); var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found."); if (relatedUser is null) return NotFound("Account was not found.");

View File

@ -0,0 +1,20 @@
using System.Globalization;
using DysonNetwork.Common.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization;
using NodaTime;
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface IAccountEventService
{
void PurgeStatusCache(Guid userId);
Task<Status> GetStatus(Guid userId);
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds);
Task<Status> CreateStatus(Models.Account user, Status status);
Task ClearStatus(Models.Account user, Status status);
Task<bool> CheckInDailyDoAskCaptcha(Models.Account user);
Task<bool> CheckInDailyIsAvailable(Models.Account user);
Task<CheckInResult> CheckInDaily(Models.Account user);
Task<List<DailyEventResponse>> GetEventCalendar(Models.Account user, int month, int year = 0, bool replaceInvisible = false);
}

View File

@ -0,0 +1,46 @@
using System.Globalization;
using NodaTime;
using DysonNetwork.Pass.Features.Auth;
using DysonNetwork.Pass.Features.Auth.OpenId;
using DysonNetwork.Common.Models;
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface IAccountService
{
static void SetCultureInfo(Models.Account account) { }
static void SetCultureInfo(string? languageCode) { }
Task PurgeAccountCache(Models.Account account);
Task<Models.Account?> LookupAccount(string probe);
Task<Models.Account?> LookupAccountByConnection(string identifier, string provider);
Task<int?> GetAccountLevel(Guid accountId);
Task<Models.Account> CreateAccount(
string name,
string nick,
string email,
string? password,
string language = "en-US",
bool isEmailVerified = false,
bool isActivated = false
);
Task<Models.Account> CreateAccount(OidcUserInfo userInfo);
Task RequestAccountDeletion(Models.Account account);
Task RequestPasswordReset(Models.Account account);
Task<bool> CheckAuthFactorExists(Models.Account account, AccountAuthFactorType type);
Task<AccountAuthFactor?> CreateAuthFactor(Models.Account account, AccountAuthFactorType type, string? secret);
Task<AccountAuthFactor> EnableAuthFactor(AccountAuthFactor factor, string? code);
Task<AccountAuthFactor> DisableAuthFactor(AccountAuthFactor factor);
Task DeleteAuthFactor(AccountAuthFactor factor);
Task SendFactorCode(Models.Account account, AccountAuthFactor factor, string? hint = null);
Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code);
Task<Session> UpdateSessionLabel(Models.Account account, Guid sessionId, string label);
Task DeleteSession(Models.Account account, Guid sessionId);
Task<AccountContact> CreateContactMethod(Models.Account account, AccountContactType type, string content);
Task VerifyContactMethod(Models.Account account, AccountContact contact);
Task<AccountContact> SetContactMethodPrimary(Models.Account account, AccountContact contact);
Task DeleteContactMethod(Models.Account account, AccountContact contact);
Task<Badge> GrantBadge(Models.Account account, Badge badge);
Task RevokeBadge(Models.Account account, Guid badgeId);
Task ActiveBadge(Models.Account account, Guid badgeId);
Task EnsureAccountProfileCreated();
}

View File

@ -0,0 +1,9 @@
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface IAccountUsernameService
{
Task<string> GenerateUniqueUsernameAsync(string baseName);
string SanitizeUsername(string username);
Task<bool> IsUsernameExistsAsync(string username);
Task<string> GenerateUsernameFromEmailAsync(string email);
}

View File

@ -0,0 +1,9 @@
using DysonNetwork.Pass.Connection;
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface IActionLogService
{
void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta);
void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, Models.Account? account = null);
}

View File

@ -0,0 +1,20 @@
using NodaTime;
using DysonNetwork.Pass.Features.Account;
using DysonNetwork.Common.Models;
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface IMagicSpellService
{
Task<MagicSpell> CreateMagicSpell(
Models.Account account,
MagicSpellType type,
Dictionary<string, object> meta,
Instant? expiredAt = null,
Instant? affectedAt = null,
bool preventRepeat = false
);
Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false);
Task ApplyMagicSpell(MagicSpell spell);
Task ApplyPasswordReset(MagicSpell spell, string newPassword);
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using DysonNetwork.Common.Models;
using NodaTime;
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface INotificationService
{
Task UnsubscribePushNotifications(string deviceId);
Task<NotificationPushSubscription> SubscribePushNotification(
Models.Account account,
NotificationPushProvider provider,
string deviceId,
string deviceToken
);
Task<Notification> SendNotification(
Models.Account account,
string topic,
string? title = null,
string? subtitle = null,
string? content = null,
Dictionary<string, object>? meta = null,
string? actionUri = null,
bool isSilent = false,
bool save = true
);
Task DeliveryNotification(Notification notification);
Task MarkNotificationsViewed(ICollection<Notification> notifications);
Task BroadcastNotification(Notification notification, bool save = false);
Task SendNotificationBatch(Notification notification, List<Models.Account> accounts, bool save = false);
}

View File

@ -0,0 +1,28 @@
using DysonNetwork.Common.Models;
using NodaTime;
namespace DysonNetwork.Pass.Features.Account.Interfaces;
public interface IRelationshipService
{
Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId);
Task<Relationship?> GetRelationship(
Guid accountId,
Guid relatedId,
RelationshipStatus? status = null,
bool ignoreExpired = false
);
Task<Relationship> CreateRelationship(Models.Account sender, Models.Account target, RelationshipStatus status);
Task<Relationship> BlockAccount(Models.Account sender, Models.Account target);
Task<Relationship> UnblockAccount(Models.Account sender, Models.Account target);
Task<Relationship> SendFriendRequest(Models.Account sender, Models.Account target);
Task DeleteFriendRequest(Guid accountId, Guid relatedId);
Task<Relationship> AcceptFriendRelationship(
Relationship relationship,
RelationshipStatus status = RelationshipStatus.Friends
);
Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status);
Task<List<Guid>> ListAccountFriends(Models.Account account);
Task<List<Guid>> ListAccountBlocked(Models.Account account);
Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, RelationshipStatus status = RelationshipStatus.Friends);
}

View File

@ -1,24 +1,30 @@
using System.Globalization; using System.Globalization;
using DysonNetwork.Sphere.Activity; using DysonNetwork.Common.Models;
using DysonNetwork.Sphere.Connection; using DysonNetwork.Pass.Data;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
using DysonNetwork.Sphere.Wallet;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using NodaTime; using NodaTime;
using Org.BouncyCastle.Asn1.X509;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account.Services;
public class AccountEventService( public class AccountEventService
AppDatabase db,
WebSocketService ws,
ICacheService cache,
PaymentService payment,
IStringLocalizer<Localization.AccountEventResource> localizer
)
{ {
private readonly PassDatabase db;
private readonly ICacheService cache;
private readonly IStringLocalizer<Localization.AccountEventResource> localizer;
public AccountEventService(
PassDatabase db,
ICacheService cache,
IStringLocalizer<Localization.AccountEventResource> localizer
)
{
this.db = db;
this.cache = cache;
this.localizer = localizer;
}
private static readonly Random Random = new(); private static readonly Random Random = new();
private const string StatusCacheKey = "AccountStatus_"; private const string StatusCacheKey = "AccountStatus_";
@ -139,7 +145,7 @@ public class AccountEventService(
return results; return results;
} }
public async Task<Status> CreateStatus(Account user, Status status) public async Task<Status> CreateStatus(Common.Models.Account user, Status status)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
await db.AccountStatuses await db.AccountStatuses
@ -152,7 +158,7 @@ public class AccountEventService(
return status; return status;
} }
public async Task ClearStatus(Account user, Status status) public async Task ClearStatus(Common.Models.Account user, Status status)
{ {
status.ClearedAt = SystemClock.Instance.GetCurrentInstant(); status.ClearedAt = SystemClock.Instance.GetCurrentInstant();
db.Update(status); db.Update(status);
@ -164,7 +170,7 @@ public class AccountEventService(
private const string CaptchaCacheKey = "CheckInCaptcha_"; private const string CaptchaCacheKey = "CheckInCaptcha_";
private const int CaptchaProbabilityPercent = 20; private const int CaptchaProbabilityPercent = 20;
public async Task<bool> CheckInDailyDoAskCaptcha(Account user) public async Task<bool> CheckInDailyDoAskCaptcha(Common.Models.Account user)
{ {
var cacheKey = $"{CaptchaCacheKey}{user.Id}"; var cacheKey = $"{CaptchaCacheKey}{user.Id}";
var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); var needsCaptcha = await cache.GetAsync<bool?>(cacheKey);
@ -176,7 +182,7 @@ public class AccountEventService(
return result; return result;
} }
public async Task<bool> CheckInDailyIsAvailable(Account user) public async Task<bool> CheckInDailyIsAvailable(Common.Models.Account user)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var lastCheckIn = await db.AccountCheckInResults var lastCheckIn = await db.AccountCheckInResults
@ -195,7 +201,7 @@ public class AccountEventService(
public const string CheckInLockKey = "CheckInLock_"; public const string CheckInLockKey = "CheckInLock_";
public async Task<CheckInResult> CheckInDaily(Account user) public async Task<CheckInResult> CheckInDaily(Common.Models.Account user)
{ {
var lockKey = $"{CheckInLockKey}{user.Id}"; var lockKey = $"{CheckInLockKey}{user.Id}";
@ -280,7 +286,7 @@ public class AccountEventService(
return result; return result;
} }
public async Task<List<DailyEventResponse>> GetEventCalendar(Account user, int month, int year = 0, public async Task<List<DailyEventResponse>> GetEventCalendar(Common.Models.Account user, int month, int year = 0,
bool replaceInvisible = false) bool replaceInvisible = false)
{ {
if (year == 0) if (year == 0)

View File

@ -1,32 +1,54 @@
using System.Globalization; using System.Globalization;
using DysonNetwork.Sphere.Auth; using DysonNetwork.Pass.Features.Auth;
using DysonNetwork.Sphere.Auth.OpenId; using DysonNetwork.Pass.Features.Auth.OpenId;
using DysonNetwork.Sphere.Email; using DysonNetwork.Pass.Email;
using DysonNetwork.Pass.Localization;
using DysonNetwork.Sphere.Localization; using DysonNetwork.Pass.Permission;
using DysonNetwork.Sphere.Permission; using DysonNetwork.Pass.Storage;
using DysonNetwork.Sphere.Storage;
using EFCore.BulkExtensions; using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using NodaTime; using NodaTime;
using Org.BouncyCastle.Utilities;
using OtpNet; using OtpNet;
using DysonNetwork.Pass.Data;
using DysonNetwork.Common.Models;
using DysonNetwork.Pass.Features.Auth.Services;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
public class AccountService( public class AccountService
AppDatabase db,
MagicSpellService spells,
AccountUsernameService uname,
NotificationService nty,
EmailService mailer,
IStringLocalizer<NotificationResource> localizer,
ICacheService cache,
ILogger<AccountService> logger
)
{ {
public static void SetCultureInfo(Account account) private readonly PassDatabase db;
private readonly MagicSpellService spells;
private readonly AccountUsernameService uname;
private readonly NotificationService nty;
private readonly EmailService mailer;
private readonly IStringLocalizer<NotificationResource> localizer;
private readonly ICacheService cache;
private readonly ILogger<AccountService> logger;
public AccountService(
PassDatabase db,
MagicSpellService spells,
AccountUsernameService uname,
NotificationService nty,
EmailService mailer,
IStringLocalizer<NotificationResource> localizer,
ICacheService cache,
ILogger<AccountService> logger
)
{
this.db = db;
this.spells = spells;
this.uname = uname;
this.nty = nty;
this.mailer = mailer;
this.localizer = localizer;
this.cache = cache;
this.logger = logger;
}
public static void SetCultureInfo(Models.Account account)
{ {
SetCultureInfo(account.Language); SetCultureInfo(account.Language);
} }
@ -40,12 +62,12 @@ public class AccountService(
public const string AccountCachePrefix = "account:"; public const string AccountCachePrefix = "account:";
public async Task PurgeAccountCache(Account account) public async Task PurgeAccountCache(Models.Account account)
{ {
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}"); await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
} }
public async Task<Account?> LookupAccount(string probe) public async Task<Models.Account?> LookupAccount(string probe)
{ {
var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync(); var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync();
if (account is not null) return account; if (account is not null) return account;
@ -57,7 +79,7 @@ public class AccountService(
return contact?.Account; return contact?.Account;
} }
public async Task<Account?> LookupAccountByConnection(string identifier, string provider) public async Task<Models.Account?> LookupAccountByConnection(string identifier, string provider)
{ {
var connection = await db.AccountConnections var connection = await db.AccountConnections
.Where(c => c.ProvidedIdentifier == identifier && c.Provider == provider) .Where(c => c.ProvidedIdentifier == identifier && c.Provider == provider)
@ -74,7 +96,7 @@ public class AccountService(
return profile?.Level; return profile?.Level;
} }
public async Task<Account> CreateAccount( public async Task<Models.Account> CreateAccount(
string name, string name,
string nick, string nick,
string email, string email,
@ -91,7 +113,7 @@ public class AccountService(
if (dupeNameCount > 0) if (dupeNameCount > 0)
throw new InvalidOperationException("Account name has already been taken."); throw new InvalidOperationException("Account name has already been taken.");
var account = new Account var account = new Models.Account
{ {
Name = name, Name = name,
Nick = nick, Nick = nick,
@ -159,7 +181,7 @@ public class AccountService(
} }
} }
public async Task<Account> CreateAccount(OidcUserInfo userInfo) public async Task<Models.Account> CreateAccount(OidcUserInfo userInfo)
{ {
if (string.IsNullOrEmpty(userInfo.Email)) if (string.IsNullOrEmpty(userInfo.Email))
throw new ArgumentException("Email is required for account creation"); throw new ArgumentException("Email is required for account creation");
@ -182,7 +204,7 @@ public class AccountService(
); );
} }
public async Task RequestAccountDeletion(Account account) public async Task RequestAccountDeletion(Models.Account account)
{ {
var spell = await spells.CreateMagicSpell( var spell = await spells.CreateMagicSpell(
account, account,
@ -194,7 +216,7 @@ public class AccountService(
await spells.NotifyMagicSpell(spell); await spells.NotifyMagicSpell(spell);
} }
public async Task RequestPasswordReset(Account account) public async Task RequestPasswordReset(Models.Account account)
{ {
var spell = await spells.CreateMagicSpell( var spell = await spells.CreateMagicSpell(
account, account,
@ -206,7 +228,7 @@ public class AccountService(
await spells.NotifyMagicSpell(spell); await spells.NotifyMagicSpell(spell);
} }
public async Task<bool> CheckAuthFactorExists(Account account, AccountAuthFactorType type) public async Task<bool> CheckAuthFactorExists(Models.Account account, AccountAuthFactorType type)
{ {
var isExists = await db.AccountAuthFactors var isExists = await db.AccountAuthFactors
.Where(x => x.AccountId == account.Id && x.Type == type) .Where(x => x.AccountId == account.Id && x.Type == type)
@ -214,7 +236,7 @@ public class AccountService(
return isExists; return isExists;
} }
public async Task<AccountAuthFactor?> CreateAuthFactor(Account account, AccountAuthFactorType type, string? secret) public async Task<AccountAuthFactor?> CreateAuthFactor(Models.Account account, AccountAuthFactorType type, string? secret)
{ {
AccountAuthFactor? factor = null; AccountAuthFactor? factor = null;
switch (type) switch (type)
@ -345,7 +367,7 @@ public class AccountService(
/// <param name="account">The owner of the auth factor</param> /// <param name="account">The owner of the auth factor</param>
/// <param name="factor">The auth factor needed to send code</param> /// <param name="factor">The auth factor needed to send code</param>
/// <param name="hint">The part of the contact method for verification</param> /// <param name="hint">The part of the contact method for verification</param>
public async Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null) public async Task SendFactorCode(Models.Account account, AccountAuthFactor factor, string? hint = null)
{ {
var code = new Random().Next(100000, 999999).ToString("000000"); var code = new Random().Next(100000, 999999).ToString("000000");
@ -454,7 +476,7 @@ public class AccountService(
); );
} }
public async Task<Session> UpdateSessionLabel(Account account, Guid sessionId, string label) public async Task<Session> UpdateSessionLabel(Models.Account account, Guid sessionId, string label)
{ {
var session = await db.AuthSessions var session = await db.AuthSessions
.Include(s => s.Challenge) .Include(s => s.Challenge)
@ -477,7 +499,7 @@ public class AccountService(
return session; return session;
} }
public async Task DeleteSession(Account account, Guid sessionId) public async Task DeleteSession(Models.Account account, Guid sessionId)
{ {
var session = await db.AuthSessions var session = await db.AuthSessions
.Include(s => s.Challenge) .Include(s => s.Challenge)
@ -503,7 +525,7 @@ public class AccountService(
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}"); await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
} }
public async Task<AccountContact> CreateContactMethod(Account account, AccountContactType type, string content) public async Task<AccountContact> CreateContactMethod(Models.Account account, AccountContactType type, string content)
{ {
var contact = new AccountContact var contact = new AccountContact
{ {
@ -518,7 +540,7 @@ public class AccountService(
return contact; return contact;
} }
public async Task VerifyContactMethod(Account account, AccountContact contact) public async Task VerifyContactMethod(Models.Account account, AccountContact contact)
{ {
var spell = await spells.CreateMagicSpell( var spell = await spells.CreateMagicSpell(
account, account,
@ -530,7 +552,7 @@ public class AccountService(
await spells.NotifyMagicSpell(spell); await spells.NotifyMagicSpell(spell);
} }
public async Task<AccountContact> SetContactMethodPrimary(Account account, AccountContact contact) public async Task<AccountContact> SetContactMethodPrimary(Models.Account account, AccountContact contact)
{ {
if (contact.AccountId != account.Id) if (contact.AccountId != account.Id)
throw new InvalidOperationException("Contact method does not belong to this account."); throw new InvalidOperationException("Contact method does not belong to this account.");
@ -559,7 +581,7 @@ public class AccountService(
} }
} }
public async Task DeleteContactMethod(Account account, AccountContact contact) public async Task DeleteContactMethod(Models.Account account, AccountContact contact)
{ {
if (contact.AccountId != account.Id) if (contact.AccountId != account.Id)
throw new InvalidOperationException("Contact method does not belong to this account."); throw new InvalidOperationException("Contact method does not belong to this account.");
@ -574,7 +596,7 @@ public class AccountService(
/// This method will grant a badge to the account. /// This method will grant a badge to the account.
/// Shouldn't be exposed to normal user and the user itself. /// Shouldn't be exposed to normal user and the user itself.
/// </summary> /// </summary>
public async Task<Badge> GrantBadge(Account account, Badge badge) public async Task<Badge> GrantBadge(Models.Account account, Badge badge)
{ {
badge.AccountId = account.Id; badge.AccountId = account.Id;
db.Badges.Add(badge); db.Badges.Add(badge);
@ -586,7 +608,7 @@ public class AccountService(
/// This method will revoke a badge from the account. /// This method will revoke a badge from the account.
/// Shouldn't be exposed to normal user and the user itself. /// Shouldn't be exposed to normal user and the user itself.
/// </summary> /// </summary>
public async Task RevokeBadge(Account account, Guid badgeId) public async Task RevokeBadge(Models.Account account, Guid badgeId)
{ {
var badge = await db.Badges var badge = await db.Badges
.Where(b => b.AccountId == account.Id && b.Id == badgeId) .Where(b => b.AccountId == account.Id && b.Id == badgeId)
@ -604,7 +626,7 @@ public class AccountService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
public async Task ActiveBadge(Account account, Guid badgeId) public async Task ActiveBadge(Models.Account account, Guid badgeId)
{ {
await using var transaction = await db.Database.BeginTransactionAsync(); await using var transaction = await db.Database.BeginTransactionAsync();

View File

@ -1,12 +1,12 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
/// <summary> /// <summary>
/// Service for handling username generation and validation /// Service for handling username generation and validation
/// </summary> /// </summary>
public class AccountUsernameService(AppDatabase db) public class AccountUsernameService(AppDatabase db) : IAccountUsernameService
{ {
private readonly Random _random = new(); private readonly Random _random = new();

View File

@ -1,11 +1,10 @@
using Quartz; using DysonNetwork.Pass.Connection;
using DysonNetwork.Sphere.Connection; using DysonNetwork.Pass.Storage;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage.Handlers;
using DysonNetwork.Sphere.Storage.Handlers;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
public class ActionLogService(GeoIpService geo, FlushBufferService fbs) public class ActionLogService(GeoIpService geo, FlushBufferService fbs) : IActionLogService
{ {
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
{ {
@ -20,7 +19,7 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
} }
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
Account? account = null) Models.Account? account = null)
{ {
var log = new ActionLog var log = new ActionLog
{ {
@ -31,7 +30,7 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
}; };
if (request.HttpContext.Items["CurrentUser"] is Account currentUser) if (request.HttpContext.Items["CurrentUser"] is Models.Account currentUser)
log.AccountId = currentUser.Id; log.AccountId = currentUser.Id;
else if (account != null) else if (account != null)
log.AccountId = account.Id; log.AccountId = account.Id;

View File

@ -1,27 +1,34 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using DysonNetwork.Sphere.Email;
using DysonNetwork.Sphere.Pages.Emails;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Resources.Localization;
using DysonNetwork.Sphere.Resources.Pages.Emails;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
public class MagicSpellService( using DysonNetwork.Common.Models;
AppDatabase db,
EmailService email, namespace DysonNetwork.Pass.Features.Account;
IConfiguration configuration,
ILogger<MagicSpellService> logger, public class MagicSpellService
IStringLocalizer<Localization.EmailResource> localizer
)
{ {
private readonly PassDatabase db;
private readonly EmailService email;
private readonly IConfiguration configuration;
private readonly ILogger<MagicSpellService> logger;
private readonly IStringLocalizer<Localization.EmailResource> localizer;
public MagicSpellService(
PassDatabase db,
EmailService email,
IConfiguration configuration,
ILogger<MagicSpellService> logger,
IStringLocalizer<Localization.EmailResource> localizer
)
{
this.db = db;
this.email = email;
this.configuration = configuration;
this.logger = logger;
this.localizer = localizer;
}
public async Task<MagicSpell> CreateMagicSpell( public async Task<MagicSpell> CreateMagicSpell(
Account account, Models.Account account,
MagicSpellType type, MagicSpellType type,
Dictionary<string, object> meta, Dictionary<string, object> meta,
Instant? expiredAt = null, Instant? expiredAt = null,

View File

@ -1,17 +1,17 @@
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Connection; using DysonNetwork.Pass.Connection;
using EFCore.BulkExtensions; using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using DysonNetwork.Pass.Data;
using DysonNetwork.Common.Models;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
public class NotificationService( public class NotificationService(
AppDatabase db, PassDatabase db,
WebSocketService ws, IConfiguration config) : INotificationService
IHttpClientFactory httpFactory,
IConfiguration config)
{ {
private readonly string _notifyTopic = config["Notifications:Topic"]!; private readonly string _notifyTopic = config["Notifications:Topic"]!;
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
@ -24,7 +24,7 @@ public class NotificationService(
} }
public async Task<NotificationPushSubscription> SubscribePushNotification( public async Task<NotificationPushSubscription> SubscribePushNotification(
Account account, Models.Account account,
NotificationPushProvider provider, NotificationPushProvider provider,
string deviceId, string deviceId,
string deviceToken string deviceToken
@ -70,7 +70,7 @@ public class NotificationService(
} }
public async Task<Notification> SendNotification( public async Task<Notification> SendNotification(
Account account, Models.Account account,
string topic, string topic,
string? title = null, string? title = null,
string? subtitle = null, string? subtitle = null,
@ -110,11 +110,7 @@ public class NotificationService(
public async Task DeliveryNotification(Notification notification) public async Task DeliveryNotification(Notification notification)
{ {
ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
{
Type = "notifications.new",
Data = notification
});
// Pushing the notification // Pushing the notification
var subscribers = await db.NotificationPushSubscriptions var subscribers = await db.NotificationPushSubscriptions
@ -160,23 +156,14 @@ public class NotificationService(
await db.BulkInsertAsync(notifications); await db.BulkInsertAsync(notifications);
} }
foreach (var account in accounts)
{
notification.Account = account;
notification.AccountId = account.Id;
ws.SendPacketToAccount(account.Id, new WebSocketPacket
{
Type = "notifications.new",
Data = notification
});
}
var subscribers = await db.NotificationPushSubscriptions var subscribers = await db.NotificationPushSubscriptions
.ToListAsync(); .ToListAsync();
await _PushNotification(notification, subscribers); await _PushNotification(notification, subscribers);
} }
public async Task SendNotificationBatch(Notification notification, List<Account> accounts, bool save = false) public async Task SendNotificationBatch(Notification notification, List<Models.Account> accounts, bool save = false)
{ {
if (save) if (save)
{ {
@ -198,16 +185,7 @@ public class NotificationService(
await db.BulkInsertAsync(notifications); await db.BulkInsertAsync(notifications);
} }
foreach (var account in accounts)
{
notification.Account = account;
notification.AccountId = account.Id;
ws.SendPacketToAccount(account.Id, new WebSocketPacket
{
Type = "notifications.new",
Data = notification
});
}
var accountsId = accounts.Select(x => x.Id).ToList(); var accountsId = accounts.Select(x => x.Id).ToList();
var subscribers = await db.NotificationPushSubscriptions var subscribers = await db.NotificationPushSubscriptions

View File

@ -1,10 +1,13 @@
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Data;
using DysonNetwork.Pass.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using DysonNetwork.Common.Models;
using DysonNetwork.Pass.Permission;
namespace DysonNetwork.Sphere.Account; namespace DysonNetwork.Pass.Features.Account;
public class RelationshipService(AppDatabase db, ICacheService cache) public class RelationshipService(PassDatabase db, ICacheService cache) : IRelationshipService
{ {
private const string UserFriendsCacheKeyPrefix = "accounts:friends:"; private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:"; private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
@ -34,7 +37,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
return relationship; return relationship;
} }
public async Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status) public async Task<Relationship> CreateRelationship(Models.Account sender, Models.Account target, RelationshipStatus status)
{ {
if (status == RelationshipStatus.Pending) if (status == RelationshipStatus.Pending)
throw new InvalidOperationException( throw new InvalidOperationException(
@ -57,14 +60,14 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
return relationship; return relationship;
} }
public async Task<Relationship> BlockAccount(Account sender, Account target) public async Task<Relationship> BlockAccount(Models.Account sender, Models.Account target)
{ {
if (await HasExistingRelationship(sender.Id, target.Id)) if (await HasExistingRelationship(sender.Id, target.Id))
return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked);
return await CreateRelationship(sender, target, RelationshipStatus.Blocked); return await CreateRelationship(sender, target, RelationshipStatus.Blocked);
} }
public async Task<Relationship> UnblockAccount(Account sender, Account target) public async Task<Relationship> UnblockAccount(Models.Account sender, Models.Account target)
{ {
var relationship = await GetRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); 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."); if (relationship is null) throw new ArgumentException("There is no relationship between you and the user.");
@ -76,7 +79,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
return relationship; return relationship;
} }
public async Task<Relationship> SendFriendRequest(Account sender, Account target) public async Task<Relationship> SendFriendRequest(Models.Account sender, Models.Account target)
{ {
if (await HasExistingRelationship(sender.Id, target.Id)) if (await HasExistingRelationship(sender.Id, target.Id))
throw new InvalidOperationException("Found existing relationship between you and target user."); throw new InvalidOperationException("Found existing relationship between you and target user.");
@ -152,7 +155,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
return relationship; return relationship;
} }
public async Task<List<Guid>> ListAccountFriends(Account account) public async Task<List<Guid>> ListAccountFriends(Models.Account account)
{ {
var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}";
var friends = await cache.GetAsync<List<Guid>>(cacheKey); var friends = await cache.GetAsync<List<Guid>>(cacheKey);
@ -171,7 +174,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
return friends ?? []; return friends ?? [];
} }
public async Task<List<Guid>> ListAccountBlocked(Account account) public async Task<List<Guid>> ListAccountBlocked(Models.Account account)
{ {
var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}"; var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}";
var blocked = await cache.GetAsync<List<Guid>>(cacheKey); var blocked = await cache.GetAsync<List<Guid>>(cacheKey);

View File

@ -1,23 +1,22 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Account; using DysonNetwork.Pass.Features.Account;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NodaTime; using NodaTime;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Connection; using DysonNetwork.Pass.Data;
using DysonNetwork.Common.Models;
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Pass.Features.Auth;
[ApiController] [ApiController]
[Route("/auth")] [Route("/auth")]
public class AuthController( public class AuthController(
AppDatabase db, PassDatabase db,
AccountService accounts, AccountService accounts,
AuthService auth, AuthService auth
GeoIpService geo,
ActionLogService als
) : ControllerBase ) : ControllerBase
{ {
public class ChallengeRequest public class ChallengeRequest
@ -59,7 +58,7 @@ public class AuthController(
Scopes = request.Scopes, Scopes = request.Scopes,
IpAddress = ipAddress, IpAddress = ipAddress,
UserAgent = userAgent, UserAgent = userAgent,
Location = geo.GetPointFromIp(ipAddress),
DeviceId = request.DeviceId, DeviceId = request.DeviceId,
AccountId = account.Id AccountId = account.Id
}.Normalize(); }.Normalize();
@ -67,9 +66,7 @@ public class AuthController(
await db.AuthChallenges.AddAsync(challenge); await db.AuthChallenges.AddAsync(challenge);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
);
return challenge; return challenge;
} }
@ -160,13 +157,7 @@ public class AuthController(
challenge.StepRemain = Math.Max(0, challenge.StepRemain); challenge.StepRemain = Math.Max(0, challenge.StepRemain);
challenge.BlacklistFactors.Add(factor.Id); challenge.BlacklistFactors.Add(factor.Id);
db.Update(challenge); db.Update(challenge);
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
new Dictionary<string, object>
{
{ "challenge_id", challenge.Id },
{ "factor_id", factor.Id }
}, Request, challenge.Account
);
} }
else else
{ {
@ -177,26 +168,14 @@ public class AuthController(
{ {
challenge.FailedAttempts++; challenge.FailedAttempts++;
db.Update(challenge); db.Update(challenge);
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
new Dictionary<string, object>
{
{ "challenge_id", challenge.Id },
{ "factor_id", factor.Id }
}, Request, challenge.Account
);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return BadRequest("Invalid password."); return BadRequest("Invalid password.");
} }
if (challenge.StepRemain == 0) if (challenge.StepRemain == 0)
{ {
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
new Dictionary<string, object>
{
{ "challenge_id", challenge.Id },
{ "account_id", challenge.AccountId }
}, Request, challenge.Account
);
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();

View File

@ -0,0 +1,15 @@
using DysonNetwork.Common.Models;
using NodaTime;
namespace DysonNetwork.Pass.Features.Auth.Interfaces;
public interface IAuthService
{
Task<int> DetectChallengeRisk(HttpRequest request, Common.Models.Account account);
Task<AuthSession> CreateSessionForOidcAsync(Common.Models.Account account, Instant time, Guid? customAppId = null);
Task<bool> ValidateCaptcha(string token);
string CreateToken(AuthSession session);
Task<bool> ValidateSudoMode(AuthSession session, string? pinCode);
Task<bool> ValidatePinCode(Guid accountId, string pinCode);
bool ValidateToken(string token, out Guid sessionId);
}

View File

@ -1,22 +1,15 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using DysonNetwork.Sphere.Account; using DysonNetwork.Pass.Features.Account;
using DysonNetwork.Sphere.Auth.OidcProvider.Options; using DysonNetwork.Pass.Features.Auth.OidcProvider.Services;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
using DysonNetwork.Sphere.Storage.Handlers; using DysonNetwork.Pass.Storage.Handlers;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
using System.Text;
using DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
using SystemClock = NodaTime.SystemClock; using SystemClock = NodaTime.SystemClock;
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Pass.Features.Auth.Services;
public static class AuthConstants public static class AuthConstants
{ {
@ -46,7 +39,7 @@ public class DysonTokenAuthHandler(
IConfiguration configuration, IConfiguration configuration,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder, UrlEncoder encoder,
AppDatabase database, PassDatabase database,
OidcProviderService oidc, OidcProviderService oidc,
ICacheService cache, ICacheService cache,
FlushBufferService fbs FlushBufferService fbs

View File

@ -1,20 +1,36 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Account; using DysonNetwork.Pass.Features.Account;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using DysonNetwork.Pass.Data;
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Pass.Features.Auth;
public class AuthService( public class AuthService
AppDatabase db,
IConfiguration config,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
ICacheService cache
)
{ {
private readonly PassDatabase db;
private readonly IConfiguration config;
private readonly IHttpClientFactory httpClientFactory;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly ICacheService cache;
public AuthService(
PassDatabase db,
IConfiguration config,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
ICacheService cache
)
{
this.db = db;
this.config = config;
this.httpClientFactory = httpClientFactory;
this.httpContextAccessor = httpContextAccessor;
this.cache = cache;
}
private HttpContext HttpContext => httpContextAccessor.HttpContext!; private HttpContext HttpContext => httpContextAccessor.HttpContext!;
/// <summary> /// <summary>
@ -24,7 +40,7 @@ public class AuthService(
/// <param name="request">The request context</param> /// <param name="request">The request context</param>
/// <param name="account">The account to login</param> /// <param name="account">The account to login</param>
/// <returns>The required steps to login</returns> /// <returns>The required steps to login</returns>
public async Task<int> DetectChallengeRisk(HttpRequest request, Account.Account account) public async Task<int> DetectChallengeRisk(HttpRequest request, Models.Account account)
{ {
// 1) Find out how many authentication factors the account has enabled. // 1) Find out how many authentication factors the account has enabled.
var maxSteps = await db.AccountAuthFactors var maxSteps = await db.AccountAuthFactors
@ -73,7 +89,7 @@ public class AuthService(
return totalRequiredSteps; return totalRequiredSteps;
} }
public async Task<Session> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null) public async Task<Session> CreateSessionForOidcAsync(Models.Account account, Instant time, Guid? customAppId = null)
{ {
var challenge = new Challenge var challenge = new Challenge
{ {

View File

@ -1,4 +1,4 @@
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Pass.Features.Auth;
public class CaptchaVerificationResponse public class CaptchaVerificationResponse
{ {

View File

@ -1,6 +1,6 @@
using System.Security.Cryptography; using System.Security.Cryptography;
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Pass.Features.Auth;
public class CompactTokenService(IConfiguration config) public class CompactTokenService(IConfiguration config)
{ {

View File

@ -1,23 +1,24 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using DysonNetwork.Sphere.Auth.OidcProvider.Options; using DysonNetwork.Pass.Features.Auth.OidcProvider.Options;
using DysonNetwork.Sphere.Auth.OidcProvider.Responses; using DysonNetwork.Pass.Features.Auth.OidcProvider.Responses;
using DysonNetwork.Sphere.Auth.OidcProvider.Services; using DysonNetwork.Pass.Features.Auth.OidcProvider.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Account; using DysonNetwork.Pass.Features.Account;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using NodaTime; using NodaTime;
using DysonNetwork.Pass.Data;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Controllers;
[Route("/auth/open")] [Route("/auth/open")]
[ApiController] [ApiController]
public class OidcProviderController( public class OidcProviderController(
AppDatabase db, PassDatabase db,
OidcProviderService oidcService, OidcProviderService oidcService,
IConfiguration configuration, IConfiguration configuration,
IOptions<OidcProviderOptions> options, IOptions<OidcProviderOptions> options,
@ -114,7 +115,7 @@ public class OidcProviderController(
[Authorize] [Authorize]
public async Task<IActionResult> GetUserInfo() public async Task<IActionResult> GetUserInfo()
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || if (HttpContext.Items["CurrentUser"] is not Pass.Models.Account currentUser ||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
// Get requested scopes from the token // Get requested scopes from the token

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Models; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Models;
public class AuthorizationCodeInfo public class AuthorizationCodeInfo
{ {

View File

@ -1,6 +1,6 @@
using System.Security.Cryptography; using System.Security.Cryptography;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Options; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Options;
public class OidcProviderOptions public class OidcProviderOptions
{ {

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Responses;
public class AuthorizationResponse public class AuthorizationResponse
{ {

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Responses;
public class ErrorResponse public class ErrorResponse
{ {

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Responses;
public class TokenResponse public class TokenResponse
{ {

View File

@ -1,21 +1,18 @@
using System.IdentityModel.Tokens.Jwt; using DysonNetwork.Pass.Features.Auth.OidcProvider.Models;
using System.Security.Claims; using DysonNetwork.Pass.Features.Auth.OidcProvider.Options;
using System.Security.Cryptography; using DysonNetwork.Pass.Features.Auth.OidcProvider.Responses;
using System.Text; using DysonNetwork.Pass.Developer;
using DysonNetwork.Sphere.Auth.OidcProvider.Models; using DysonNetwork.Pass.Storage;
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
using DysonNetwork.Sphere.Auth.OidcProvider.Responses;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using NodaTime; using NodaTime;
using DysonNetwork.Pass.Data;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Services; namespace DysonNetwork.Pass.Features.Auth.OidcProvider.Services;
public class OidcProviderService( public class OidcProviderService(
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache, ICacheService cache,
IOptions<OidcProviderOptions> options, IOptions<OidcProviderOptions> options,

View File

@ -1,13 +1,14 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
using DysonNetwork.Pass.Data;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
public class AfdianOidcService( public class AfdianOidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache, ICacheService cache,
ILogger<AfdianOidcService> logger ILogger<AfdianOidcService> logger

View File

@ -2,7 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
public class AppleMobileConnectRequest public class AppleMobileConnectRequest
{ {

View File

@ -3,10 +3,10 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Common.Services;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
/// <summary> /// <summary>
/// Implementation of OpenID Connect service for Apple Sign In /// Implementation of OpenID Connect service for Apple Sign In
@ -14,7 +14,7 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
public class AppleOidcService( public class AppleOidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache ICacheService cache
) )

View File

@ -1,17 +1,19 @@
using DysonNetwork.Sphere.Account; using DysonNetwork.Pass.Features.Account;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
using NodaTime; using NodaTime;
using DysonNetwork.Pass.Data;
using DysonNetwork.Common.Models;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
[ApiController] [ApiController]
[Route("/accounts/me/connections")] [Route("/accounts/me/connections")]
[Authorize] [Authorize]
public class ConnectionController( public class ConnectionController(
AppDatabase db, PassDatabase db,
IEnumerable<OidcService> oidcServices, IEnumerable<OidcService> oidcServices,
AccountService accounts, AccountService accounts,
AuthService auth, AuthService auth,
@ -25,7 +27,7 @@ public class ConnectionController(
[HttpGet] [HttpGet]
public async Task<ActionResult<List<AccountConnection>>> GetConnections() public async Task<ActionResult<List<AccountConnection>>> GetConnections()
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) if (HttpContext.Items["CurrentUser"] is not Models.Account currentUser)
return Unauthorized(); return Unauthorized();
var connections = await db.AccountConnections var connections = await db.AccountConnections
@ -48,7 +50,7 @@ public class ConnectionController(
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
public async Task<ActionResult> RemoveConnection(Guid id) public async Task<ActionResult> RemoveConnection(Guid id)
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) if (HttpContext.Items["CurrentUser"] is not Models.Account currentUser)
return Unauthorized(); return Unauthorized();
var connection = await db.AccountConnections var connection = await db.AccountConnections
@ -66,7 +68,7 @@ public class ConnectionController(
[HttpPost("/auth/connect/apple/mobile")] [HttpPost("/auth/connect/apple/mobile")]
public async Task<ActionResult> ConnectAppleMobile([FromBody] AppleMobileConnectRequest request) public async Task<ActionResult> ConnectAppleMobile([FromBody] AppleMobileConnectRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) if (HttpContext.Items["CurrentUser"] is not Models.Account currentUser)
return Unauthorized(); return Unauthorized();
if (GetOidcService("apple") is not AppleOidcService appleService) if (GetOidcService("apple") is not AppleOidcService appleService)
@ -132,7 +134,7 @@ public class ConnectionController(
[HttpPost("connect")] [HttpPost("connect")]
public async Task<ActionResult<object>> InitiateConnection([FromBody] ConnectProviderRequest request) public async Task<ActionResult<object>> InitiateConnection([FromBody] ConnectProviderRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) if (HttpContext.Items["CurrentUser"] is not Models.Account currentUser)
return Unauthorized(); return Unauthorized();
var oidcService = GetOidcService(request.Provider); var oidcService = GetOidcService(request.Provider);

View File

@ -1,13 +1,13 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
public class DiscordOidcService( public class DiscordOidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache ICacheService cache
) )

View File

@ -1,13 +1,13 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
public class GitHubOidcService( public class GitHubOidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache ICacheService cache
) )

View File

@ -2,15 +2,15 @@ using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Common.Services;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
public class GoogleOidcService( public class GoogleOidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache ICacheService cache
) )

View File

@ -1,13 +1,13 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Pass.Storage;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
public class MicrosoftOidcService( public class MicrosoftOidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache ICacheService cache
) )

View File

@ -1,17 +1,16 @@
using DysonNetwork.Sphere.Account; using DysonNetwork.Common.Services;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
[ApiController] [ApiController]
[Route("/auth/login")] [Route("/auth/login")]
public class OidcController( public class OidcController(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
AppDatabase db, PassDatabase db,
AccountService accounts, AccountService accounts,
ICacheService cache ICacheService cache
) )
@ -32,7 +31,7 @@ public class OidcController(
var oidcService = GetOidcService(provider); var oidcService = GetOidcService(provider);
// If the user is already authenticated, treat as an account connection request // If the user is already authenticated, treat as an account connection request
if (HttpContext.Items["CurrentUser"] is Account.Account currentUser) if (HttpContext.Items["CurrentUser"] is Models.Account currentUser)
{ {
var state = Guid.NewGuid().ToString(); var state = Guid.NewGuid().ToString();
var nonce = Guid.NewGuid().ToString(); var nonce = Guid.NewGuid().ToString();
@ -125,7 +124,7 @@ public class OidcController(
}; };
} }
private async Task<Account.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider) private async Task<Models.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
{ {
if (string.IsNullOrEmpty(userInfo.Email)) if (string.IsNullOrEmpty(userInfo.Email))
throw new ArgumentException("Email is required for account creation"); throw new ArgumentException("Email is required for account creation");

View File

@ -1,13 +1,12 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Account; using DysonNetwork.Common.Services;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
/// <summary> /// <summary>
/// Base service for OpenID Connect authentication providers /// Base service for OpenID Connect authentication providers
@ -15,7 +14,7 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
public abstract class OidcService( public abstract class OidcService(
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AppDatabase db, PassDatabase db,
AuthService auth, AuthService auth,
ICacheService cache ICacheService cache
) )
@ -190,7 +189,7 @@ public abstract class OidcService(
/// </summary> /// </summary>
public async Task<Challenge> CreateChallengeForUserAsync( public async Task<Challenge> CreateChallengeForUserAsync(
OidcUserInfo userInfo, OidcUserInfo userInfo,
Account.Account account, Models.Account account,
HttpContext request, HttpContext request,
string deviceId string deviceId
) )

View File

@ -1,7 +1,7 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
/// <summary> /// <summary>
/// Represents the state parameter used in OpenID Connect flows. /// Represents the state parameter used in OpenID Connect flows.

View File

@ -1,4 +1,4 @@
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Pass.Features.Auth.OpenId;
/// <summary> /// <summary>
/// Represents the user information from an OIDC provider /// Represents the user information from an OIDC provider

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class AccountEventResource
{
// Dummy class for AccountEventResource
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class EmailResource
{
// Dummy class for EmailResource
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class NotificationResource
{
// Dummy class for NotificationResource
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class SharedResource
{
// Dummy class for SharedResource
}

View File

@ -0,0 +1,51 @@
namespace DysonNetwork.Pass.Pages.Emails;
public class LandingEmail
{
// Dummy class
}
public class LandingEmailModel
{
// Dummy class
}
public class AccountDeletionEmail
{
// Dummy class
}
public class AccountDeletionEmailModel
{
// Dummy class
}
public class PasswordResetEmail
{
// Dummy class
}
public class PasswordResetEmailModel
{
// Dummy class
}
public class ContactVerificationEmail
{
// Dummy class
}
public class ContactVerificationEmailModel
{
// Dummy class
}
public class VerificationEmail
{
// Dummy class
}
public class VerificationEmailModel
{
// Dummy class
}

View File

@ -0,0 +1,115 @@
using DysonNetwork.Pass.Data;
using DysonNetwork.Pass.Features.Account;
using DysonNetwork.Pass.Features.Auth;
using DysonNetwork.Pass.Features.Auth.OidcProvider.Options;
using DysonNetwork.Pass.Features.Auth.OidcProvider.Services;
using DysonNetwork.Pass.Features.Auth.OpenId;
using DysonNetwork.Pass.Storage;
using DysonNetwork.Pass.Storage.Handlers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
using System.Text;
using DysonNetwork.Pass.Email;
using DysonNetwork.Pass.Developer;
using DysonNetwork.Pass.Features.Account.DysonNetwork.Pass.Features.Account;
using DysonNetwork.Pass.Features.Account.Services;
using DysonNetwork.Pass.Permission;
using Quartz;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure AppDatabase
builder.Services.AddDbContext<PassDatabase>(options =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("App"),
o => o.UseNodaTime().UseNetTopologySuite().UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
});
// Add custom services
builder.Services.AddScoped<AccountService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<MagicSpellService>();
builder.Services.AddScoped<AccountEventService>();
builder.Services.AddScoped<AccountUsernameService>();
builder.Services.AddScoped<NotificationService>();
builder.Services.AddScoped<RelationshipService>();
builder.Services.AddScoped<EmailService>();
builder.Services.AddScoped<PermissionService>();
// Add OIDC services
builder.Services.AddScoped<OidcProviderService>();
builder.Services.AddScoped<AppleOidcService>();
builder.Services.AddScoped<GoogleOidcService>();
builder.Services.AddScoped<MicrosoftOidcService>();
builder.Services.AddScoped<DiscordOidcService>();
builder.Services.AddScoped<GitHubOidcService>();
builder.Services.AddScoped<AfdianOidcService>();
// Add other services
builder.Services.AddSingleton<ICacheService, InMemoryCacheService>();
builder.Services.AddSingleton<FlushBufferService>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient();
// Configure OIDC Provider Options
builder.Services.Configure<OidcProviderOptions>(builder.Configuration.GetSection("OidcProvider"));
// Configure Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false, // Will be validated by the OidcProviderService
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["OidcProvider:IssuerUri"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
// Configure Quartz for background jobs
builder.Services.AddQuartz(q =>
{
var jobKey = new JobKey("PassDatabaseRecyclingJob");
q.AddJob<PassDatabaseRecyclingJob>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithSimpleSchedule(s => s.WithIntervalInHours(24).RepeatForever())
.StartAt(DateBuilder.EvenHourDate(DateTimeOffset.UtcNow).AddHours(1))
);
});
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5048",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7212;http://localhost:5048",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Resources.Localization;
public class EmailResource
{
// Dummy class
}

View File

@ -0,0 +1,51 @@
namespace DysonNetwork.Pass.Resources.Pages.Emails;
public class AccountDeletionEmail
{
// Dummy class
}
public class AccountDeletionEmailModel
{
// Dummy class
}
public class ContactVerificationEmail
{
// Dummy class
}
public class ContactVerificationEmailModel
{
// Dummy class
}
public class LandingEmail
{
// Dummy class
}
public class LandingEmailModel
{
// Dummy class
}
public class PasswordResetEmail
{
// Dummy class
}
public class PasswordResetEmailModel
{
// Dummy class
}
public class VerificationEmail
{
// Dummy class
}
public class VerificationEmailModel
{
// Dummy class
}

View File

@ -0,0 +1,105 @@
using System.Collections.Concurrent;
using DysonNetwork.Pass.Features.Account;
using DysonNetwork.Common.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Pass.Storage.Handlers;
public class FlushBufferService
{
private readonly ConcurrentQueue<object> _buffer = new();
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<FlushBufferService> _logger;
private Timer? _timer;
public FlushBufferService(IServiceScopeFactory scopeFactory, ILogger<FlushBufferService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
_timer = new Timer(FlushBuffer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
}
public void Enqueue(object item)
{
_buffer.Enqueue(item);
}
private async void FlushBuffer(object? state)
{
if (_buffer.IsEmpty) return;
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Data.PassDatabase>();
var itemsToProcess = new List<object>();
while (_buffer.TryDequeue(out var item))
{
itemsToProcess.Add(item);
}
if (itemsToProcess.Count == 0) return;
_logger.LogInformation("Flushing {Count} items from buffer.", itemsToProcess.Count);
foreach (var item in itemsToProcess)
{
switch (item)
{
case LastActiveInfo lastActiveInfo:
await HandleLastActiveInfo(db, lastActiveInfo);
break;
case ActionLog actionLog:
await HandleActionLog(db, actionLog);
break;
default:
_logger.LogWarning("Unknown item type in buffer: {Type}", item.GetType().Name);
break;
}
}
try
{
await db.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving changes during buffer flush.");
}
}
private async Task HandleLastActiveInfo(Data.PassDatabase db, LastActiveInfo info)
{
var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == info.Account.Id);
if (profile != null)
{
profile.LastSeenAt = info.SeenAt;
db.AccountProfiles.Update(profile);
}
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == info.Session.Id);
if (session != null)
{
session.LastGrantedAt = info.SeenAt;
db.AuthSessions.Update(session);
}
}
private async Task HandleActionLog(Data.PassDatabase db, ActionLog log)
{
db.ActionLogs.Add(log);
}
public void Dispose()
{
_timer?.Dispose();
_timer = null;
}
}
public class LastActiveInfo
{
public Account Account { get; set; } = null!;
public AuthSession Session { get; set; } = null!;
public Instant SeenAt { get; set; }
}

View File

@ -0,0 +1,127 @@
using System.Collections.Concurrent;
namespace DysonNetwork.Pass.Storage;
public interface ICacheService
{
Task<T?> GetAsync<T>(string key);
Task<(bool found, T? value)> GetAsyncWithStatus<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiry = null);
Task SetWithGroupsAsync<T>(string key, T value, string[] groups, TimeSpan? expiry = null);
Task RemoveAsync(string key);
Task RemoveGroupAsync(string groupName);
Task<IAsyncDisposable?> AcquireLockAsync(string key, TimeSpan expiry, TimeSpan? wait = null);
}
public class InMemoryCacheService : ICacheService
{
private readonly ConcurrentDictionary<string, object> _cache = new();
private readonly ConcurrentDictionary<string, HashSet<string>> _groups = new();
private readonly ConcurrentDictionary<string, DateTimeOffset> _expirations = new();
public Task<T?> GetAsync<T>(string key)
{
if (_cache.TryGetValue(key, out var value) && !IsExpired(key))
{
return Task.FromResult((T?)value);
}
Remove(key);
return Task.FromResult(default(T));
}
public Task<(bool found, T? value)> GetAsyncWithStatus<T>(string key)
{
if (_cache.TryGetValue(key, out var value) && !IsExpired(key))
{
return Task.FromResult((true, (T?)value));
}
Remove(key);
return Task.FromResult((false, default(T)));
}
public Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
{
_cache[key] = value!;
_expirations[key] = expiry.HasValue ? DateTimeOffset.UtcNow.Add(expiry.Value) : DateTimeOffset.MaxValue;
return Task.CompletedTask;
}
public Task SetWithGroupsAsync<T>(string key, T value, string[] groups, TimeSpan? expiry = null)
{
_cache[key] = value!;
_expirations[key] = expiry.HasValue ? DateTimeOffset.UtcNow.Add(expiry.Value) : DateTimeOffset.MaxValue;
foreach (var group in groups)
{
_groups.GetOrAdd(group, _ => new HashSet<string>()).Add(key);
}
return Task.CompletedTask;
}
public Task RemoveAsync(string key)
{
Remove(key);
return Task.CompletedTask;
}
public Task RemoveGroupAsync(string groupName)
{
if (_groups.TryRemove(groupName, out var keysToRemove))
{
foreach (var key in keysToRemove)
{
Remove(key);
}
}
return Task.CompletedTask;
}
public async Task<IAsyncDisposable?> AcquireLockAsync(string key, TimeSpan expiry, TimeSpan? wait = null)
{
var startTime = DateTime.UtcNow;
while (true)
{
if (_cache.TryAdd(key, new object()))
{
_expirations[key] = DateTimeOffset.UtcNow.Add(expiry);
return new AsyncLockReleaser(() => Remove(key));
}
if (wait == null || DateTime.UtcNow - startTime > wait.Value)
{
return null; // Could not acquire lock within wait time
}
await Task.Delay(50); // Wait a bit before retrying
}
}
private bool IsExpired(string key)
{
return _expirations.TryGetValue(key, out var expiration) && expiration <= DateTimeOffset.UtcNow;
}
private void Remove(string key)
{
_cache.TryRemove(key, out _);
_expirations.TryRemove(key, out _);
foreach (var group in _groups.Values)
{
group.Remove(key);
}
}
private class AsyncLockReleaser : IAsyncDisposable
{
private readonly Action _releaseAction;
public AsyncLockReleaser(Action releaseAction)
{
_releaseAction = releaseAction;
}
public ValueTask DisposeAsync()
{
_releaseAction();
return ValueTask.CompletedTask;
}
}
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Wallet.PaymentHandlers;
public class AfdianPaymentHandler
{
// Dummy class
}

View File

@ -0,0 +1,50 @@
namespace DysonNetwork.Pass.Wallet;
public class Subscription
{
// Dummy class
}
public class SubscriptionReferenceObject
{
// Dummy class
}
public class Wallet
{
// Dummy class
}
public class WalletPocket
{
// Dummy class
}
public class Order
{
// Dummy class
}
public class Transaction
{
// Dummy class
}
public class Coupon
{
// Dummy class
}
public enum WalletCurrency
{
// Dummy enum
SourcePoint
}
public class PaymentService
{
public Task CreateTransactionWithAccountAsync(object a, Guid b, WalletCurrency c, decimal d, string e)
{
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -1,4 +1,3 @@
using Microsoft.AspNetCore.Mvc;
using NodaTime; using NodaTime;
using NodaTime.Text; using NodaTime.Text;
@ -22,7 +21,7 @@ public class ActivityController(
/// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them. /// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them.
/// </summary> /// </summary>
[HttpGet] [HttpGet]
public async Task<ActionResult<List<Activity>>> ListActivities( public async Task<ActionResult<List<Common.Models.Activity>>> ListActivities(
[FromQuery] string? cursor, [FromQuery] string? cursor,
[FromQuery] string? filter, [FromQuery] string? filter,
[FromQuery] int take = 20, [FromQuery] int take = 20,
@ -45,7 +44,7 @@ public class ActivityController(
var debugIncludeSet = debugInclude?.Split(',').ToHashSet() ?? new HashSet<string>(); var debugIncludeSet = debugInclude?.Split(',').ToHashSet() ?? new HashSet<string>();
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
return currentUserValue is not Account.Account currentUser return currentUserValue is Account.Account currentUser
? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet)) ? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet))
: Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet)); : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet));
} }

View File

@ -1,4 +1,4 @@
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Connection.WebReader; using DysonNetwork.Sphere.Connection.WebReader;
using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Post; using DysonNetwork.Sphere.Post;
@ -11,11 +11,11 @@ namespace DysonNetwork.Sphere.Activity;
public class ActivityService( public class ActivityService(
AppDatabase db, AppDatabase db,
PublisherService pub, PublisherService pub,
RelationshipService rels,
PostService ps, PostService ps,
DiscoveryService ds) DiscoveryService ds)
{ {
private static double CalculateHotRank(Post.Post post, Instant now) private static double CalculateHotRank(Common.Models.Post post, Instant now)
{ {
var score = post.Upvotes - post.Downvotes; var score = post.Upvotes - post.Downvotes;
var postTime = post.PublishedAt ?? post.CreatedAt; var postTime = post.PublishedAt ?? post.CreatedAt;
@ -24,10 +24,10 @@ public class ActivityService(
return (score + 1) / Math.Pow(hours + 2, 1.8); return (score + 1) / Math.Pow(hours + 2, 1.8);
} }
public async Task<List<Activity>> GetActivitiesForAnyone(int take, Instant? cursor, public async Task<List<Common.Models.Activity>> GetActivitiesForAnyone(int take, Instant? cursor,
HashSet<string>? debugInclude = null) HashSet<string>? debugInclude = null)
{ {
var activities = new List<Activity>(); var activities = new List<Common.Models.Activity>();
debugInclude ??= new HashSet<string>(); debugInclude ??= new HashSet<string>();
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
@ -110,12 +110,12 @@ public class ActivityService(
activities.Add(post.ToActivity()); activities.Add(post.ToActivity());
if (activities.Count == 0) if (activities.Count == 0)
activities.Add(Activity.Empty()); activities.Add(Common.Models.Activity.Empty());
return activities; return activities;
} }
public async Task<List<Activity>> GetActivities( public async Task<List<Common.Models.Activity>> GetActivities(
int take, int take,
Instant? cursor, Instant? cursor,
Account.Account currentUser, Account.Account currentUser,
@ -123,8 +123,7 @@ public class ActivityService(
HashSet<string>? debugInclude = null HashSet<string>? debugInclude = null
) )
{ {
var activities = new List<Activity>(); var activities = new List<Common.Models.Activity>();
var userFriends = await rels.ListAccountFriends(currentUser);
var userPublishers = await pub.GetUserPublishers(currentUser.Id); var userPublishers = await pub.GetUserPublishers(currentUser.Id);
debugInclude ??= []; debugInclude ??= [];
@ -191,9 +190,7 @@ public class ActivityService(
var filteredPublishers = filter switch var filteredPublishers = filter switch
{ {
"subscriptions" => await pub.GetSubscribedPublishers(currentUser.Id), "subscriptions" => await pub.GetSubscribedPublishers(currentUser.Id),
"friends" => (await pub.GetUserPublishersBatch(userFriends)).SelectMany(x => x.Value)
.DistinctBy(x => x.Id)
.ToList(),
_ => null _ => null
}; };
@ -214,7 +211,7 @@ public class ActivityService(
// Complete the query with visibility filtering and execute // Complete the query with visibility filtering and execute
var posts = await postsQuery var posts = await postsQuery
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true) .FilterWithVisibility(filter is null ? userPublishers : [], isListing: true)
.Take(take * 5) // Fetch more posts to have a good pool for ranking .Take(take * 5) // Fetch more posts to have a good pool for ranking
.ToListAsync(); .ToListAsync();
@ -245,12 +242,12 @@ public class ActivityService(
activities.Add(post.ToActivity()); activities.Add(post.ToActivity());
if (activities.Count == 0) if (activities.Count == 0)
activities.Add(Activity.Empty()); activities.Add(Common.Models.Activity.Empty());
return activities; return activities;
} }
private static double CalculatePopularity(List<Post.Post> posts) private static double CalculatePopularity(List<Common.Models.Post> posts)
{ {
var score = posts.Sum(p => p.Upvotes - p.Downvotes); var score = posts.Sum(p => p.Upvotes - p.Downvotes);
var postCount = posts.Count; var postCount = posts.Count;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using DysonNetwork.Common.Models;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Activity; namespace DysonNetwork.Sphere.Activity;
@ -8,10 +9,10 @@ public class DiscoveryActivity(List<DiscoveryItem> items) : IActivity
{ {
public List<DiscoveryItem> Items { get; set; } = items; public List<DiscoveryItem> Items { get; set; } = items;
public Activity ToActivity() public Common.Models.Activity ToActivity()
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
return new Activity return new Common.Models.Activity
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Type = "discovery", Type = "discovery",

Some files were not shown because too many files have changed in this diff Show More