:drunk: Write shit code trying to split up the Auth (WIP)
This commit is contained in:
23
DysonNetwork.Common/DysonNetwork.Common.csproj
Normal file
23
DysonNetwork.Common/DysonNetwork.Common.csproj
Normal 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>
|
@ -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.
|
6
DysonNetwork.Common/Interfaces/IIdentifiedResource.cs
Normal file
6
DysonNetwork.Common/Interfaces/IIdentifiedResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Common.Interfaces;
|
||||||
|
|
||||||
|
public interface IIdentifiedResource
|
||||||
|
{
|
||||||
|
public string ResourceIdentifier { get; }
|
||||||
|
}
|
@ -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
|
||||||
{
|
{
|
@ -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>();
|
@ -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
|
||||||
{
|
{
|
@ -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
|
||||||
{
|
{
|
@ -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;
|
@ -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()
|
||||||
{
|
{
|
@ -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; }
|
||||||
|
|
@ -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()
|
@ -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;
|
||||||
}
|
}
|
@ -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
|
14
DysonNetwork.Common/Models/LoginModels.cs
Normal file
14
DysonNetwork.Common/Models/LoginModels.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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
|
||||||
{
|
{
|
10
DysonNetwork.Common/Models/ModelBase.cs
Normal file
10
DysonNetwork.Common/Models/ModelBase.cs
Normal 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; }
|
||||||
|
}
|
@ -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!;
|
||||||
}
|
}
|
@ -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
|
||||||
{
|
{
|
@ -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.
|
@ -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!;
|
||||||
}
|
}
|
@ -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;
|
@ -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; }
|
@ -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
|
||||||
{
|
{
|
@ -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; }
|
||||||
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
17
DysonNetwork.Pass/Connection/DummyConnection.cs
Normal file
17
DysonNetwork.Pass/Connection/DummyConnection.cs
Normal 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!;
|
||||||
|
}
|
196
DysonNetwork.Pass/Data/PassDatabase.cs
Normal file
196
DysonNetwork.Pass/Data/PassDatabase.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
33
DysonNetwork.Pass/Developer/Developer.cs
Normal file
33
DysonNetwork.Pass/Developer/Developer.cs
Normal 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; }
|
||||||
|
}
|
49
DysonNetwork.Pass/DysonNetwork.Pass.csproj
Normal file
49
DysonNetwork.Pass/DysonNetwork.Pass.csproj
Normal 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>
|
6
DysonNetwork.Pass/DysonNetwork.Pass.http
Normal file
6
DysonNetwork.Pass/DysonNetwork.Pass.http
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@DysonNetwork.Pass_HostAddress = http://localhost:5048
|
||||||
|
|
||||||
|
GET {{DysonNetwork.Pass_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
6
DysonNetwork.Pass/Email/EmailModels.cs
Normal file
6
DysonNetwork.Pass/Email/EmailModels.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Email;
|
||||||
|
|
||||||
|
public class EmailModels
|
||||||
|
{
|
||||||
|
// Dummy class for EmailModels
|
||||||
|
}
|
10
DysonNetwork.Pass/Email/EmailService.cs
Normal file
10
DysonNetwork.Pass/Email/EmailService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
6
DysonNetwork.Pass/Email/RazorViewRenderer.cs
Normal file
6
DysonNetwork.Pass/Email/RazorViewRenderer.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Email;
|
||||||
|
|
||||||
|
public class RazorViewRenderer
|
||||||
|
{
|
||||||
|
// Dummy class for RazorViewRenderer
|
||||||
|
}
|
45
DysonNetwork.Pass/Extensions/OptionalQueryExtensions.cs
Normal file
45
DysonNetwork.Pass/Extensions/OptionalQueryExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 [];
|
@ -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
|
@ -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)
|
@ -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();
|
@ -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.");
|
@ -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);
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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,
|
private readonly PassDatabase db;
|
||||||
|
private readonly ICacheService cache;
|
||||||
|
private readonly IStringLocalizer<Localization.AccountEventResource> localizer;
|
||||||
|
|
||||||
|
public AccountEventService(
|
||||||
|
PassDatabase db,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
PaymentService payment,
|
|
||||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
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)
|
@ -1,22 +1,34 @@
|
|||||||
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,
|
{
|
||||||
|
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,
|
MagicSpellService spells,
|
||||||
AccountUsernameService uname,
|
AccountUsernameService uname,
|
||||||
NotificationService nty,
|
NotificationService nty,
|
||||||
@ -26,7 +38,17 @@ public class AccountService(
|
|||||||
ILogger<AccountService> logger
|
ILogger<AccountService> logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public static void SetCultureInfo(Account account)
|
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();
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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;
|
@ -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,
|
|
||||||
|
namespace DysonNetwork.Pass.Features.Account;
|
||||||
|
|
||||||
|
public class MagicSpellService
|
||||||
|
{
|
||||||
|
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,
|
EmailService email,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<MagicSpellService> logger,
|
ILogger<MagicSpellService> logger,
|
||||||
IStringLocalizer<Localization.EmailResource> localizer
|
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,
|
@ -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
|
@ -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);
|
@ -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();
|
15
DysonNetwork.Pass/Features/Auth/Interfaces/IAuthService.cs
Normal file
15
DysonNetwork.Pass/Features/Auth/Interfaces/IAuthService.cs
Normal 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);
|
||||||
|
}
|
@ -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
|
@ -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,
|
{
|
||||||
|
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,
|
IConfiguration config,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ICacheService cache
|
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
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Pass.Features.Auth;
|
||||||
|
|
||||||
public class CaptchaVerificationResponse
|
public class CaptchaVerificationResponse
|
||||||
{
|
{
|
@ -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)
|
||||||
{
|
{
|
@ -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
|
@ -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
|
||||||
{
|
{
|
@ -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
|
||||||
{
|
{
|
@ -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
|
||||||
{
|
{
|
@ -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
|
||||||
{
|
{
|
@ -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
|
||||||
{
|
{
|
@ -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,
|
@ -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
|
@ -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
|
||||||
{
|
{
|
@ -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
|
||||||
)
|
)
|
@ -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);
|
@ -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
|
||||||
)
|
)
|
@ -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
|
||||||
)
|
)
|
@ -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
|
||||||
)
|
)
|
@ -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
|
||||||
)
|
)
|
@ -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");
|
@ -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
|
||||||
)
|
)
|
@ -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.
|
@ -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
|
6
DysonNetwork.Pass/Localization/AccountEventResource.cs
Normal file
6
DysonNetwork.Pass/Localization/AccountEventResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class AccountEventResource
|
||||||
|
{
|
||||||
|
// Dummy class for AccountEventResource
|
||||||
|
}
|
6
DysonNetwork.Pass/Localization/EmailResource.cs
Normal file
6
DysonNetwork.Pass/Localization/EmailResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class EmailResource
|
||||||
|
{
|
||||||
|
// Dummy class for EmailResource
|
||||||
|
}
|
6
DysonNetwork.Pass/Localization/NotificationResource.cs
Normal file
6
DysonNetwork.Pass/Localization/NotificationResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class NotificationResource
|
||||||
|
{
|
||||||
|
// Dummy class for NotificationResource
|
||||||
|
}
|
6
DysonNetwork.Pass/Localization/SharedResource.cs
Normal file
6
DysonNetwork.Pass/Localization/SharedResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class SharedResource
|
||||||
|
{
|
||||||
|
// Dummy class for SharedResource
|
||||||
|
}
|
51
DysonNetwork.Pass/Pages/Emails/DummyEmails.cs
Normal file
51
DysonNetwork.Pass/Pages/Emails/DummyEmails.cs
Normal 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
|
||||||
|
}
|
115
DysonNetwork.Pass/Program.cs
Normal file
115
DysonNetwork.Pass/Program.cs
Normal 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();
|
23
DysonNetwork.Pass/Properties/launchSettings.json
Normal file
23
DysonNetwork.Pass/Properties/launchSettings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Resources.Localization;
|
||||||
|
|
||||||
|
public class EmailResource
|
||||||
|
{
|
||||||
|
// Dummy class
|
||||||
|
}
|
51
DysonNetwork.Pass/Resources/Pages/Emails/DummyEmails.cs
Normal file
51
DysonNetwork.Pass/Resources/Pages/Emails/DummyEmails.cs
Normal 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
|
||||||
|
}
|
105
DysonNetwork.Pass/Storage/Handlers/FlushBufferService.cs
Normal file
105
DysonNetwork.Pass/Storage/Handlers/FlushBufferService.cs
Normal 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; }
|
||||||
|
}
|
127
DysonNetwork.Pass/Storage/ICacheService.cs
Normal file
127
DysonNetwork.Pass/Storage/ICacheService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
DysonNetwork.Pass/Wallet/PaymentHandlers.cs
Normal file
6
DysonNetwork.Pass/Wallet/PaymentHandlers.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||||
|
|
||||||
|
public class AfdianPaymentHandler
|
||||||
|
{
|
||||||
|
// Dummy class
|
||||||
|
}
|
50
DysonNetwork.Pass/Wallet/Wallet.cs
Normal file
50
DysonNetwork.Pass/Wallet/Wallet.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
8
DysonNetwork.Pass/appsettings.Development.json
Normal file
8
DysonNetwork.Pass/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
DysonNetwork.Pass/appsettings.json
Normal file
9
DysonNetwork.Pass/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
Reference in New Issue
Block a user