♻️ Moving to MagicOnion
This commit is contained in:
@@ -12,13 +12,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.65.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MagicOnion.Client" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
|
||||
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
@@ -27,4 +27,10 @@
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore">
|
||||
<HintPath>..\..\..\..\..\..\opt\homebrew\Cellar\dotnet\9.0.6\libexec\shared\Microsoft.AspNetCore.App\9.0.6\Microsoft.AspNetCore.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
30
DysonNetwork.Shared/Models/MagicSpell.cs
Normal file
30
DysonNetwork.Shared/Models/MagicSpell.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public enum MagicSpellType
|
||||
{
|
||||
AccountActivation,
|
||||
AccountDeactivation,
|
||||
AccountRemoval,
|
||||
AuthPasswordReset,
|
||||
ContactVerification,
|
||||
}
|
||||
|
||||
[Index(nameof(Spell), IsUnique = true)]
|
||||
public class MagicSpell : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[JsonIgnore] [MaxLength(1024)] public string Spell { get; set; } = null!;
|
||||
public MagicSpellType Type { get; set; }
|
||||
public Instant? ExpiresAt { get; set; }
|
||||
public Instant? AffectedAt { get; set; }
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
|
||||
|
||||
public Guid? AccountId { get; set; }
|
||||
public Shared.Models.Account? Account { get; set; }
|
||||
}
|
||||
49
DysonNetwork.Shared/Models/OidcUserInfo.cs
Normal file
49
DysonNetwork.Shared/Models/OidcUserInfo.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the user information from an OIDC provider
|
||||
/// </summary>
|
||||
public class OidcUserInfo
|
||||
{
|
||||
public string? UserId { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public bool EmailVerified { get; set; }
|
||||
public string FirstName { get; set; } = "";
|
||||
public string LastName { get; set; } = "";
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string PreferredUsername { get; set; } = "";
|
||||
public string? ProfilePictureUrl { get; set; }
|
||||
public string Provider { get; set; } = "";
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
|
||||
public Dictionary<string, object> ToMetadata()
|
||||
{
|
||||
var metadata = new Dictionary<string, object>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(UserId))
|
||||
metadata["user_id"] = UserId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Email))
|
||||
metadata["email"] = Email;
|
||||
|
||||
metadata["email_verified"] = EmailVerified;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FirstName))
|
||||
metadata["first_name"] = FirstName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LastName))
|
||||
metadata["last_name"] = LastName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(DisplayName))
|
||||
metadata["display_name"] = DisplayName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(PreferredUsername))
|
||||
metadata["preferred_username"] = PreferredUsername;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ProfilePictureUrl))
|
||||
metadata["profile_picture_url"] = ProfilePictureUrl;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package dyson_network.sphere.account;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option csharp_namespace = "DysonNetwork.Shared.Protos.Account";
|
||||
|
||||
service AccountService {
|
||||
rpc GetAccount(google.protobuf.Empty) returns (AccountResponse);
|
||||
rpc UpdateAccount(UpdateAccountRequest) returns (AccountResponse);
|
||||
}
|
||||
|
||||
message AccountResponse {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string nick = 3;
|
||||
string language = 4;
|
||||
google.protobuf.Timestamp activated_at = 5;
|
||||
bool is_superuser = 6;
|
||||
Profile profile = 7;
|
||||
}
|
||||
|
||||
message Profile {
|
||||
string first_name = 1;
|
||||
string last_name = 2;
|
||||
string bio = 3;
|
||||
string gender = 4;
|
||||
string pronouns = 5;
|
||||
string time_zone = 6;
|
||||
string location = 7;
|
||||
google.protobuf.Timestamp birthday = 8;
|
||||
google.protobuf.Timestamp last_seen_at = 9;
|
||||
int32 experience = 10;
|
||||
int32 level = 11;
|
||||
double leveling_progress = 12;
|
||||
}
|
||||
|
||||
message UpdateAccountRequest {
|
||||
optional string nick = 1;
|
||||
optional string language = 2;
|
||||
optional string first_name = 3;
|
||||
optional string last_name = 4;
|
||||
optional string bio = 5;
|
||||
optional string gender = 6;
|
||||
optional string pronouns = 7;
|
||||
optional string time_zone = 8;
|
||||
optional string location = 9;
|
||||
optional google.protobuf.Timestamp birthday = 10;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package dyson_network.sphere.auth;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option csharp_namespace = "DysonNetwork.Shared.Protos.Auth";
|
||||
|
||||
service AuthService {
|
||||
rpc Login(LoginRequest) returns (LoginResponse);
|
||||
rpc IntrospectToken(IntrospectTokenRequest) returns (IntrospectionResponse);
|
||||
rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
optional string two_factor_code = 3;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
string access_token = 1;
|
||||
string refresh_token = 2;
|
||||
int64 expires_in = 3;
|
||||
}
|
||||
|
||||
message IntrospectTokenRequest {
|
||||
string token = 1;
|
||||
optional string token_type_hint = 2;
|
||||
}
|
||||
|
||||
message IntrospectionResponse {
|
||||
bool active = 1;
|
||||
string claims = 2;
|
||||
string client_id = 3;
|
||||
string username = 4;
|
||||
string scope = 5;
|
||||
google.protobuf.Timestamp iat = 6;
|
||||
google.protobuf.Timestamp exp = 7;
|
||||
}
|
||||
|
||||
message Session {
|
||||
string id = 1;
|
||||
string label = 2;
|
||||
google.protobuf.Timestamp last_granted_at = 3;
|
||||
google.protobuf.Timestamp expired_at = 4;
|
||||
string account_id = 5;
|
||||
string challenge_id = 6;
|
||||
optional string app_id = 7;
|
||||
}
|
||||
|
||||
message Challenge {
|
||||
string id = 1;
|
||||
google.protobuf.Timestamp expired_at = 2;
|
||||
int32 step_remain = 3;
|
||||
int32 step_total = 4;
|
||||
int32 failed_attempts = 5;
|
||||
string platform = 6;
|
||||
string type = 7;
|
||||
repeated string blacklist_factors = 8;
|
||||
repeated string audiences = 9;
|
||||
repeated string scopes = 10;
|
||||
string ip_address = 11;
|
||||
string user_agent = 12;
|
||||
optional string device_id = 13;
|
||||
optional string nonce = 14;
|
||||
string account_id = 15;
|
||||
}
|
||||
28
DysonNetwork.Shared/Services/IAccountEventService.cs
Normal file
28
DysonNetwork.Shared/Services/IAccountEventService.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IAccountEventService : IService<IAccountEventService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Purges the status cache for a user
|
||||
/// </summary>
|
||||
void PurgeStatusCache(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of a user
|
||||
/// </summary>
|
||||
Task<Status> GetStatus(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a daily check-in for a user
|
||||
/// </summary>
|
||||
Task<CheckInResult> CheckInDaily(Account user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the check-in streak for a user
|
||||
/// </summary>
|
||||
Task<int> GetCheckInStreak(Account user);
|
||||
}
|
||||
62
DysonNetwork.Shared/Services/IAccountService.cs
Normal file
62
DysonNetwork.Shared/Services/IAccountService.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IAccountService : IService<IAccountService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes all cached data for the specified account
|
||||
/// </summary>
|
||||
Task PurgeAccountCache(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an account by username or contact information
|
||||
/// </summary>
|
||||
/// <param name="probe">Username or contact information to search for</param>
|
||||
/// <returns>The matching account if found, otherwise null</returns>
|
||||
Task<Account?> LookupAccount(string probe);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an account by external authentication provider connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">The provider's unique identifier for the user</param>
|
||||
/// <param name="provider">The name of the authentication provider</param>
|
||||
/// <returns>The matching account if found, otherwise null</returns>
|
||||
Task<Account?> LookupAccountByConnection(string identifier, string provider);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the account level for the specified account ID
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account</param>
|
||||
/// <returns>The account level if found, otherwise null</returns>
|
||||
Task<int?> GetAccountLevel(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new account with the specified details
|
||||
/// </summary>
|
||||
/// <param name="name">The account username</param>
|
||||
/// <param name="nick">The display name/nickname</param>
|
||||
/// <param name="email">The primary email address</param>
|
||||
/// <param name="password">The account password (optional, can be set later)</param>
|
||||
/// <param name="language">The preferred language (defaults to en-US)</param>
|
||||
/// <param name="isEmailVerified">Whether the email is verified (defaults to false)</param>
|
||||
/// <param name="isActivated">Whether the account is activated (defaults to false)</param>
|
||||
/// <returns>The newly created account</returns>
|
||||
Task<Account> CreateAccount(
|
||||
string name,
|
||||
string nick,
|
||||
string email,
|
||||
string? password,
|
||||
string language = "en-US",
|
||||
bool isEmailVerified = false,
|
||||
bool isActivated = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new account using OpenID Connect user information
|
||||
/// </summary>
|
||||
/// <param name="userInfo">The OpenID Connect user information</param>
|
||||
/// <returns>The newly created account</returns>
|
||||
Task<Account> CreateAccount(OidcUserInfo userInfo);
|
||||
}
|
||||
23
DysonNetwork.Shared/Services/IAccountUsernameService.cs
Normal file
23
DysonNetwork.Shared/Services/IAccountUsernameService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IAccountUsernameService : IService<IAccountUsernameService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a unique username based on the provided base name
|
||||
/// </summary>
|
||||
/// <param name="baseName">The preferred username</param>
|
||||
/// <returns>A unique username</returns>
|
||||
Task<string> GenerateUniqueUsernameAsync(string baseName);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a username already exists
|
||||
/// </summary>
|
||||
Task<bool> IsUsernameExistsAsync(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a username to remove invalid characters
|
||||
/// </summary>
|
||||
string SanitizeUsername(string username);
|
||||
}
|
||||
24
DysonNetwork.Shared/Services/IActionLogService.cs
Normal file
24
DysonNetwork.Shared/Services/IActionLogService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IActionLogService : IService<IActionLogService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an action log entry
|
||||
/// </summary>
|
||||
void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action log entry from an HTTP request
|
||||
/// </summary>
|
||||
void CreateActionLogFromRequest(
|
||||
string action,
|
||||
Dictionary<string, object> meta,
|
||||
HttpRequest request,
|
||||
Account? account = null
|
||||
);
|
||||
}
|
||||
30
DysonNetwork.Shared/Services/IMagicSpellService.cs
Normal file
30
DysonNetwork.Shared/Services/IMagicSpellService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IMagicSpellService : IService<IMagicSpellService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new magic spell
|
||||
/// </summary>
|
||||
Task<MagicSpell> CreateMagicSpell(
|
||||
Account account,
|
||||
MagicSpellType type,
|
||||
Dictionary<string, object> meta,
|
||||
Instant? expiredAt = null,
|
||||
Instant? affectedAt = null,
|
||||
bool preventRepeat = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a magic spell by its token
|
||||
/// </summary>
|
||||
Task<MagicSpell?> GetMagicSpellAsync(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Consumes a magic spell
|
||||
/// </summary>
|
||||
Task ApplyMagicSpell(string token);
|
||||
}
|
||||
23
DysonNetwork.Shared/Services/INotificationService.cs
Normal file
23
DysonNetwork.Shared/Services/INotificationService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface INotificationService : IService<INotificationService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsubscribes a device from push notifications
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device ID to unsubscribe</param>
|
||||
Task UnsubscribePushNotifications(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes a device to push notifications
|
||||
/// </summary>
|
||||
Task<NotificationPushSubscription> SubscribePushNotification(
|
||||
Account account,
|
||||
NotificationPushProvider provider,
|
||||
string deviceId,
|
||||
string deviceToken
|
||||
);
|
||||
}
|
||||
27
DysonNetwork.Shared/Services/IRelationshipService.cs
Normal file
27
DysonNetwork.Shared/Services/IRelationshipService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IRelationshipService : IService<IRelationshipService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a relationship exists between two accounts
|
||||
/// </summary>
|
||||
Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a relationship between two accounts
|
||||
/// </summary>
|
||||
Task<Relationship?> GetRelationship(
|
||||
Guid accountId,
|
||||
Guid relatedId,
|
||||
RelationshipStatus? status = null,
|
||||
bool ignoreExpired = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new relationship between two accounts
|
||||
/// </summary>
|
||||
Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status);
|
||||
}
|
||||
60
DysonNetwork.Shared/Startup/ApplicationConfiguration.cs
Normal file
60
DysonNetwork.Shared/Startup/ApplicationConfiguration.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DysonNetwork.Shared.Startup;
|
||||
|
||||
public static class ApplicationConfiguration
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
|
||||
ConfigureForwardedHeaders(app, configuration);
|
||||
|
||||
app.UseCors(opts =>
|
||||
opts.SetIsOriginAllowed(_ => true)
|
||||
.WithExposedHeaders("*")
|
||||
.WithHeaders()
|
||||
.AllowCredentials()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
);
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseRateLimiter();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers().RequireRateLimiting("fixed");
|
||||
app.MapStaticAssets().RequireRateLimiting("fixed");
|
||||
app.MapRazorPages().RequireRateLimiting("fixed");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
var knownProxiesSection = configuration.GetSection("KnownProxies");
|
||||
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
||||
|
||||
if (knownProxiesSection.Exists())
|
||||
{
|
||||
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
||||
if (proxyAddresses != null)
|
||||
foreach (var proxy in proxyAddresses)
|
||||
if (IPAddress.TryParse(proxy, out var ipAddress))
|
||||
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
||||
}
|
||||
|
||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||
}
|
||||
}
|
||||
21
DysonNetwork.Shared/Startup/KestrelConfiguration.cs
Normal file
21
DysonNetwork.Shared/Startup/KestrelConfiguration.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DysonNetwork.Shared.Startup;
|
||||
|
||||
public static class KestrelConfiguration
|
||||
{
|
||||
public static WebApplicationBuilder ConfigureAppKestrel(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024;
|
||||
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
|
||||
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user