Compare commits
1 Commits
refactor/s
...
master
Author | SHA1 | Date | |
---|---|---|---|
8e5b6ace45 |
@ -1,269 +0,0 @@
|
|||||||
using System.Linq.Expressions;
|
|
||||||
using System.Reflection;
|
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using DysonNetwork.Pass.Permission;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NodaTime;
|
|
||||||
using Quartz;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass;
|
|
||||||
|
|
||||||
public abstract class ModelBase
|
|
||||||
{
|
|
||||||
public Instant CreatedAt { get; set; }
|
|
||||||
public Instant UpdatedAt { get; set; }
|
|
||||||
public Instant? DeletedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppDatabase(
|
|
||||||
DbContextOptions<AppDatabase> 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<Shared.Models.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<Status> AccountStatuses { get; set; }
|
|
||||||
public DbSet<CheckInResult> AccountCheckInResults { get; set; }
|
|
||||||
public DbSet<Badge> AccountBadges { get; set; }
|
|
||||||
|
|
||||||
public DbSet<ActionLog> ActionLogs { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Notification> Notifications { get; set; }
|
|
||||||
public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Session> AuthSessions { get; set; }
|
|
||||||
public DbSet<Challenge> AuthChallenges { get; set; }
|
|
||||||
|
|
||||||
public DbSet<MagicSpell> MagicSpells { get; set; }
|
|
||||||
public DbSet<AbuseReport> AbuseReports { 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) =>
|
|
||||||
{
|
|
||||||
var defaultPermissionGroup = await context.Set<PermissionGroup>()
|
|
||||||
.FirstOrDefaultAsync(g => g.Key == "default", cancellationToken);
|
|
||||||
if (defaultPermissionGroup is null)
|
|
||||||
{
|
|
||||||
context.Set<PermissionGroup>().Add(new PermissionGroup
|
|
||||||
{
|
|
||||||
Key = "default",
|
|
||||||
Nodes = new List<string>
|
|
||||||
{
|
|
||||||
"posts.create",
|
|
||||||
"posts.react",
|
|
||||||
"publishers.create",
|
|
||||||
"files.create",
|
|
||||||
"chat.create",
|
|
||||||
"chat.messages.create",
|
|
||||||
"chat.realtime.create",
|
|
||||||
"accounts.statuses.create",
|
|
||||||
"accounts.statuses.update",
|
|
||||||
"stickers.packs.create",
|
|
||||||
"stickers.create"
|
|
||||||
}.Select(permission =>
|
|
||||||
PermissionService.NewPermissionNode("group:default", "global", permission, true))
|
|
||||||
.ToList()
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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(AppDatabase)
|
|
||||||
.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>())
|
|
||||||
{
|
|
||||||
if (entry.State == EntityState.Added)
|
|
||||||
{
|
|
||||||
entry.Entity.CreatedAt = now;
|
|
||||||
entry.Entity.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
else if (entry.State == EntityState.Modified)
|
|
||||||
{
|
|
||||||
entry.Entity.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
else if (entry.State == EntityState.Deleted)
|
|
||||||
{
|
|
||||||
entry.State = EntityState.Modified;
|
|
||||||
entry.Entity.DeletedAt = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await base.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclingJob> 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);
|
|
||||||
// Expired permission group members
|
|
||||||
affectedRows = await db.PermissionGroupMembers
|
|
||||||
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
logger.LogDebug("Removed {Count} records of expired permission group members.", 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|
||||||
{
|
|
||||||
public AppDatabase CreateDbContext(string[] args)
|
|
||||||
{
|
|
||||||
var configuration = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddJsonFile("appsettings.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
|
|
||||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,77 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
|
||||||
<PackageReference Include="MagicOnion.Client" Version="7.0.5" />
|
|
||||||
<PackageReference Include="MagicOnion.Server" Version="7.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
|
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
|
||||||
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
|
|
||||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
|
||||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Update="Pages\Emails\ContactVerificationEmail.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
<Content Update="Pages\Emails\EmailLayout.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
<Content Update="Pages\Emails\LandingEmail.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
<Content Update="Pages\Emails\PasswordResetEmail.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
<Content Update="Pages\Emails\VerificationEmail.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
<Content Update="Pages\Emails\AccountDeletionEmail.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="Pages\Emails\AccountDeletionEmail.razor" />
|
|
||||||
<AdditionalFiles Include="Pages\Emails\ContactVerificationEmail.razor" />
|
|
||||||
<AdditionalFiles Include="Pages\Emails\EmailLayout.razor" />
|
|
||||||
<AdditionalFiles Include="Pages\Emails\LandingEmail.razor" />
|
|
||||||
<AdditionalFiles Include="Pages\Emails\PasswordResetEmail.razor" />
|
|
||||||
<AdditionalFiles Include="Pages\Emails\VerificationEmail.razor" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace DysonNetwork.Pass.Localization;
|
|
||||||
|
|
||||||
public class AccountEventResource
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
namespace DysonNetwork.Pass.Localization;
|
|
||||||
|
|
||||||
public class EmailResource
|
|
||||||
{
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace DysonNetwork.Pass.Localization;
|
|
||||||
|
|
||||||
public class NotificationResource
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace DysonNetwork.Pass.Localization;
|
|
||||||
|
|
||||||
public class SharedResource
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
@using DysonNetwork.Pass.Localization
|
|
||||||
@using Microsoft.Extensions.Localization
|
|
||||||
|
|
||||||
<EmailLayout>
|
|
||||||
<tr>
|
|
||||||
<td class="wrapper">
|
|
||||||
<p class="font-bold">@(Localizer["AccountDeletionHeader"])</p>
|
|
||||||
<p>@(Localizer["AccountDeletionPara1"]) @@@Name,</p>
|
|
||||||
<p>@(Localizer["AccountDeletionPara2"])</p>
|
|
||||||
<p>@(Localizer["AccountDeletionPara3"])</p>
|
|
||||||
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="left">
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="@Link" target="_blank">
|
|
||||||
@(Localizer["AccountDeletionButton"])
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p>@(Localizer["AccountDeletionPara4"])</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</EmailLayout>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public required string Name { get; set; }
|
|
||||||
[Parameter] public required string Link { get; set; }
|
|
||||||
|
|
||||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
@using DysonNetwork.Pass.Localization
|
|
||||||
@using Microsoft.Extensions.Localization
|
|
||||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
|
||||||
|
|
||||||
<EmailLayout>
|
|
||||||
<tr>
|
|
||||||
<td class="wrapper">
|
|
||||||
<p class="font-bold">@(Localizer["ContactVerificationHeader"])</p>
|
|
||||||
<p>@(Localizer["ContactVerificationPara1"]) @Name,</p>
|
|
||||||
<p>@(Localizer["ContactVerificationPara2"])</p>
|
|
||||||
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="left">
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="@Link" target="_blank">
|
|
||||||
@(Localizer["ContactVerificationButton"])
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p>@(Localizer["ContactVerificationPara3"])</p>
|
|
||||||
<p>@(Localizer["ContactVerificationPara4"])</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</EmailLayout>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public required string Name { get; set; }
|
|
||||||
[Parameter] public required string Link { get; set; }
|
|
||||||
|
|
||||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
|
||||||
}
|
|
@ -1,337 +0,0 @@
|
|||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<style media="all" type="text/css">
|
|
||||||
body {
|
|
||||||
font-family: Helvetica, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.3;
|
|
||||||
-ms-text-size-adjust: 100%;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: separate;
|
|
||||||
mso-table-lspace: 0pt;
|
|
||||||
mso-table-rspace: 0pt;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table td {
|
|
||||||
font-family: Helvetica, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #f4f5f6;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
background-color: #f4f5f6;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 0 auto !important;
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 0;
|
|
||||||
padding-top: 24px;
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #eaebed;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
clear: both;
|
|
||||||
padding-top: 24px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer td,
|
|
||||||
.footer p,
|
|
||||||
.footer span,
|
|
||||||
.footer a {
|
|
||||||
color: #9a9ea6;
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: Helvetica, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #0867ec;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-width: 100% !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn > tbody > tr > td {
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn table {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn table td {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn a {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: solid 2px #0867ec;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #0867ec;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
padding: 12px 24px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary table td {
|
|
||||||
background-color: #0867ec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary a {
|
|
||||||
background-color: #0867ec;
|
|
||||||
border-color: #0867ec;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-code
|
|
||||||
{
|
|
||||||
font-family: "Courier New", Courier, monospace;
|
|
||||||
font-size: 24px;
|
|
||||||
letter-spacing: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media all {
|
|
||||||
.btn-primary table td:hover {
|
|
||||||
background-color: #ec0867 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary a:hover {
|
|
||||||
background-color: #ec0867 !important;
|
|
||||||
border-color: #ec0867 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.last {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.first {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
|
||||||
color: #0867ec !important;
|
|
||||||
text-decoration: underline !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt0 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb0 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preheader {
|
|
||||||
color: transparent;
|
|
||||||
display: none;
|
|
||||||
height: 0;
|
|
||||||
max-height: 0;
|
|
||||||
max-width: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
mso-hide: all;
|
|
||||||
visibility: hidden;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.powered-by a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media only screen and (max-width: 640px) {
|
|
||||||
.main p,
|
|
||||||
.main td,
|
|
||||||
.main span {
|
|
||||||
font-size: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
padding: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 0 !important;
|
|
||||||
padding-top: 8px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
border-left-width: 0 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border-right-width: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn table {
|
|
||||||
max-width: 100% !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn a {
|
|
||||||
font-size: 16px !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media all {
|
|
||||||
.ExternalClass {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExternalClass,
|
|
||||||
.ExternalClass p,
|
|
||||||
.ExternalClass span,
|
|
||||||
.ExternalClass font,
|
|
||||||
.ExternalClass td,
|
|
||||||
.ExternalClass div {
|
|
||||||
line-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apple-link a {
|
|
||||||
color: inherit !important;
|
|
||||||
font-family: inherit !important;
|
|
||||||
font-size: inherit !important;
|
|
||||||
font-weight: inherit !important;
|
|
||||||
line-height: inherit !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#MessageViewBody a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
|
||||||
<tr>
|
|
||||||
<td> </td>
|
|
||||||
<td class="container">
|
|
||||||
<div class="content">
|
|
||||||
|
|
||||||
<!-- START CENTERED WHITE CONTAINER -->
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main">
|
|
||||||
<!-- START MAIN CONTENT AREA -->
|
|
||||||
@ChildContent
|
|
||||||
<!-- END MAIN CONTENT AREA -->
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- START FOOTER -->
|
|
||||||
<div class="footer">
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
<tr>
|
|
||||||
<td class="content-block">
|
|
||||||
<span class="apple-link">Solar Network</span>
|
|
||||||
<br> Solsynth LLC © @(DateTime.Now.Year)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="content-block powered-by">
|
|
||||||
Powered by <a href="https://github.com/solsynth/dysonnetwork">Dyson Network</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- END FOOTER -->
|
|
||||||
|
|
||||||
<!-- END CENTERED WHITE CONTAINER --></div>
|
|
||||||
</td>
|
|
||||||
<td> </td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
@using DysonNetwork.Pass.Localization
|
|
||||||
@using Microsoft.Extensions.Localization
|
|
||||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
|
||||||
|
|
||||||
<EmailLayout>
|
|
||||||
<tr>
|
|
||||||
<td class="wrapper">
|
|
||||||
<p class="font-bold">@(Localizer["LandingHeader1"])</p>
|
|
||||||
<p>@(Localizer["LandingPara1"]) @@@Name,</p>
|
|
||||||
<p>@(Localizer["LandingPara2"])</p>
|
|
||||||
<p>@(Localizer["LandingPara3"])</p>
|
|
||||||
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="left">
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="@Link" target="_blank">
|
|
||||||
@(Localizer["LandingButton1"])
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p>@(Localizer["LandingPara4"])</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</EmailLayout>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public required string Name { get; set; }
|
|
||||||
[Parameter] public required string Link { get; set; }
|
|
||||||
|
|
||||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
@using DysonNetwork.Pass.Localization
|
|
||||||
@using Microsoft.Extensions.Localization
|
|
||||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
|
||||||
|
|
||||||
<EmailLayout>
|
|
||||||
<tr>
|
|
||||||
<td class="wrapper">
|
|
||||||
<p class="font-bold">@(Localizer["PasswordResetHeader"])</p>
|
|
||||||
<p>@(Localizer["PasswordResetPara1"]) @@@Name,</p>
|
|
||||||
<p>@(Localizer["PasswordResetPara2"])</p>
|
|
||||||
<p>@(Localizer["PasswordResetPara3"])</p>
|
|
||||||
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="left">
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="@Link" target="_blank">
|
|
||||||
@(Localizer["PasswordResetButton"])
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p>@(Localizer["PasswordResetPara4"])</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</EmailLayout>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public required string Name { get; set; }
|
|
||||||
[Parameter] public required string Link { get; set; }
|
|
||||||
|
|
||||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
|
||||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
@using DysonNetwork.Pass.Localization
|
|
||||||
@using Microsoft.Extensions.Localization
|
|
||||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
|
||||||
|
|
||||||
<EmailLayout>
|
|
||||||
<tr>
|
|
||||||
<td class="wrapper">
|
|
||||||
<p class="font-bold">@(Localizer["VerificationHeader1"])</p>
|
|
||||||
<p>@(Localizer["VerificationPara1"]) @@@Name,</p>
|
|
||||||
<p>@(Localizer["VerificationPara2"])</p>
|
|
||||||
<p>@(Localizer["VerificationPara3"])</p>
|
|
||||||
|
|
||||||
<p class="verification-code">@Code</p>
|
|
||||||
|
|
||||||
<p>@(Localizer["VerificationPara4"])</p>
|
|
||||||
<p>@(Localizer["VerificationPara5"])</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</EmailLayout>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public required string Name { get; set; }
|
|
||||||
[Parameter] public required string Code { get; set; }
|
|
||||||
|
|
||||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
|
||||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
using DysonNetwork.Pass;
|
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using DysonNetwork.Pass.Auth;
|
|
||||||
using DysonNetwork.Pass.Startup;
|
|
||||||
using DysonNetwork.Shared.Startup;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.ConfigureAppKestrel();
|
|
||||||
builder.Services.AddAppSwagger();
|
|
||||||
builder.Services.AddAppAuthentication();
|
|
||||||
builder.Services.AddAppRateLimiting();
|
|
||||||
builder.Services.AddAppBusinessServices(builder.Configuration);
|
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
builder.Services.AddMagicOnion();
|
|
||||||
builder.Services.AddDbContext<AppDatabase>(options =>
|
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("App")));
|
|
||||||
|
|
||||||
builder.Services.AddScoped<AccountService>();
|
|
||||||
builder.Services.AddScoped<AuthService>();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.ConfigureAppMiddleware(builder.Configuration);
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
app.MapMagicOnionService();
|
|
||||||
|
|
||||||
// Run database migrations
|
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
|
||||||
await db.Database.MigrateAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Run();
|
|
@ -1,186 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.RateLimiting;
|
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using DysonNetwork.Pass.Auth;
|
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
|
||||||
using DysonNetwork.Pass.Auth.OpenId;
|
|
||||||
using DysonNetwork.Pass.Localization;
|
|
||||||
using DysonNetwork.Pass.Permission;
|
|
||||||
using DysonNetwork.Shared.Cache;
|
|
||||||
using DysonNetwork.Shared.Services;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using NodaTime;
|
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Startup;
|
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
|
||||||
|
|
||||||
services.AddDbContext<AppDatabase>();
|
|
||||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
|
||||||
{
|
|
||||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
|
||||||
return ConnectionMultiplexer.Connect(connection);
|
|
||||||
});
|
|
||||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
|
||||||
services.AddHttpContextAccessor();
|
|
||||||
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
|
||||||
|
|
||||||
services.AddHttpClient();
|
|
||||||
|
|
||||||
// Register MagicOnion services
|
|
||||||
services.AddScoped<IAccountService, AccountService>();
|
|
||||||
services.AddScoped<INotificationService, NotificationService>();
|
|
||||||
services.AddScoped<IRelationshipService, RelationshipService>();
|
|
||||||
services.AddScoped<IActionLogService, ActionLogService>();
|
|
||||||
services.AddScoped<IAccountUsernameService, AccountUsernameService>();
|
|
||||||
services.AddScoped<IMagicSpellService, MagicSpellService>();
|
|
||||||
services.AddScoped<IAccountEventService, AccountEventService>();
|
|
||||||
|
|
||||||
// Register OIDC services
|
|
||||||
services.AddScoped<OidcService, GoogleOidcService>();
|
|
||||||
services.AddScoped<OidcService, AppleOidcService>();
|
|
||||||
services.AddScoped<OidcService, GitHubOidcService>();
|
|
||||||
services.AddScoped<OidcService, MicrosoftOidcService>();
|
|
||||||
services.AddScoped<OidcService, DiscordOidcService>();
|
|
||||||
services.AddScoped<OidcService, AfdianOidcService>();
|
|
||||||
services.AddScoped<GoogleOidcService>();
|
|
||||||
services.AddScoped<AppleOidcService>();
|
|
||||||
services.AddScoped<GitHubOidcService>();
|
|
||||||
services.AddScoped<MicrosoftOidcService>();
|
|
||||||
services.AddScoped<DiscordOidcService>();
|
|
||||||
services.AddScoped<AfdianOidcService>();
|
|
||||||
|
|
||||||
services.AddControllers().AddJsonOptions(options =>
|
|
||||||
{
|
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
|
||||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
|
||||||
|
|
||||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
|
||||||
}).AddDataAnnotationsLocalization(options =>
|
|
||||||
{
|
|
||||||
options.DataAnnotationLocalizerProvider = (type, factory) =>
|
|
||||||
factory.Create(typeof(SharedResource));
|
|
||||||
});
|
|
||||||
services.AddRazorPages();
|
|
||||||
|
|
||||||
services.Configure<RequestLocalizationOptions>(options =>
|
|
||||||
{
|
|
||||||
var supportedCultures = new[]
|
|
||||||
{
|
|
||||||
new CultureInfo("en-US"),
|
|
||||||
new CultureInfo("zh-Hans"),
|
|
||||||
};
|
|
||||||
|
|
||||||
options.SupportedCultures = supportedCultures;
|
|
||||||
options.SupportedUICultures = supportedCultures;
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppRateLimiting(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
|
||||||
{
|
|
||||||
opts.Window = TimeSpan.FromMinutes(1);
|
|
||||||
opts.PermitLimit = 120;
|
|
||||||
opts.QueueLimit = 2;
|
|
||||||
opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddCors();
|
|
||||||
services.AddAuthorization();
|
|
||||||
services.AddAuthentication(options =>
|
|
||||||
{
|
|
||||||
options.DefaultAuthenticateScheme = AuthConstants.SchemeName;
|
|
||||||
options.DefaultChallengeScheme = AuthConstants.SchemeName;
|
|
||||||
})
|
|
||||||
.AddScheme<DysonTokenAuthOptions, DysonTokenAuthHandler>(AuthConstants.SchemeName, _ => { });
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppSwagger(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddEndpointsApiExplorer();
|
|
||||||
services.AddSwaggerGen(options =>
|
|
||||||
{
|
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
|
||||||
{
|
|
||||||
Version = "v1",
|
|
||||||
Title = "Solar Network API",
|
|
||||||
Description = "An open-source social network",
|
|
||||||
TermsOfService = new Uri("https://solsynth.dev/terms"),
|
|
||||||
License = new OpenApiLicense
|
|
||||||
{
|
|
||||||
Name = "APGLv3",
|
|
||||||
Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
In = ParameterLocation.Header,
|
|
||||||
Description = "Please enter a valid token",
|
|
||||||
Name = "Authorization",
|
|
||||||
Type = SecuritySchemeType.Http,
|
|
||||||
BearerFormat = "JWT",
|
|
||||||
Scheme = "Bearer"
|
|
||||||
});
|
|
||||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
||||||
{
|
|
||||||
{
|
|
||||||
new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Reference = new OpenApiReference
|
|
||||||
{
|
|
||||||
Type = ReferenceType.SecurityScheme,
|
|
||||||
Id = "Bearer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
services.AddOpenApi();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services,
|
|
||||||
IConfiguration configuration)
|
|
||||||
{
|
|
||||||
services.AddScoped<CompactTokenService>();
|
|
||||||
services.AddScoped<PermissionService>();
|
|
||||||
services.AddScoped<ActionLogService>();
|
|
||||||
services.AddScoped<AccountService>();
|
|
||||||
services.AddScoped<AccountEventService>();
|
|
||||||
services.AddScoped<ActionLogService>();
|
|
||||||
services.AddScoped<RelationshipService>();
|
|
||||||
services.AddScoped<MagicSpellService>();
|
|
||||||
services.AddScoped<NotificationService>();
|
|
||||||
services.AddScoped<AuthService>();
|
|
||||||
services.AddScoped<AccountUsernameService>();
|
|
||||||
|
|
||||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
|
||||||
services.AddScoped<OidcProviderService>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"App": "Host=localhost;Port=5432;Database=dyson_network_pass;Username=postgres;Password=password"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
|
|
||||||
<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" />
|
|
||||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
|
||||||
<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>
|
|
@ -1,15 +0,0 @@
|
|||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
|
||||||
|
|
||||||
public interface IIdentifiedResource
|
|
||||||
{
|
|
||||||
public string ResourceIdentifier { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class ModelBase
|
|
||||||
{
|
|
||||||
public Instant CreatedAt { get; set; }
|
|
||||||
public Instant UpdatedAt { get; set; }
|
|
||||||
public Instant? DeletedAt { get; set; }
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public enum AbuseReportType
|
public enum AbuseReportType
|
||||||
{
|
{
|
||||||
@ -26,5 +26,5 @@ public class AbuseReport : ModelBase
|
|||||||
[MaxLength(8192)] public string? Resolution { get; set; }
|
[MaxLength(8192)] public string? Resolution { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Shared.Models.Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
}
|
}
|
@ -1,11 +1,14 @@
|
|||||||
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.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
[Index(nameof(Name), IsUnique = true)]
|
[Index(nameof(Name), IsUnique = true)]
|
||||||
public class Account : ModelBase
|
public class Account : ModelBase
|
||||||
@ -23,8 +26,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<Session> Sessions { get; set; } = new List<Session>();
|
[JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>();
|
||||||
[JsonIgnore] public ICollection<Challenge> Challenges { get; set; } = new List<Challenge>();
|
[JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>();
|
||||||
|
|
||||||
[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,12 +1,14 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Sphere.Permission;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Microsoft.AspNetCore.Http;
|
using NodaTime.Extensions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/accounts")]
|
[Route("/accounts")]
|
||||||
@ -18,9 +20,9 @@ public class AccountController(
|
|||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("{name}")]
|
[HttpGet("{name}")]
|
||||||
[ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<Shared.Models.Account?>> GetByName(string name)
|
public async Task<ActionResult<Account?>> GetByName(string name)
|
||||||
{
|
{
|
||||||
var account = await db.Accounts
|
var account = await db.Accounts
|
||||||
.Include(e => e.Badges)
|
.Include(e => e.Badges)
|
||||||
@ -71,9 +73,9 @@ public class AccountController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<Shared.Models.Account>> CreateAccount([FromBody] AccountCreateRequest request)
|
public async Task<ActionResult<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.");
|
||||||
|
|
||||||
@ -161,7 +163,7 @@ public class AccountController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("search")]
|
[HttpGet("search")]
|
||||||
public async Task<List<Shared.Models.Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
return [];
|
return [];
|
@ -1,15 +1,14 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
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.Account;
|
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@ -17,15 +16,16 @@ namespace DysonNetwork.Pass.Account;
|
|||||||
public class AccountCurrentController(
|
public class AccountCurrentController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
|
FileReferenceService fileRefService,
|
||||||
AccountEventService events,
|
AccountEventService events,
|
||||||
AuthService auth
|
AuthService auth
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<Shared.Models.Account>> GetCurrentIdentity()
|
public async Task<ActionResult<Account>> GetCurrentIdentity()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var account = await db.Accounts
|
var account = await db.Accounts
|
||||||
@ -44,9 +44,9 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch]
|
[HttpPatch]
|
||||||
public async Task<ActionResult<Shared.Models.Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
|
public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
|
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPatch("profile")]
|
[HttpPatch("profile")]
|
||||||
public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request)
|
public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var profile = await db.AccountProfiles
|
var profile = await db.AccountProfiles
|
||||||
@ -95,61 +95,61 @@ public class AccountCurrentController(
|
|||||||
if (request.Location is not null) profile.Location = request.Location;
|
if (request.Location is not null) profile.Location = request.Location;
|
||||||
if (request.TimeZone is not null) profile.TimeZone = request.TimeZone;
|
if (request.TimeZone is not null) profile.TimeZone = request.TimeZone;
|
||||||
|
|
||||||
// if (request.PictureId is not null)
|
if (request.PictureId is not null)
|
||||||
// {
|
{
|
||||||
// var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
|
var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
|
||||||
// if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
//
|
|
||||||
// var profileResourceId = $"profile:{profile.Id}";
|
var profileResourceId = $"profile:{profile.Id}";
|
||||||
//
|
|
||||||
// // Remove old references for the profile picture
|
// Remove old references for the profile picture
|
||||||
// if (profile.Picture is not null)
|
if (profile.Picture is not null)
|
||||||
// {
|
{
|
||||||
// var oldPictureRefs =
|
var oldPictureRefs =
|
||||||
// await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
|
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
|
||||||
// foreach (var oldRef in oldPictureRefs)
|
foreach (var oldRef in oldPictureRefs)
|
||||||
// {
|
{
|
||||||
// await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// profile.Picture = picture.ToReferenceObject();
|
profile.Picture = picture.ToReferenceObject();
|
||||||
//
|
|
||||||
// // Create new reference
|
// Create new reference
|
||||||
// await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
// picture.Id,
|
picture.Id,
|
||||||
// "profile.picture",
|
"profile.picture",
|
||||||
// profileResourceId
|
profileResourceId
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// if (request.BackgroundId is not null)
|
if (request.BackgroundId is not null)
|
||||||
// {
|
{
|
||||||
// var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
|
var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
|
||||||
// if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
//
|
|
||||||
// var profileResourceId = $"profile:{profile.Id}";
|
var profileResourceId = $"profile:{profile.Id}";
|
||||||
//
|
|
||||||
// // Remove old references for the profile background
|
// Remove old references for the profile background
|
||||||
// if (profile.Background is not null)
|
if (profile.Background is not null)
|
||||||
// {
|
{
|
||||||
// var oldBackgroundRefs =
|
var oldBackgroundRefs =
|
||||||
// await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
|
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
|
||||||
// foreach (var oldRef in oldBackgroundRefs)
|
foreach (var oldRef in oldBackgroundRefs)
|
||||||
// {
|
{
|
||||||
// await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// profile.Background = background.ToReferenceObject();
|
profile.Background = background.ToReferenceObject();
|
||||||
//
|
|
||||||
// // Create new reference
|
// Create new reference
|
||||||
// await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
// background.Id,
|
background.Id,
|
||||||
// "profile.background",
|
"profile.background",
|
||||||
// profileResourceId
|
profileResourceId
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
db.Update(profile);
|
db.Update(profile);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -162,7 +162,7 @@ public class AccountCurrentController(
|
|||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
public async Task<ActionResult> RequestDeleteAccount()
|
public async Task<ActionResult> RequestDeleteAccount()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -179,7 +179,7 @@ public class AccountCurrentController(
|
|||||||
[HttpGet("statuses")]
|
[HttpGet("statuses")]
|
||||||
public async Task<ActionResult<Status>> GetCurrentStatus()
|
public async Task<ActionResult<Status>> GetCurrentStatus()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var status = await events.GetStatus(currentUser.Id);
|
var status = await events.GetStatus(currentUser.Id);
|
||||||
return Ok(status);
|
return Ok(status);
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ public class AccountCurrentController(
|
|||||||
[RequiredPermission("global", "accounts.statuses.update")]
|
[RequiredPermission("global", "accounts.statuses.update")]
|
||||||
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var status = await db.AccountStatuses
|
||||||
@ -215,7 +215,7 @@ public class AccountCurrentController(
|
|||||||
[RequiredPermission("global", "accounts.statuses.create")]
|
[RequiredPermission("global", "accounts.statuses.create")]
|
||||||
public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request)
|
public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var status = new Status
|
var status = new Status
|
||||||
{
|
{
|
||||||
@ -233,7 +233,7 @@ public class AccountCurrentController(
|
|||||||
[HttpDelete("me/statuses")]
|
[HttpDelete("me/statuses")]
|
||||||
public async Task<ActionResult> DeleteStatus()
|
public async Task<ActionResult> DeleteStatus()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var status = await db.AccountStatuses
|
||||||
@ -250,7 +250,7 @@ public class AccountCurrentController(
|
|||||||
[HttpGet("check-in")]
|
[HttpGet("check-in")]
|
||||||
public async Task<ActionResult<CheckInResult>> GetCheckInResult()
|
public async Task<ActionResult<CheckInResult>> GetCheckInResult()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
@ -270,7 +270,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPost("check-in")]
|
[HttpPost("check-in")]
|
||||||
public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken)
|
public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
|
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
|
||||||
if (!isAvailable)
|
if (!isAvailable)
|
||||||
@ -297,7 +297,7 @@ public class AccountCurrentController(
|
|||||||
public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month,
|
public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month,
|
||||||
[FromQuery] int? year)
|
[FromQuery] int? year)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
||||||
month ??= currentDate.Month;
|
month ??= currentDate.Month;
|
||||||
@ -318,7 +318,7 @@ public class AccountCurrentController(
|
|||||||
[FromQuery] int offset = 0
|
[FromQuery] int offset = 0
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var query = db.ActionLogs
|
var query = db.ActionLogs
|
||||||
.Where(log => log.AccountId == currentUser.Id)
|
.Where(log => log.AccountId == currentUser.Id)
|
||||||
@ -338,7 +338,7 @@ public class AccountCurrentController(
|
|||||||
[HttpGet("factors")]
|
[HttpGet("factors")]
|
||||||
public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors()
|
public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var factors = await db.AccountAuthFactors
|
var factors = await db.AccountAuthFactors
|
||||||
.Include(f => f.Account)
|
.Include(f => f.Account)
|
||||||
@ -358,7 +358,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request)
|
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
||||||
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
||||||
|
|
||||||
@ -370,7 +370,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code)
|
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var factor = await db.AccountAuthFactors
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
@ -392,7 +392,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id)
|
public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var factor = await db.AccountAuthFactors
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
@ -414,7 +414,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id)
|
public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var factor = await db.AccountAuthFactors
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
@ -445,7 +445,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices()
|
public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
|
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
|
||||||
@ -480,7 +480,7 @@ public class AccountCurrentController(
|
|||||||
[FromQuery] int offset = 0
|
[FromQuery] int offset = 0
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
var query = db.AuthSessions
|
var query = db.AuthSessions
|
||||||
@ -505,7 +505,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Session>> DeleteSession(Guid id)
|
public async Task<ActionResult<Session>> DeleteSession(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -522,7 +522,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Session>> DeleteCurrentSession()
|
public async Task<ActionResult<Session>> DeleteCurrentSession()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -539,7 +539,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPatch("sessions/{id:guid}/label")]
|
[HttpPatch("sessions/{id:guid}/label")]
|
||||||
public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string label)
|
public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string label)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -555,7 +555,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPatch("sessions/current/label")]
|
[HttpPatch("sessions/current/label")]
|
||||||
public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string label)
|
public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string label)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -573,7 +573,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<AccountContact>>> GetContacts()
|
public async Task<ActionResult<List<AccountContact>>> GetContacts()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var contacts = await db.AccountContacts
|
var contacts = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id)
|
.Where(c => c.AccountId == currentUser.Id)
|
||||||
@ -592,7 +592,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request)
|
public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -609,7 +609,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> VerifyContact(Guid id)
|
public async Task<ActionResult<AccountContact>> VerifyContact(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var contact = await db.AccountContacts
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||||
@ -631,7 +631,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id)
|
public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var contact = await db.AccountContacts
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||||
@ -653,7 +653,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> DeleteContact(Guid id)
|
public async Task<ActionResult<AccountContact>> DeleteContact(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var contact = await db.AccountContacts
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||||
@ -676,9 +676,9 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<Badge>>> GetBadges()
|
public async Task<ActionResult<List<Badge>>> GetBadges()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var badges = await db.AccountBadges
|
var badges = await db.Badges
|
||||||
.Where(b => b.AccountId == currentUser.Id)
|
.Where(b => b.AccountId == currentUser.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Ok(badges);
|
return Ok(badges);
|
||||||
@ -688,7 +688,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Badge>> ActivateBadge(Guid id)
|
public async Task<ActionResult<Badge>> ActivateBadge(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
@ -1,19 +1,23 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Connection;
|
||||||
using DysonNetwork.Shared.Services;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using MagicOnion.Server;
|
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.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class AccountEventService(
|
public class AccountEventService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
|
WebSocketService ws,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
|
PaymentService payment,
|
||||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||||
) : ServiceBase<IAccountEventService>, IAccountEventService
|
)
|
||||||
{
|
{
|
||||||
private static readonly Random Random = new();
|
private static readonly Random Random = new();
|
||||||
private const string StatusCacheKey = "AccountStatus_";
|
private const string StatusCacheKey = "AccountStatus_";
|
||||||
@ -30,7 +34,7 @@ public class AccountEventService(
|
|||||||
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
||||||
if (cachedStatus is not null)
|
if (cachedStatus is not null)
|
||||||
{
|
{
|
||||||
cachedStatus!.IsOnline = !cachedStatus.IsInvisible /*&& ws.GetAccountIsConnected(userId)*/;
|
cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
||||||
return cachedStatus;
|
return cachedStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,17 +44,12 @@ public class AccountEventService(
|
|||||||
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
// var isOnline = ws.GetAccountIsConnected(userId);
|
var isOnline = ws.GetAccountIsConnected(userId);
|
||||||
var isOnline = false; // Placeholder
|
|
||||||
if (status is not null)
|
if (status is not null)
|
||||||
{
|
{
|
||||||
status.IsOnline = !status.IsInvisible && isOnline;
|
status.IsOnline = !status.IsInvisible && isOnline;
|
||||||
await cache.SetWithGroupsAsync(
|
await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"],
|
||||||
cacheKey,
|
TimeSpan.FromMinutes(5));
|
||||||
status,
|
|
||||||
[$"{AccountService.AccountCachePrefix}{status.AccountId}"],
|
|
||||||
TimeSpan.FromMinutes(5)
|
|
||||||
);
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +65,7 @@ public class AccountEventService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Status
|
return new Status
|
||||||
{
|
{
|
||||||
Attitude = StatusAttitude.Neutral,
|
Attitude = StatusAttitude.Neutral,
|
||||||
IsOnline = false,
|
IsOnline = false,
|
||||||
@ -84,16 +83,16 @@ public class AccountEventService(
|
|||||||
foreach (var userId in userIds)
|
foreach (var userId in userIds)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{StatusCacheKey}{userId}";
|
var cacheKey = $"{StatusCacheKey}{userId}";
|
||||||
// var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
||||||
// if (cachedStatus != null)
|
if (cachedStatus != null)
|
||||||
// {
|
{
|
||||||
// cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
||||||
// results[userId] = cachedStatus;
|
results[userId] = cachedStatus;
|
||||||
// }
|
}
|
||||||
// else
|
else
|
||||||
// {
|
{
|
||||||
cacheMissUserIds.Add(userId);
|
cacheMissUserIds.Add(userId);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheMissUserIds.Any())
|
if (cacheMissUserIds.Any())
|
||||||
@ -110,12 +109,11 @@ public class AccountEventService(
|
|||||||
|
|
||||||
foreach (var status in statusesFromDb)
|
foreach (var status in statusesFromDb)
|
||||||
{
|
{
|
||||||
// var isOnline = ws.GetAccountIsConnected(status.AccountId);
|
var isOnline = ws.GetAccountIsConnected(status.AccountId);
|
||||||
var isOnline = false; // Placeholder
|
|
||||||
status.IsOnline = !status.IsInvisible && isOnline;
|
status.IsOnline = !status.IsInvisible && isOnline;
|
||||||
results[status.AccountId] = status;
|
results[status.AccountId] = status;
|
||||||
var cacheKey = $"{StatusCacheKey}{status.AccountId}";
|
var cacheKey = $"{StatusCacheKey}{status.AccountId}";
|
||||||
// await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5));
|
await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5));
|
||||||
foundUserIds.Add(status.AccountId);
|
foundUserIds.Add(status.AccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,8 +122,7 @@ public class AccountEventService(
|
|||||||
{
|
{
|
||||||
foreach (var userId in usersWithoutStatus)
|
foreach (var userId in usersWithoutStatus)
|
||||||
{
|
{
|
||||||
// var isOnline = ws.GetAccountIsConnected(userId);
|
var isOnline = ws.GetAccountIsConnected(userId);
|
||||||
var isOnline = false; // Placeholder
|
|
||||||
var defaultStatus = new Status
|
var defaultStatus = new Status
|
||||||
{
|
{
|
||||||
Attitude = StatusAttitude.Neutral,
|
Attitude = StatusAttitude.Neutral,
|
||||||
@ -142,7 +139,7 @@ public class AccountEventService(
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Status> CreateStatus(Shared.Models.Account user, Status status)
|
public async Task<Status> CreateStatus(Account user, Status status)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.AccountStatuses
|
await db.AccountStatuses
|
||||||
@ -155,7 +152,7 @@ public class AccountEventService(
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ClearStatus(Shared.Models.Account user, Status status)
|
public async Task ClearStatus(Account user, Status status)
|
||||||
{
|
{
|
||||||
status.ClearedAt = SystemClock.Instance.GetCurrentInstant();
|
status.ClearedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
db.Update(status);
|
db.Update(status);
|
||||||
@ -167,19 +164,19 @@ 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(Shared.Models.Account user)
|
public async Task<bool> CheckInDailyDoAskCaptcha(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);
|
||||||
// if (needsCaptcha is not null)
|
if (needsCaptcha is not null)
|
||||||
// return needsCaptcha!.Value;
|
return needsCaptcha!.Value;
|
||||||
|
|
||||||
var result = Random.Next(100) < CaptchaProbabilityPercent;
|
var result = Random.Next(100) < CaptchaProbabilityPercent;
|
||||||
// await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24));
|
await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckInDailyIsAvailable(Shared.Models.Account user)
|
public async Task<bool> CheckInDailyIsAvailable(Account user)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var lastCheckIn = await db.AccountCheckInResults
|
var lastCheckIn = await db.AccountCheckInResults
|
||||||
@ -196,9 +193,9 @@ public class AccountEventService(
|
|||||||
return lastDate < currentDate;
|
return lastDate < currentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string CheckInLockKey = "checkin-lock:";
|
public const string CheckInLockKey = "CheckInLock_";
|
||||||
|
|
||||||
public async Task<CheckInResult> CheckInDaily(Shared.Models.Account user)
|
public async Task<CheckInResult> CheckInDaily(Account user)
|
||||||
{
|
{
|
||||||
var lockKey = $"{CheckInLockKey}{user.Id}";
|
var lockKey = $"{CheckInLockKey}{user.Id}";
|
||||||
|
|
||||||
@ -215,8 +212,7 @@ public class AccountEventService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now try to acquire the lock properly
|
// Now try to acquire the lock properly
|
||||||
await using var lockObj =
|
await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
|
||||||
await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
|
|
||||||
if (lockObj is null) throw new InvalidOperationException("Check-in was in progress.");
|
if (lockObj is null) throw new InvalidOperationException("Check-in was in progress.");
|
||||||
|
|
||||||
var cultureInfo = new CultureInfo(user.Language, false);
|
var cultureInfo = new CultureInfo(user.Language, false);
|
||||||
@ -259,14 +255,13 @@ public class AccountEventService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (result.RewardPoints.HasValue)
|
if (result.RewardPoints.HasValue)
|
||||||
// await payment.CreateTransactionWithAccountAsync(
|
await payment.CreateTransactionWithAccountAsync(
|
||||||
// null,
|
null,
|
||||||
// user.Id,
|
user.Id,
|
||||||
// WalletCurrency.SourcePoint,
|
WalletCurrency.SourcePoint,
|
||||||
// result.RewardPoints.Value,
|
result.RewardPoints.Value,
|
||||||
// $"Check-in reward on {now:yyyy/MM/dd}"
|
$"Check-in reward on {now:yyyy/MM/dd}"
|
||||||
// );
|
);
|
||||||
Console.WriteLine($"Simulating transaction for {result.RewardPoints.Value} points");
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -279,54 +274,13 @@ public class AccountEventService(
|
|||||||
s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience)
|
s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience)
|
||||||
);
|
);
|
||||||
db.AccountCheckInResults.Add(result);
|
db.AccountCheckInResults.Add(result);
|
||||||
await db.SaveChangesAsync(); // Remember to save changes to the database
|
await db.SaveChangesAsync(); // Don't forget to save changes to the database
|
||||||
|
|
||||||
// The lock will be automatically released by the await using statement
|
// The lock will be automatically released by the await using statement
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetCheckInStreak(Shared.Models.Account user)
|
public async Task<List<DailyEventResponse>> GetEventCalendar(Account user, int month, int year = 0,
|
||||||
{
|
|
||||||
var today = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
|
||||||
var yesterdayEnd = today.PlusDays(-1).AtMidnight().InUtc().ToInstant();
|
|
||||||
var yesterdayStart = today.PlusDays(-1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
|
||||||
var tomorrowEnd = today.PlusDays(1).AtMidnight().InUtc().ToInstant();
|
|
||||||
var tomorrowStart = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
|
||||||
|
|
||||||
var yesterdayResult = await db.AccountCheckInResults
|
|
||||||
.Where(x => x.AccountId == user.Id)
|
|
||||||
.Where(x => x.CreatedAt >= yesterdayStart)
|
|
||||||
.Where(x => x.CreatedAt < yesterdayEnd)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
var tomorrowResult = await db.AccountCheckInResults
|
|
||||||
.Where(x => x.AccountId == user.Id)
|
|
||||||
.Where(x => x.CreatedAt >= tomorrowStart)
|
|
||||||
.Where(x => x.CreatedAt < tomorrowEnd)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (yesterdayResult is null && tomorrowResult is null)
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
var results = await db.AccountCheckInResults
|
|
||||||
.Where(x => x.AccountId == user.Id)
|
|
||||||
.OrderByDescending(x => x.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var streak = 0;
|
|
||||||
var day = today;
|
|
||||||
while (results.Any(x =>
|
|
||||||
x.CreatedAt >= day.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant() &&
|
|
||||||
x.CreatedAt < day.AtMidnight().InUtc().ToInstant()))
|
|
||||||
{
|
|
||||||
streak++;
|
|
||||||
day = day.PlusDays(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return streak;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<DailyEventResponse>> GetEventCalendar(Shared.Models.Account user, int month, int year = 0,
|
|
||||||
bool replaceInvisible = false)
|
bool replaceInvisible = false)
|
||||||
{
|
{
|
||||||
if (year == 0)
|
if (year == 0)
|
@ -1,31 +1,32 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Pass.Auth.OpenId;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Email;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Shared.Services;
|
using DysonNetwork.Sphere.Localization;
|
||||||
|
using DysonNetwork.Sphere.Permission;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
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 Microsoft.Extensions.Logging;
|
|
||||||
using EFCore.BulkExtensions;
|
|
||||||
using MagicOnion.Server;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class AccountService(
|
public class AccountService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
// MagicSpellService spells,
|
MagicSpellService spells,
|
||||||
// AccountUsernameService uname,
|
AccountUsernameService uname,
|
||||||
// NotificationService nty,
|
NotificationService nty,
|
||||||
// EmailService mailer,
|
EmailService mailer,
|
||||||
// IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
ILogger<AccountService> logger
|
ILogger<AccountService> logger
|
||||||
) : ServiceBase<IAccountService>, IAccountService
|
)
|
||||||
{
|
{
|
||||||
public static void SetCultureInfo(Shared.Models.Account account)
|
public static void SetCultureInfo(Account account)
|
||||||
{
|
{
|
||||||
SetCultureInfo(account.Language);
|
SetCultureInfo(account.Language);
|
||||||
}
|
}
|
||||||
@ -39,12 +40,12 @@ public class AccountService(
|
|||||||
|
|
||||||
public const string AccountCachePrefix = "account:";
|
public const string AccountCachePrefix = "account:";
|
||||||
|
|
||||||
public async Task PurgeAccountCache(Shared.Models.Account account)
|
public async Task PurgeAccountCache(Account account)
|
||||||
{
|
{
|
||||||
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
|
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Shared.Models.Account?> LookupAccount(string probe)
|
public async Task<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;
|
||||||
@ -56,7 +57,7 @@ public class AccountService(
|
|||||||
return contact?.Account;
|
return contact?.Account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Shared.Models.Account?> LookupAccountByConnection(string identifier, string provider)
|
public async Task<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)
|
||||||
@ -73,7 +74,7 @@ public class AccountService(
|
|||||||
return profile?.Level;
|
return profile?.Level;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Shared.Models.Account> CreateAccount(
|
public async Task<Account> CreateAccount(
|
||||||
string name,
|
string name,
|
||||||
string nick,
|
string nick,
|
||||||
string email,
|
string email,
|
||||||
@ -90,7 +91,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 Shared.Models.Account
|
var account = new Account
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Nick = nick,
|
Nick = nick,
|
||||||
@ -134,15 +135,15 @@ public class AccountService(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// var spell = await spells.CreateMagicSpell(
|
var spell = await spells.CreateMagicSpell(
|
||||||
// account,
|
account,
|
||||||
// MagicSpellType.AccountActivation,
|
MagicSpellType.AccountActivation,
|
||||||
// new Dictionary<string, object>
|
new Dictionary<string, object>
|
||||||
// {
|
{
|
||||||
// { "contact_method", account.Contacts.First().Content }
|
{ "contact_method", account.Contacts.First().Content }
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
// await spells.NotifyMagicSpell(spell, true);
|
await spells.NotifyMagicSpell(spell, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Accounts.Add(account);
|
db.Accounts.Add(account);
|
||||||
@ -158,7 +159,7 @@ public class AccountService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Shared.Models.Account> CreateAccount(OidcUserInfo userInfo)
|
public async Task<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");
|
||||||
@ -168,8 +169,7 @@ public class AccountService(
|
|||||||
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
|
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
|
||||||
|
|
||||||
// Generate username from email
|
// Generate username from email
|
||||||
// var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
|
var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
|
||||||
var username = userInfo.Email.Split('@')[0]; // Placeholder
|
|
||||||
|
|
||||||
return await CreateAccount(
|
return await CreateAccount(
|
||||||
username,
|
username,
|
||||||
@ -182,31 +182,31 @@ public class AccountService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RequestAccountDeletion(Shared.Models.Account account)
|
public async Task RequestAccountDeletion(Account account)
|
||||||
{
|
{
|
||||||
// var spell = await spells.CreateMagicSpell(
|
var spell = await spells.CreateMagicSpell(
|
||||||
// account,
|
account,
|
||||||
// MagicSpellType.AccountRemoval,
|
MagicSpellType.AccountRemoval,
|
||||||
// new Dictionary<string, object>(),
|
new Dictionary<string, object>(),
|
||||||
// SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
||||||
// preventRepeat: true
|
preventRepeat: true
|
||||||
// );
|
);
|
||||||
// await spells.NotifyMagicSpell(spell);
|
await spells.NotifyMagicSpell(spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RequestPasswordReset(Shared.Models.Account account)
|
public async Task RequestPasswordReset(Account account)
|
||||||
{
|
{
|
||||||
// var spell = await spells.CreateMagicSpell(
|
var spell = await spells.CreateMagicSpell(
|
||||||
// account,
|
account,
|
||||||
// MagicSpellType.AuthPasswordReset,
|
MagicSpellType.AuthPasswordReset,
|
||||||
// new Dictionary<string, object>(),
|
new Dictionary<string, object>(),
|
||||||
// SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
||||||
// preventRepeat: true
|
preventRepeat: true
|
||||||
// );
|
);
|
||||||
// await spells.NotifyMagicSpell(spell);
|
await spells.NotifyMagicSpell(spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type)
|
public async Task<bool> CheckAuthFactorExists(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 +214,7 @@ public class AccountService(
|
|||||||
return isExists;
|
return isExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AccountAuthFactor?> CreateAuthFactor(Shared.Models.Account account, AccountAuthFactorType type, string? secret)
|
public async Task<AccountAuthFactor?> CreateAuthFactor(Account account, AccountAuthFactorType type, string? secret)
|
||||||
{
|
{
|
||||||
AccountAuthFactor? factor = null;
|
AccountAuthFactor? factor = null;
|
||||||
switch (type)
|
switch (type)
|
||||||
@ -329,7 +329,7 @@ public class AccountService(
|
|||||||
{
|
{
|
||||||
var count = await db.AccountAuthFactors
|
var count = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == factor.AccountId)
|
.Where(f => f.AccountId == factor.AccountId)
|
||||||
// .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null))
|
.If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null))
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
if (count <= 1)
|
if (count <= 1)
|
||||||
throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor.");
|
throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor.");
|
||||||
@ -345,7 +345,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(Shared.Models.Account account, AccountAuthFactor factor, string? hint = null)
|
public async Task SendFactorCode(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");
|
||||||
|
|
||||||
@ -355,14 +355,14 @@ public class AccountService(
|
|||||||
if (await _GetFactorCode(factor) is not null)
|
if (await _GetFactorCode(factor) is not null)
|
||||||
throw new InvalidOperationException("A factor code has been sent and in active duration.");
|
throw new InvalidOperationException("A factor code has been sent and in active duration.");
|
||||||
|
|
||||||
// await nty.SendNotification(
|
await nty.SendNotification(
|
||||||
// account,
|
account,
|
||||||
// "auth.verification",
|
"auth.verification",
|
||||||
// localizer["AuthCodeTitle"],
|
localizer["AuthCodeTitle"],
|
||||||
// null,
|
null,
|
||||||
// localizer["AuthCodeBody", code],
|
localizer["AuthCodeBody", code],
|
||||||
// save: true
|
save: true
|
||||||
// );
|
);
|
||||||
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5));
|
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5));
|
||||||
break;
|
break;
|
||||||
case AccountAuthFactorType.EmailCode:
|
case AccountAuthFactorType.EmailCode:
|
||||||
@ -397,16 +397,16 @@ public class AccountService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
||||||
// account.Nick,
|
account.Nick,
|
||||||
// contact.Content,
|
contact.Content,
|
||||||
// localizer["VerificationEmail"],
|
localizer["VerificationEmail"],
|
||||||
// new VerificationEmailModel
|
new VerificationEmailModel
|
||||||
// {
|
{
|
||||||
// Name = account.Name,
|
Name = account.Name,
|
||||||
// Code = code
|
Code = code
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
|
|
||||||
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
||||||
break;
|
break;
|
||||||
@ -454,7 +454,7 @@ public class AccountService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Session> UpdateSessionLabel(Shared.Models.Account account, Guid sessionId, string label)
|
public async Task<Session> UpdateSessionLabel(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 +477,7 @@ public class AccountService(
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteSession(Shared.Models.Account account, Guid sessionId)
|
public async Task DeleteSession(Account account, Guid sessionId)
|
||||||
{
|
{
|
||||||
var session = await db.AuthSessions
|
var session = await db.AuthSessions
|
||||||
.Include(s => s.Challenge)
|
.Include(s => s.Challenge)
|
||||||
@ -491,7 +491,7 @@ public class AccountService(
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (session.Challenge.DeviceId is not null)
|
if (session.Challenge.DeviceId is not null)
|
||||||
// await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
|
await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
|
||||||
|
|
||||||
// The current session should be included in the sessions' list
|
// The current session should be included in the sessions' list
|
||||||
await db.AuthSessions
|
await db.AuthSessions
|
||||||
@ -503,7 +503,7 @@ public class AccountService(
|
|||||||
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
|
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AccountContact> CreateContactMethod(Shared.Models.Account account, AccountContactType type, string content)
|
public async Task<AccountContact> CreateContactMethod(Account account, AccountContactType type, string content)
|
||||||
{
|
{
|
||||||
var contact = new AccountContact
|
var contact = new AccountContact
|
||||||
{
|
{
|
||||||
@ -518,19 +518,19 @@ public class AccountService(
|
|||||||
return contact;
|
return contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task VerifyContactMethod(Shared.Models.Account account, AccountContact contact)
|
public async Task VerifyContactMethod(Account account, AccountContact contact)
|
||||||
{
|
{
|
||||||
// var spell = await spells.CreateMagicSpell(
|
var spell = await spells.CreateMagicSpell(
|
||||||
// account,
|
account,
|
||||||
// MagicSpellType.ContactVerification,
|
MagicSpellType.ContactVerification,
|
||||||
// new Dictionary<string, object> { { "contact_method", contact.Content } },
|
new Dictionary<string, object> { { "contact_method", contact.Content } },
|
||||||
// expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
||||||
// preventRepeat: true
|
preventRepeat: true
|
||||||
// );
|
);
|
||||||
// await spells.NotifyMagicSpell(spell);
|
await spells.NotifyMagicSpell(spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact)
|
public async Task<AccountContact> SetContactMethodPrimary(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 +559,7 @@ public class AccountService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteContactMethod(Shared.Models.Account account, AccountContact contact)
|
public async Task DeleteContactMethod(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,10 +574,10 @@ 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(Shared.Models.Account account, Badge badge)
|
public async Task<Badge> GrantBadge(Account account, Badge badge)
|
||||||
{
|
{
|
||||||
badge.AccountId = account.Id;
|
badge.AccountId = account.Id;
|
||||||
db.AccountBadges.Add(badge);
|
db.Badges.Add(badge);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
@ -586,9 +586,9 @@ 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(Shared.Models.Account account, Guid badgeId)
|
public async Task RevokeBadge(Account account, Guid badgeId)
|
||||||
{
|
{
|
||||||
var badge = await db.AccountBadges
|
var badge = await db.Badges
|
||||||
.Where(b => b.AccountId == account.Id && b.Id == badgeId)
|
.Where(b => b.AccountId == account.Id && b.Id == badgeId)
|
||||||
.OrderByDescending(b => b.CreatedAt)
|
.OrderByDescending(b => b.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
@ -604,19 +604,19 @@ public class AccountService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ActiveBadge(Shared.Models.Account account, Guid badgeId)
|
public async Task ActiveBadge(Account account, Guid badgeId)
|
||||||
{
|
{
|
||||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var badge = await db.AccountBadges
|
var badge = await db.Badges
|
||||||
.Where(b => b.AccountId == account.Id && b.Id == badgeId)
|
.Where(b => b.AccountId == account.Id && b.Id == badgeId)
|
||||||
.OrderByDescending(b => b.CreatedAt)
|
.OrderByDescending(b => b.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (badge is null) throw new InvalidOperationException("Badge was not found.");
|
if (badge is null) throw new InvalidOperationException("Badge was not found.");
|
||||||
|
|
||||||
await db.AccountBadges
|
await db.Badges
|
||||||
.Where(b => b.AccountId == account.Id && b.Id != badgeId)
|
.Where(b => b.AccountId == account.Id && b.Id != badgeId)
|
||||||
.ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null));
|
.ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null));
|
||||||
|
|
@ -1,14 +1,12 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DysonNetwork.Shared.Services;
|
|
||||||
using MagicOnion.Server;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for handling username generation and validation
|
/// Service for handling username generation and validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AccountUsernameService(AppDatabase db) : ServiceBase<IAccountUsernameService>, IAccountUsernameService
|
public class AccountUsernameService(AppDatabase db)
|
||||||
{
|
{
|
||||||
private readonly Random _random = new();
|
private readonly Random _random = new();
|
||||||
|
|
@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Point = NetTopologySuite.Geometries.Point;
|
using Point = NetTopologySuite.Geometries.Point;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public abstract class ActionLogType
|
public abstract class ActionLogType
|
||||||
{
|
{
|
||||||
@ -53,6 +53,6 @@ public class ActionLog : ModelBase
|
|||||||
public Point? Location { get; set; }
|
public Point? Location { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Shared.Models.Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
public Guid? SessionId { get; set; }
|
public Guid? SessionId { get; set; }
|
||||||
}
|
}
|
@ -1,24 +1,12 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
using Quartz;
|
||||||
using DysonNetwork.Shared.Services;
|
using DysonNetwork.Sphere.Connection;
|
||||||
using MagicOnion.Server;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Http;
|
using DysonNetwork.Sphere.Storage.Handlers;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class ActionLogService : ServiceBase<IActionLogService>, IActionLogService
|
public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
|
||||||
{
|
{
|
||||||
// private readonly GeoIpService _geo;
|
|
||||||
// private readonly FlushBufferService _fbs;
|
|
||||||
|
|
||||||
public ActionLogService(
|
|
||||||
// GeoIpService geo,
|
|
||||||
// FlushBufferService fbs
|
|
||||||
)
|
|
||||||
{
|
|
||||||
// _geo = geo;
|
|
||||||
// _fbs = fbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
|
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
|
||||||
{
|
{
|
||||||
var log = new ActionLog
|
var log = new ActionLog
|
||||||
@ -28,11 +16,11 @@ public class ActionLogService : ServiceBase<IActionLogService>, IActionLogServic
|
|||||||
Meta = meta,
|
Meta = meta,
|
||||||
};
|
};
|
||||||
|
|
||||||
// fbs.Enqueue(log);
|
fbs.Enqueue(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
|
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
|
||||||
Shared.Models.Account? account = null)
|
Account? account = null)
|
||||||
{
|
{
|
||||||
var log = new ActionLog
|
var log = new ActionLog
|
||||||
{
|
{
|
||||||
@ -40,19 +28,19 @@ public class ActionLogService : ServiceBase<IActionLogService>, IActionLogServic
|
|||||||
Meta = meta,
|
Meta = meta,
|
||||||
UserAgent = request.Headers.UserAgent,
|
UserAgent = request.Headers.UserAgent,
|
||||||
IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||||
// Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
|
Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
|
if (request.HttpContext.Items["CurrentUser"] is 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;
|
||||||
else
|
else
|
||||||
throw new ArgumentException("No user context was found");
|
throw new ArgumentException("No user context was found");
|
||||||
|
|
||||||
if (request.HttpContext.Items["CurrentSession"] is Session currentSession)
|
if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession)
|
||||||
log.SessionId = currentSession.Id;
|
log.SessionId = currentSession.Id;
|
||||||
|
|
||||||
// fbs.Enqueue(log);
|
fbs.Enqueue(log);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
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 Shared.Models.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||||
|
|
||||||
public BadgeReferenceObject ToReference()
|
public BadgeReferenceObject ToReference()
|
||||||
{
|
{
|
@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
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 Shared.Models.Account Account { get; set; } = null!;
|
public 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 Shared.Models.Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FortuneTip
|
public class FortuneTip
|
@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
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 Shared.Models.Account? Account { get; set; }
|
public Account? Account { get; set; }
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/spells")]
|
[Route("/spells")]
|
@ -1,25 +1,27 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Email;
|
||||||
using DysonNetwork.Shared.Services;
|
using DysonNetwork.Sphere.Pages.Emails;
|
||||||
using MagicOnion.Server;
|
using DysonNetwork.Sphere.Permission;
|
||||||
|
using DysonNetwork.Sphere.Resources.Localization;
|
||||||
|
using DysonNetwork.Sphere.Resources.Pages.Emails;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class MagicSpellService(
|
public class MagicSpellService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
|
EmailService email,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<MagicSpellService> logger
|
ILogger<MagicSpellService> logger,
|
||||||
) : ServiceBase<IMagicSpellService>, IMagicSpellService
|
IStringLocalizer<Localization.EmailResource> localizer
|
||||||
|
)
|
||||||
{
|
{
|
||||||
public async Task<MagicSpell> CreateMagicSpell(
|
public async Task<MagicSpell> CreateMagicSpell(
|
||||||
Shared.Models.Account account,
|
Account account,
|
||||||
MagicSpellType type,
|
MagicSpellType type,
|
||||||
Dictionary<string, object> meta,
|
Dictionary<string, object> meta,
|
||||||
Instant? expiredAt = null,
|
Instant? expiredAt = null,
|
||||||
@ -59,17 +61,6 @@ public class MagicSpellService(
|
|||||||
return spell;
|
return spell;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MagicSpell?> GetMagicSpellAsync(string token)
|
|
||||||
{
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
var spell = await db.MagicSpells
|
|
||||||
.Where(s => s.Spell == token)
|
|
||||||
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
return spell;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false)
|
public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false)
|
||||||
{
|
{
|
||||||
var contact = await db.AccountContacts
|
var contact = await db.AccountContacts
|
||||||
@ -96,54 +87,54 @@ public class MagicSpellService(
|
|||||||
switch (spell.Type)
|
switch (spell.Type)
|
||||||
{
|
{
|
||||||
case MagicSpellType.AccountActivation:
|
case MagicSpellType.AccountActivation:
|
||||||
// await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
|
await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
|
||||||
// contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
// contact.Content,
|
contact.Content,
|
||||||
// localizer["EmailLandingTitle"],
|
localizer["EmailLandingTitle"],
|
||||||
// new LandingEmailModel
|
new LandingEmailModel
|
||||||
// {
|
{
|
||||||
// Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
// Link = link
|
Link = link
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
break;
|
break;
|
||||||
case MagicSpellType.AccountRemoval:
|
case MagicSpellType.AccountRemoval:
|
||||||
// await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
||||||
// contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
// contact.Content,
|
contact.Content,
|
||||||
// localizer["EmailAccountDeletionTitle"],
|
localizer["EmailAccountDeletionTitle"],
|
||||||
// new AccountDeletionEmailModel
|
new AccountDeletionEmailModel
|
||||||
// {
|
{
|
||||||
// Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
// Link = link
|
Link = link
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
break;
|
break;
|
||||||
case MagicSpellType.AuthPasswordReset:
|
case MagicSpellType.AuthPasswordReset:
|
||||||
// await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
||||||
// contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
// contact.Content,
|
contact.Content,
|
||||||
// localizer["EmailAccountDeletionTitle"],
|
localizer["EmailAccountDeletionTitle"],
|
||||||
// new PasswordResetEmailModel
|
new PasswordResetEmailModel
|
||||||
// {
|
{
|
||||||
// Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
// Link = link
|
Link = link
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
break;
|
break;
|
||||||
case MagicSpellType.ContactVerification:
|
case MagicSpellType.ContactVerification:
|
||||||
if (spell.Meta["contact_method"] is not string contactMethod)
|
if (spell.Meta["contact_method"] is not string contactMethod)
|
||||||
throw new InvalidOperationException("Contact method is not found.");
|
throw new InvalidOperationException("Contact method is not found.");
|
||||||
// await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
||||||
// contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
// contactMethod!,
|
contactMethod!,
|
||||||
// localizer["EmailContactVerificationTitle"],
|
localizer["EmailContactVerificationTitle"],
|
||||||
// new ContactVerificationEmailModel
|
new ContactVerificationEmailModel
|
||||||
// {
|
{
|
||||||
// Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
// Link = link
|
Link = link
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
@ -155,15 +146,8 @@ public class MagicSpellService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ApplyMagicSpell(string token)
|
public async Task ApplyMagicSpell(MagicSpell spell)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
var spell = await db.MagicSpells
|
|
||||||
.Where(s => s.Spell == token)
|
|
||||||
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (spell is null) throw new ArgumentException("Magic spell not found.");
|
|
||||||
|
|
||||||
switch (spell.Type)
|
switch (spell.Type)
|
||||||
{
|
{
|
||||||
case MagicSpellType.AuthPasswordReset:
|
case MagicSpellType.AuthPasswordReset:
|
@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
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 Shared.Models.Account Account { get; set; } = null!;
|
[JsonIgnore] public 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 Shared.Models.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||||
}
|
}
|
@ -1,14 +1,12 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Pass.Permission;
|
|
||||||
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 Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/notifications")]
|
[Route("/notifications")]
|
||||||
@ -19,7 +17,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
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 Shared.Models.Account currentUser) return Unauthorized();
|
if (currentUserValue is not 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)
|
||||||
@ -37,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (currentUserValue is not 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)
|
||||||
@ -69,15 +67,16 @@ 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 Shared.Models.Account;
|
var currentUser = currentUserValue as 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();
|
||||||
|
|
||||||
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
|
var result =
|
||||||
request.DeviceToken);
|
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
|
||||||
|
request.DeviceToken);
|
||||||
|
|
||||||
return Ok();
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("subscription")]
|
[HttpDelete("subscription")]
|
||||||
@ -86,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 Shared.Models.Account;
|
var currentUser = currentUserValue as 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,20 +1,17 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Connection;
|
||||||
using NodaTime;
|
|
||||||
using DysonNetwork.Shared.Services;
|
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using MagicOnion.Server;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class NotificationService(
|
public class NotificationService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IConfiguration config,
|
WebSocketService ws,
|
||||||
IHttpClientFactory httpFactory
|
IHttpClientFactory httpFactory,
|
||||||
) : ServiceBase<INotificationService>, INotificationService
|
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"]!);
|
||||||
@ -27,7 +24,7 @@ public class NotificationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
||||||
Shared.Models.Account account,
|
Account account,
|
||||||
NotificationPushProvider provider,
|
NotificationPushProvider provider,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
string deviceToken
|
string deviceToken
|
||||||
@ -66,14 +63,14 @@ public class NotificationService(
|
|||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
// db.NotificationPushSubscriptions.Add(subscription);
|
db.NotificationPushSubscriptions.Add(subscription);
|
||||||
// await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Notification> SendNotification(
|
public async Task<Notification> SendNotification(
|
||||||
Shared.Models.Account account,
|
Account account,
|
||||||
string topic,
|
string topic,
|
||||||
string? title = null,
|
string? title = null,
|
||||||
string? subtitle = null,
|
string? subtitle = null,
|
||||||
@ -106,19 +103,18 @@ public class NotificationService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSilent)
|
if (!isSilent) _ = DeliveryNotification(notification);
|
||||||
Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification);
|
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeliveryNotification(Notification notification)
|
public async Task DeliveryNotification(Notification notification)
|
||||||
{
|
{
|
||||||
// ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
||||||
// {
|
{
|
||||||
// Type = "notifications.new",
|
Type = "notifications.new",
|
||||||
// Data = notification
|
Data = notification
|
||||||
// });
|
});
|
||||||
|
|
||||||
// Pushing the notification
|
// Pushing the notification
|
||||||
var subscribers = await db.NotificationPushSubscriptions
|
var subscribers = await db.NotificationPushSubscriptions
|
||||||
@ -134,15 +130,15 @@ public class NotificationService(
|
|||||||
var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList();
|
var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList();
|
||||||
if (id.Count == 0) return;
|
if (id.Count == 0) return;
|
||||||
|
|
||||||
// await db.Notifications
|
await db.Notifications
|
||||||
// .Where(n => id.Contains(n.Id))
|
.Where(n => id.Contains(n.Id))
|
||||||
// .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now)
|
.ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now)
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BroadcastNotification(Notification notification, bool save = false)
|
public async Task BroadcastNotification(Notification notification, bool save = false)
|
||||||
{
|
{
|
||||||
var accounts = new List<Shared.Models.Account>(); // await db.Accounts.ToListAsync();
|
var accounts = await db.Accounts.ToListAsync();
|
||||||
|
|
||||||
if (save)
|
if (save)
|
||||||
{
|
{
|
||||||
@ -161,18 +157,18 @@ public class NotificationService(
|
|||||||
};
|
};
|
||||||
return newNotification;
|
return newNotification;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
// await db.BulkInsertAsync(notifications);
|
await db.BulkInsertAsync(notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var account in accounts)
|
foreach (var account in accounts)
|
||||||
{
|
{
|
||||||
notification.Account = account;
|
notification.Account = account;
|
||||||
notification.AccountId = account.Id;
|
notification.AccountId = account.Id;
|
||||||
// ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||||
// {
|
{
|
||||||
// Type = "notifications.new",
|
Type = "notifications.new",
|
||||||
// Data = notification
|
Data = notification
|
||||||
// });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscribers = await db.NotificationPushSubscriptions
|
var subscribers = await db.NotificationPushSubscriptions
|
||||||
@ -180,8 +176,7 @@ public class NotificationService(
|
|||||||
await _PushNotification(notification, subscribers);
|
await _PushNotification(notification, subscribers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendNotificationBatch(Notification notification, List<Shared.Models.Account> accounts,
|
public async Task SendNotificationBatch(Notification notification, List<Account> accounts, bool save = false)
|
||||||
bool save = false)
|
|
||||||
{
|
{
|
||||||
if (save)
|
if (save)
|
||||||
{
|
{
|
||||||
@ -207,18 +202,18 @@ public class NotificationService(
|
|||||||
{
|
{
|
||||||
notification.Account = account;
|
notification.Account = account;
|
||||||
notification.AccountId = account.Id;
|
notification.AccountId = account.Id;
|
||||||
// ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||||
// {
|
{
|
||||||
// Type = "notifications.new",
|
Type = "notifications.new",
|
||||||
// Data = notification
|
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
|
||||||
// .Where(s => accountsId.Contains(s.AccountId))
|
.Where(s => accountsId.Contains(s.AccountId))
|
||||||
// .ToListAsync();
|
.ToListAsync();
|
||||||
// await _PushNotification(notification, subscribers);
|
await _PushNotification(notification, subscribers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification,
|
private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification,
|
@ -1,6 +1,6 @@
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
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 Shared.Models.Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
public Guid RelatedId { get; set; }
|
public Guid RelatedId { get; set; }
|
||||||
public Shared.Models.Account Related { get; set; } = null!;
|
public Account Related { get; set; } = null!;
|
||||||
|
|
||||||
public Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
|
|
@ -1,12 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared.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;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/relationships")]
|
[Route("/relationships")]
|
||||||
@ -17,7 +15,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
|||||||
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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var query = db.AccountRelationships.AsQueryable()
|
var query = db.AccountRelationships.AsQueryable()
|
||||||
@ -48,7 +46,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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)
|
||||||
@ -71,7 +69,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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.");
|
||||||
@ -94,7 +92,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -115,7 +113,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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()
|
||||||
@ -135,7 +133,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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.");
|
||||||
@ -160,7 +158,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -177,7 +175,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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.");
|
||||||
@ -197,7 +195,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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.");
|
||||||
@ -217,7 +215,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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.");
|
||||||
@ -237,7 +235,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 Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not 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.");
|
@ -1,13 +1,10 @@
|
|||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Shared.Services;
|
|
||||||
using MagicOnion.Server;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceBase<IRelationshipService>, IRelationshipService
|
public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||||
{
|
{
|
||||||
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
||||||
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
|
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
|
||||||
@ -37,7 +34,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Relationship> CreateRelationship(Shared.Models.Account sender, Shared.Models.Account target, RelationshipStatus status)
|
public async Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status)
|
||||||
{
|
{
|
||||||
if (status == RelationshipStatus.Pending)
|
if (status == RelationshipStatus.Pending)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
@ -55,31 +52,31 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
db.AccountRelationships.Add(relationship);
|
db.AccountRelationships.Add(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// await PurgeRelationshipCache(sender.Id, target.Id);
|
await PurgeRelationshipCache(sender.Id, target.Id);
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Relationship> BlockAccount(Shared.Models.Account sender, Shared.Models.Account target)
|
public async Task<Relationship> BlockAccount(Account sender, 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(Shared.Models.Account sender, Shared.Models.Account target)
|
public async Task<Relationship> UnblockAccount(Account sender, 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.");
|
||||||
db.Remove(relationship);
|
db.Remove(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// await PurgeRelationshipCache(sender.Id, target.Id);
|
await PurgeRelationshipCache(sender.Id, target.Id);
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Relationship> SendFriendRequest(Shared.Models.Account sender, Shared.Models.Account target)
|
public async Task<Relationship> SendFriendRequest(Account sender, 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.");
|
||||||
@ -107,7 +104,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
|
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
// await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Relationship> AcceptFriendRelationship(
|
public async Task<Relationship> AcceptFriendRelationship(
|
||||||
@ -136,7 +133,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
||||||
|
|
||||||
return relationshipBackward;
|
return relationshipBackward;
|
||||||
}
|
}
|
||||||
@ -155,7 +152,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Guid>> ListAccountFriends(Shared.Models.Account account)
|
public async Task<List<Guid>> ListAccountFriends(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,10 +168,10 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
|
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return friends;
|
return friends ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Guid>> ListAccountBlocked(Shared.Models.Account account)
|
public async Task<List<Guid>> ListAccountBlocked(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);
|
||||||
@ -190,7 +187,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
|
|||||||
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
|
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocked;
|
return blocked ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The verification info of a resource
|
/// The verification info of a resource
|
@ -45,7 +45,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 Shared.Models.Account currentUser
|
return currentUserValue is not 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));
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,7 @@ public class ActivityService(
|
|||||||
PublisherService pub,
|
PublisherService pub,
|
||||||
RelationshipService rels,
|
RelationshipService rels,
|
||||||
PostService ps,
|
PostService ps,
|
||||||
DiscoveryService ds
|
DiscoveryService ds)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
private static double CalculateHotRank(Post.Post post, Instant now)
|
private static double CalculateHotRank(Post.Post post, Instant now)
|
||||||
{
|
{
|
||||||
@ -33,7 +32,7 @@ public class ActivityService(
|
|||||||
|
|
||||||
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
|
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
|
||||||
{
|
{
|
||||||
var realms = await ds.GetPublicRealmsAsync(null, 5, 0, true);
|
var realms = await ds.GetPublicRealmsAsync(null, null, 5, 0, true);
|
||||||
if (realms.Count > 0)
|
if (realms.Count > 0)
|
||||||
{
|
{
|
||||||
activities.Add(new DiscoveryActivity(
|
activities.Add(new DiscoveryActivity(
|
||||||
@ -119,7 +118,7 @@ public class ActivityService(
|
|||||||
public async Task<List<Activity>> GetActivities(
|
public async Task<List<Activity>> GetActivities(
|
||||||
int take,
|
int take,
|
||||||
Instant? cursor,
|
Instant? cursor,
|
||||||
Shared.Models.Account currentUser,
|
Account.Account currentUser,
|
||||||
string? filter = null,
|
string? filter = null,
|
||||||
HashSet<string>? debugInclude = null
|
HashSet<string>? debugInclude = null
|
||||||
)
|
)
|
||||||
@ -133,7 +132,7 @@ public class ActivityService(
|
|||||||
{
|
{
|
||||||
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
|
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
|
||||||
{
|
{
|
||||||
var realms = await ds.GetPublicRealmsAsync(null, 5, 0, true);
|
var realms = await ds.GetPublicRealmsAsync(null, null, 5, 0, true);
|
||||||
if (realms.Count > 0)
|
if (realms.Count > 0)
|
||||||
{
|
{
|
||||||
activities.Add(new DiscoveryActivity(
|
activities.Add(new DiscoveryActivity(
|
||||||
@ -258,7 +257,7 @@ public class ActivityService(
|
|||||||
return score + postCount;
|
return score + postCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Shared.Models.Publisher>> GetPopularPublishers(int take)
|
private async Task<List<Publisher.Publisher>> GetPopularPublishers(int take)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var recent = now.Minus(Duration.FromDays(7));
|
var recent = now.Minus(Duration.FromDays(7));
|
||||||
|
@ -1,16 +1,30 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using DysonNetwork.Sphere.Chat;
|
||||||
|
using DysonNetwork.Sphere.Developer;
|
||||||
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
|
using DysonNetwork.Sphere.Publisher;
|
||||||
|
using DysonNetwork.Sphere.Realm;
|
||||||
using DysonNetwork.Sphere.Sticker;
|
using DysonNetwork.Sphere.Sticker;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
using DysonNetwork.Sphere.Wallet;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Npgsql;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere;
|
namespace DysonNetwork.Sphere;
|
||||||
|
|
||||||
|
public interface IIdentifiedResource
|
||||||
|
{
|
||||||
|
public string ResourceIdentifier { get; }
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class ModelBase
|
public abstract class ModelBase
|
||||||
{
|
{
|
||||||
public Instant CreatedAt { get; set; }
|
public Instant CreatedAt { get; set; }
|
||||||
@ -23,10 +37,32 @@ public class AppDatabase(
|
|||||||
IConfiguration configuration
|
IConfiguration configuration
|
||||||
) : DbContext(options)
|
) : 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.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<Status> AccountStatuses { get; set; }
|
||||||
|
public DbSet<CheckInResult> AccountCheckInResults { get; set; }
|
||||||
|
public DbSet<Notification> Notifications { get; set; }
|
||||||
|
public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
||||||
|
public DbSet<Badge> Badges { get; set; }
|
||||||
|
public DbSet<ActionLog> ActionLogs { get; set; }
|
||||||
|
public DbSet<AbuseReport> AbuseReports { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Session> AuthSessions { get; set; }
|
||||||
|
public DbSet<Challenge> AuthChallenges { get; set; }
|
||||||
|
|
||||||
public DbSet<CloudFile> Files { get; set; }
|
public DbSet<CloudFile> Files { get; set; }
|
||||||
public DbSet<CloudFileReference> FileReferences { get; set; }
|
public DbSet<CloudFileReference> FileReferences { get; set; }
|
||||||
|
|
||||||
public DbSet<Shared.Models.Publisher> Publishers { get; set; }
|
public DbSet<Publisher.Publisher> Publishers { get; set; }
|
||||||
public DbSet<PublisherMember> PublisherMembers { get; set; }
|
public DbSet<PublisherMember> PublisherMembers { get; set; }
|
||||||
public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; }
|
public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; }
|
||||||
public DbSet<PublisherFeature> PublisherFeatures { get; set; }
|
public DbSet<PublisherFeature> PublisherFeatures { get; set; }
|
||||||
@ -37,8 +73,10 @@ public class AppDatabase(
|
|||||||
public DbSet<PostCategory> PostCategories { get; set; }
|
public DbSet<PostCategory> PostCategories { get; set; }
|
||||||
public DbSet<PostCollection> PostCollections { get; set; }
|
public DbSet<PostCollection> PostCollections { get; set; }
|
||||||
|
|
||||||
public DbSet<Shared.Models.Realm> Realms { get; set; }
|
public DbSet<Realm.Realm> Realms { get; set; }
|
||||||
public DbSet<RealmMember> RealmMembers { get; set; }
|
public DbSet<RealmMember> RealmMembers { get; set; }
|
||||||
|
public DbSet<Tag> Tags { get; set; }
|
||||||
|
public DbSet<RealmTag> RealmTags { get; set; }
|
||||||
|
|
||||||
public DbSet<ChatRoom> ChatRooms { get; set; }
|
public DbSet<ChatRoom> ChatRooms { get; set; }
|
||||||
public DbSet<ChatMember> ChatMembers { get; set; }
|
public DbSet<ChatMember> ChatMembers { get; set; }
|
||||||
@ -49,7 +87,7 @@ public class AppDatabase(
|
|||||||
public DbSet<Sticker.Sticker> Stickers { get; set; }
|
public DbSet<Sticker.Sticker> Stickers { get; set; }
|
||||||
public DbSet<StickerPack> StickerPacks { get; set; }
|
public DbSet<StickerPack> StickerPacks { get; set; }
|
||||||
|
|
||||||
public DbSet<Shared.Models.Wallet> Wallets { get; set; }
|
public DbSet<Wallet.Wallet> Wallets { get; set; }
|
||||||
public DbSet<WalletPocket> WalletPockets { get; set; }
|
public DbSet<WalletPocket> WalletPockets { get; set; }
|
||||||
public DbSet<Order> PaymentOrders { get; set; }
|
public DbSet<Order> PaymentOrders { get; set; }
|
||||||
public DbSet<Transaction> PaymentTransactions { get; set; }
|
public DbSet<Transaction> PaymentTransactions { get; set; }
|
||||||
@ -73,6 +111,38 @@ public class AppDatabase(
|
|||||||
.UseNodaTime()
|
.UseNodaTime()
|
||||||
).UseSnakeCaseNamingConvention();
|
).UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
|
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var defaultPermissionGroup = await context.Set<PermissionGroup>()
|
||||||
|
.FirstOrDefaultAsync(g => g.Key == "default", cancellationToken);
|
||||||
|
if (defaultPermissionGroup is null)
|
||||||
|
{
|
||||||
|
context.Set<PermissionGroup>().Add(new PermissionGroup
|
||||||
|
{
|
||||||
|
Key = "default",
|
||||||
|
Nodes = new List<string>
|
||||||
|
{
|
||||||
|
"posts.create",
|
||||||
|
"posts.react",
|
||||||
|
"publishers.create",
|
||||||
|
"files.create",
|
||||||
|
"chat.create",
|
||||||
|
"chat.messages.create",
|
||||||
|
"chat.realtime.create",
|
||||||
|
"accounts.statuses.create",
|
||||||
|
"accounts.statuses.update",
|
||||||
|
"stickers.packs.create",
|
||||||
|
"stickers.create"
|
||||||
|
}.Select(permission =>
|
||||||
|
PermissionService.NewPermissionNode("group:default", "global", permission, true))
|
||||||
|
.ToList()
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
optionsBuilder.UseSeeding((context, _) => {});
|
||||||
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +150,25 @@ public class AppDatabase(
|
|||||||
{
|
{
|
||||||
base.OnModelCreating(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);
|
||||||
|
|
||||||
modelBuilder.Entity<PublisherMember>()
|
modelBuilder.Entity<PublisherMember>()
|
||||||
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
|
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
|
||||||
modelBuilder.Entity<PublisherMember>()
|
modelBuilder.Entity<PublisherMember>()
|
||||||
@ -154,6 +243,19 @@ public class AppDatabase(
|
|||||||
.HasForeignKey(pm => pm.AccountId)
|
.HasForeignKey(pm => pm.AccountId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
modelBuilder.Entity<RealmTag>()
|
||||||
|
.HasKey(rt => new { rt.RealmId, rt.TagId });
|
||||||
|
modelBuilder.Entity<RealmTag>()
|
||||||
|
.HasOne(rt => rt.Realm)
|
||||||
|
.WithMany(r => r.RealmTags)
|
||||||
|
.HasForeignKey(rt => rt.RealmId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<RealmTag>()
|
||||||
|
.HasOne(rt => rt.Tag)
|
||||||
|
.WithMany(t => t.RealmTags)
|
||||||
|
.HasForeignKey(rt => rt.TagId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
modelBuilder.Entity<ChatMember>()
|
modelBuilder.Entity<ChatMember>()
|
||||||
.HasKey(pm => new { pm.Id });
|
.HasKey(pm => new { pm.Id });
|
||||||
modelBuilder.Entity<ChatMember>()
|
modelBuilder.Entity<ChatMember>()
|
||||||
@ -250,9 +352,23 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin
|
|||||||
{
|
{
|
||||||
public async Task Execute(IJobExecutionContext context)
|
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);
|
||||||
|
// Expired permission group members
|
||||||
|
affectedRows = await db.PermissionGroupMembers
|
||||||
|
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows);
|
||||||
|
|
||||||
logger.LogInformation("Deleting soft-deleted records...");
|
logger.LogInformation("Deleting soft-deleted records...");
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
var threshold = now - Duration.FromDays(7);
|
var threshold = now - Duration.FromDays(7);
|
||||||
|
|
||||||
var entityTypes = db.Model.GetEntityTypes()
|
var entityTypes = db.Model.GetEntityTypes()
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
|
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.Sphere.Auth.OidcProvider.Options;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
using DysonNetwork.Sphere.Storage.Handlers;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Microsoft.AspNetCore.Http;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Configuration;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
|
||||||
using Microsoft.Extensions.Logging;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
||||||
using SystemClock = Microsoft.Extensions.Internal.SystemClock;
|
using SystemClock = NodaTime.SystemClock;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public static class AuthConstants
|
public static class AuthConstants
|
||||||
{
|
{
|
||||||
@ -40,11 +46,12 @@ public class DysonTokenAuthHandler(
|
|||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
AppDatabase database
|
AppDatabase database,
|
||||||
// OidcProviderService oidc,
|
OidcProviderService oidc,
|
||||||
// ICacheService cache,
|
ICacheService cache,
|
||||||
// FlushBufferService fbs
|
FlushBufferService fbs
|
||||||
) : AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
)
|
||||||
|
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
||||||
{
|
{
|
||||||
public const string AuthCachePrefix = "auth:";
|
public const string AuthCachePrefix = "auth:";
|
||||||
|
|
||||||
@ -57,42 +64,36 @@ public class DysonTokenAuthHandler(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var now = NodaTime.SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
// Validate token and extract session ID
|
// Validate token and extract session ID
|
||||||
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
||||||
return AuthenticateResult.Fail("Invalid token.");
|
return AuthenticateResult.Fail("Invalid token.");
|
||||||
|
|
||||||
// Try to get session from cache first
|
// Try to get session from cache first
|
||||||
// var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
|
var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
|
||||||
var session = await database.AuthSessions
|
|
||||||
|
// If not in cache, load from database
|
||||||
|
if (session is null)
|
||||||
|
{
|
||||||
|
session = await database.AuthSessions
|
||||||
.Where(e => e.Id == sessionId)
|
.Where(e => e.Id == sessionId)
|
||||||
.Include(e => e.Challenge)
|
.Include(e => e.Challenge)
|
||||||
.Include(e => e.Account)
|
.Include(e => e.Account)
|
||||||
.ThenInclude(e => e.Profile)
|
.ThenInclude(e => e.Profile)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
// If not in cache, load from database
|
if (session is not null)
|
||||||
// if (session is null)
|
{
|
||||||
// {
|
// Store in cache for future requests
|
||||||
// session = await database.AuthSessions
|
await cache.SetWithGroupsAsync(
|
||||||
// .Where(e => e.Id == sessionId)
|
$"auth:{sessionId}",
|
||||||
// .Include(e => e.Challenge)
|
session,
|
||||||
// .Include(e => e.Account)
|
[$"{AccountService.AccountCachePrefix}{session.Account.Id}"],
|
||||||
// .ThenInclude(e => e.Profile)
|
TimeSpan.FromHours(1)
|
||||||
// .FirstOrDefaultAsync();
|
);
|
||||||
|
}
|
||||||
// if (session is not null)
|
}
|
||||||
// {
|
|
||||||
// // Store in cache for future requests
|
|
||||||
// await cache.SetWithGroupsAsync(
|
|
||||||
// $"auth:{sessionId}",
|
|
||||||
// session,
|
|
||||||
// // [$"{AccountService.AccountCachePrefix}{session.Account.Id}"],
|
|
||||||
// TimeSpan.FromHours(1)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Check if the session exists
|
// Check if the session exists
|
||||||
if (session == null)
|
if (session == null)
|
||||||
@ -128,13 +129,13 @@ public class DysonTokenAuthHandler(
|
|||||||
|
|
||||||
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
|
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
|
||||||
|
|
||||||
// var lastInfo = new LastActiveInfo
|
var lastInfo = new LastActiveInfo
|
||||||
// {
|
{
|
||||||
// Account = session.Account,
|
Account = session.Account,
|
||||||
// Session = session,
|
Session = session,
|
||||||
// SeenAt = SystemClock.Instance.GetCurrentInstant(),
|
SeenAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
// };
|
};
|
||||||
// fbs.Enqueue(lastInfo);
|
fbs.Enqueue(lastInfo);
|
||||||
|
|
||||||
return AuthenticateResult.Success(ticket);
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
@ -157,13 +158,12 @@ public class DysonTokenAuthHandler(
|
|||||||
// Handle JWT tokens (3 parts)
|
// Handle JWT tokens (3 parts)
|
||||||
case 3:
|
case 3:
|
||||||
{
|
{
|
||||||
// var (isValid, jwtResult) = oidc.ValidateToken(token);
|
var (isValid, jwtResult) = oidc.ValidateToken(token);
|
||||||
// if (!isValid) return false;
|
if (!isValid) return false;
|
||||||
// var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
||||||
// if (jti is null) return false;
|
if (jti is null) return false;
|
||||||
|
|
||||||
// return Guid.TryParse(jti, out sessionId);
|
return Guid.TryParse(jti, out sessionId);
|
||||||
return false; // Placeholder
|
|
||||||
}
|
}
|
||||||
// Handle compact tokens (2 parts)
|
// Handle compact tokens (2 parts)
|
||||||
case 2:
|
case 2:
|
@ -1,23 +1,23 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Sphere.Connection;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/auth")]
|
[Route("/auth")]
|
||||||
public class AuthController(
|
public class AuthController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
AuthService auth
|
AuthService auth,
|
||||||
// GeoIpService geo,
|
GeoIpService geo,
|
||||||
// ActionLogService als
|
ActionLogService als
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
public class ChallengeRequest
|
public class ChallengeRequest
|
||||||
@ -59,7 +59,7 @@ public class AuthController(
|
|||||||
Scopes = request.Scopes,
|
Scopes = request.Scopes,
|
||||||
IpAddress = ipAddress,
|
IpAddress = ipAddress,
|
||||||
UserAgent = userAgent,
|
UserAgent = userAgent,
|
||||||
// Location = geo.GetPointFromIp(ipAddress),
|
Location = geo.GetPointFromIp(ipAddress),
|
||||||
DeviceId = request.DeviceId,
|
DeviceId = request.DeviceId,
|
||||||
AccountId = account.Id
|
AccountId = account.Id
|
||||||
}.Normalize();
|
}.Normalize();
|
||||||
@ -67,9 +67,9 @@ public class AuthController(
|
|||||||
await db.AuthChallenges.AddAsync(challenge);
|
await db.AuthChallenges.AddAsync(challenge);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
|
als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
|
||||||
// new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
|
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
|
||||||
// );
|
);
|
||||||
|
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
@ -160,13 +160,13 @@ 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,
|
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||||
// new Dictionary<string, object>
|
new Dictionary<string, object>
|
||||||
// {
|
{
|
||||||
// { "challenge_id", challenge.Id },
|
{ "challenge_id", challenge.Id },
|
||||||
// { "factor_id", factor.Id }
|
{ "factor_id", factor.Id }
|
||||||
// }, Request, challenge.Account
|
}, Request, challenge.Account
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -177,26 +177,26 @@ public class AuthController(
|
|||||||
{
|
{
|
||||||
challenge.FailedAttempts++;
|
challenge.FailedAttempts++;
|
||||||
db.Update(challenge);
|
db.Update(challenge);
|
||||||
// als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||||
// new Dictionary<string, object>
|
new Dictionary<string, object>
|
||||||
// {
|
{
|
||||||
// { "challenge_id", challenge.Id },
|
{ "challenge_id", challenge.Id },
|
||||||
// { "factor_id", factor.Id }
|
{ "factor_id", factor.Id }
|
||||||
// }, Request, challenge.Account
|
}, 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,
|
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||||
// new Dictionary<string, object>
|
new Dictionary<string, object>
|
||||||
// {
|
{
|
||||||
// { "challenge_id", challenge.Id },
|
{ "challenge_id", challenge.Id },
|
||||||
// { "account_id", challenge.AccountId }
|
{ "account_id", challenge.AccountId }
|
||||||
// }, Request, challenge.Account
|
}, Request, challenge.Account
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
@ -1,23 +1,21 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class AuthService(
|
public class AuthService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IConfiguration config
|
IConfiguration config,
|
||||||
// IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
// IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
// ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Detect the risk of the current request to login
|
/// Detect the risk of the current request to login
|
||||||
@ -26,7 +24,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, Shared.Models.Account account)
|
public async Task<int> DetectChallengeRisk(HttpRequest request, Account.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
|
||||||
@ -75,13 +73,13 @@ public class AuthService(
|
|||||||
return totalRequiredSteps;
|
return totalRequiredSteps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Session> CreateSessionForOidcAsync(Shared.Models.Account account, Instant time, Guid? customAppId = null)
|
public async Task<Session> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null)
|
||||||
{
|
{
|
||||||
var challenge = new Challenge
|
var challenge = new Challenge
|
||||||
{
|
{
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
IpAddress = "127.0.0.1", // HttpContext.Connection.RemoteIpAddress?.ToString(),
|
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||||
UserAgent = "TestAgent", // HttpContext.Request.Headers.UserAgent,
|
UserAgent = HttpContext.Request.Headers.UserAgent,
|
||||||
StepRemain = 1,
|
StepRemain = 1,
|
||||||
StepTotal = 1,
|
StepTotal = 1,
|
||||||
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
|
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
|
||||||
@ -107,54 +105,53 @@ public class AuthService(
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(token)) return false;
|
if (string.IsNullOrWhiteSpace(token)) return false;
|
||||||
|
|
||||||
// var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
|
var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
|
||||||
// var apiSecret = config.GetSection("Captcha")["ApiSecret"];
|
var apiSecret = config.GetSection("Captcha")["ApiSecret"];
|
||||||
|
|
||||||
// var client = httpClientFactory.CreateClient();
|
var client = httpClientFactory.CreateClient();
|
||||||
|
|
||||||
// var jsonOpts = new JsonSerializerOptions
|
var jsonOpts = new JsonSerializerOptions
|
||||||
// {
|
{
|
||||||
// PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
// DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
|
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
// };
|
};
|
||||||
|
|
||||||
// switch (provider)
|
switch (provider)
|
||||||
// {
|
{
|
||||||
// case "cloudflare":
|
case "cloudflare":
|
||||||
// var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||||
// "application/x-www-form-urlencoded");
|
"application/x-www-form-urlencoded");
|
||||||
// var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
// content);
|
content);
|
||||||
// response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// var json = await response.Content.ReadAsStringAsync();
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
// var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
|
var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
|
||||||
|
|
||||||
// return result?.Success == true;
|
return result?.Success == true;
|
||||||
// case "google":
|
case "google":
|
||||||
// content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||||
// "application/x-www-form-urlencoded");
|
"application/x-www-form-urlencoded");
|
||||||
// response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content);
|
response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content);
|
||||||
// response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// json = await response.Content.ReadAsStringAsync();
|
json = await response.Content.ReadAsStringAsync();
|
||||||
// result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
|
result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
|
||||||
|
|
||||||
// return result?.Success == true;
|
return result?.Success == true;
|
||||||
// case "hcaptcha":
|
case "hcaptcha":
|
||||||
// content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||||
// "application/x-www-form-urlencoded");
|
"application/x-www-form-urlencoded");
|
||||||
// response = await client.PostAsync("https://hcaptcha.com/siteverify", content);
|
response = await client.PostAsync("https://hcaptcha.com/siteverify", content);
|
||||||
// response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// json = await response.Content.ReadAsStringAsync();
|
json = await response.Content.ReadAsStringAsync();
|
||||||
// result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
|
result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
|
||||||
|
|
||||||
// return result?.Success == true;
|
return result?.Success == true;
|
||||||
// default:
|
default:
|
||||||
// throw new ArgumentException("The server misconfigured for the captcha.");
|
throw new ArgumentException("The server misconfigured for the captcha.");
|
||||||
// }
|
}
|
||||||
return true; // Placeholder for captcha validation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CreateToken(Session session)
|
public string CreateToken(Session session)
|
||||||
@ -186,56 +183,56 @@ public class AuthService(
|
|||||||
return $"{payloadBase64}.{signatureBase64}";
|
return $"{payloadBase64}.{signatureBase64}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
|
public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
|
||||||
// {
|
{
|
||||||
// // Check if the session is already in sudo mode (cached)
|
// Check if the session is already in sudo mode (cached)
|
||||||
// var sudoModeKey = $"accounts:{session.Id}:sudo";
|
var sudoModeKey = $"accounts:{session.Id}:sudo";
|
||||||
// var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
|
var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
|
||||||
|
|
||||||
// if (found)
|
if (found)
|
||||||
// {
|
{
|
||||||
// // Session is already in sudo mode
|
// Session is already in sudo mode
|
||||||
// return true;
|
return true;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Check if the user has a pin code
|
// Check if the user has a pin code
|
||||||
// var hasPinCode = await db.AccountAuthFactors
|
var hasPinCode = await db.AccountAuthFactors
|
||||||
// .Where(f => f.AccountId == session.AccountId)
|
.Where(f => f.AccountId == session.AccountId)
|
||||||
// .Where(f => f.EnabledAt != null)
|
.Where(f => f.EnabledAt != null)
|
||||||
// .Where(f => f.Type == AccountAuthFactorType.PinCode)
|
.Where(f => f.Type == AccountAuthFactorType.PinCode)
|
||||||
// .AnyAsync();
|
.AnyAsync();
|
||||||
|
|
||||||
// if (!hasPinCode)
|
if (!hasPinCode)
|
||||||
// {
|
{
|
||||||
// // User doesn't have a pin code, no validation needed
|
// User doesn't have a pin code, no validation needed
|
||||||
// return true;
|
return true;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // If pin code is not provided, we can't validate
|
// If pin code is not provided, we can't validate
|
||||||
// if (string.IsNullOrEmpty(pinCode))
|
if (string.IsNullOrEmpty(pinCode))
|
||||||
// {
|
{
|
||||||
// return false;
|
return false;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// try
|
try
|
||||||
// {
|
{
|
||||||
// // Validate the pin code
|
// Validate the pin code
|
||||||
// var isValid = await ValidatePinCode(session.AccountId, pinCode);
|
var isValid = await ValidatePinCode(session.AccountId, pinCode);
|
||||||
|
|
||||||
// if (isValid)
|
if (isValid)
|
||||||
// {
|
{
|
||||||
// // Set session in sudo mode for 5 minutes
|
// Set session in sudo mode for 5 minutes
|
||||||
// await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
|
await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// return isValid;
|
return isValid;
|
||||||
// }
|
}
|
||||||
// catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
// {
|
{
|
||||||
// // No pin code enabled for this account, so validation is successful
|
// No pin code enabled for this account, so validation is successful
|
||||||
// return true;
|
return true;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidatePinCode(Guid accountId, string pinCode)
|
public async Task<bool> ValidatePinCode(Guid accountId, string pinCode)
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace DysonNetwork.Pass.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class CaptchaVerificationResponse
|
public class CaptchaVerificationResponse
|
||||||
{
|
{
|
@ -1,8 +1,6 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class CompactTokenService(IConfiguration config)
|
public class CompactTokenService(IConfiguration config)
|
||||||
{
|
{
|
@ -1,19 +1,18 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers;
|
namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
|
||||||
|
|
||||||
[Route("/auth/open")]
|
[Route("/auth/open")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@ -115,7 +114,7 @@ public class OidcProviderController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> GetUserInfo()
|
public async Task<IActionResult> GetUserInfo()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account.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
|
@ -1,6 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Models;
|
namespace DysonNetwork.Sphere.Auth.OidcProvider.Models;
|
||||||
|
|
||||||
public class AuthorizationCodeInfo
|
public class AuthorizationCodeInfo
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Options;
|
namespace DysonNetwork.Sphere.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.Pass.Auth.OidcProvider.Responses;
|
namespace DysonNetwork.Sphere.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.Pass.Auth.OidcProvider.Responses;
|
namespace DysonNetwork.Sphere.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.Pass.Auth.OidcProvider.Responses;
|
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
||||||
|
|
||||||
public class TokenResponse
|
public class TokenResponse
|
||||||
{
|
{
|
@ -2,18 +2,17 @@ using System.IdentityModel.Tokens.Jwt;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Models;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Models;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Developer;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Services;
|
namespace DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
||||||
|
|
||||||
public class OidcProviderService(
|
public class OidcProviderService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
@ -27,18 +26,16 @@ public class OidcProviderService(
|
|||||||
|
|
||||||
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
|
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
|
||||||
{
|
{
|
||||||
return null;
|
return await db.CustomApps
|
||||||
// return await db.CustomApps
|
.Include(c => c.Secrets)
|
||||||
// .Include(c => c.Secrets)
|
.FirstOrDefaultAsync(c => c.Id == clientId);
|
||||||
// .FirstOrDefaultAsync(c => c.Id == clientId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId)
|
public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId)
|
||||||
{
|
{
|
||||||
return null;
|
return await db.CustomApps
|
||||||
// return await db.CustomApps
|
.Include(c => c.Secrets)
|
||||||
// .Include(c => c.Secrets)
|
.FirstOrDefaultAsync(c => c.Id == appId);
|
||||||
// .FirstOrDefaultAsync(c => c.Id == appId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId)
|
public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId)
|
@ -1,10 +1,8 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
public class AfdianOidcService(
|
public class AfdianOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,6 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
public class AppleMobileConnectRequest
|
public class AppleMobileConnectRequest
|
||||||
{
|
{
|
@ -3,12 +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.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of OpenID Connect service for Apple Sign In
|
/// Implementation of OpenID Connect service for Apple Sign In
|
@ -1,13 +1,11 @@
|
|||||||
using DysonNetwork.Pass.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Shared.Cache;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/accounts/me/connections")]
|
[Route("/accounts/me/connections")]
|
||||||
@ -27,7 +25,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 Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var connections = await db.AccountConnections
|
var connections = await db.AccountConnections
|
||||||
@ -50,7 +48,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 Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var connection = await db.AccountConnections
|
var connection = await db.AccountConnections
|
||||||
@ -68,7 +66,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 Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
if (GetOidcService("apple") is not AppleOidcService appleService)
|
if (GetOidcService("apple") is not AppleOidcService appleService)
|
||||||
@ -134,7 +132,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 Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var oidcService = GetOidcService(request.Provider);
|
var oidcService = GetOidcService(request.Provider);
|
@ -1,10 +1,8 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
public class DiscordOidcService(
|
public class DiscordOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,10 +1,8 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
public class GitHubOidcService(
|
public class GitHubOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,11 +1,11 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
using System.Security.Cryptography;
|
||||||
using DysonNetwork.Shared.Models;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Configuration;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
public class GoogleOidcService(
|
public class GoogleOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,10 +1,8 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
public class MicrosoftOidcService(
|
public class MicrosoftOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,13 +1,11 @@
|
|||||||
using DysonNetwork.Pass.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/auth/login")]
|
[Route("/auth/login")]
|
||||||
@ -34,7 +32,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 Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is Account.Account currentUser)
|
||||||
{
|
{
|
||||||
var state = Guid.NewGuid().ToString();
|
var state = Guid.NewGuid().ToString();
|
||||||
var nonce = Guid.NewGuid().ToString();
|
var nonce = Guid.NewGuid().ToString();
|
||||||
@ -127,7 +125,7 @@ public class OidcController(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Shared.Models.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
|
private async Task<Account.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,15 +1,13 @@
|
|||||||
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.Shared.Cache;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base service for OpenID Connect authentication providers
|
/// Base service for OpenID Connect authentication providers
|
||||||
@ -192,7 +190,7 @@ public abstract class OidcService(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<Challenge> CreateChallengeForUserAsync(
|
public async Task<Challenge> CreateChallengeForUserAsync(
|
||||||
OidcUserInfo userInfo,
|
OidcUserInfo userInfo,
|
||||||
Shared.Models.Account account,
|
Account.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.Pass.Auth.OpenId;
|
namespace DysonNetwork.Sphere.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.Shared.Models;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the user information from an OIDC provider
|
/// Represents the user information from an OIDC provider
|
@ -1,10 +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.Developer;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Point = NetTopologySuite.Geometries.Point;
|
using Point = NetTopologySuite.Geometries.Point;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class Session : ModelBase
|
public class Session : ModelBase
|
||||||
{
|
{
|
||||||
@ -14,7 +15,7 @@ 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 { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
public Guid ChallengeId { get; set; }
|
public Guid ChallengeId { get; set; }
|
||||||
public Challenge Challenge { get; set; } = null!;
|
public Challenge Challenge { get; set; } = null!;
|
||||||
public Guid? AppId { get; set; }
|
public Guid? AppId { get; set; }
|
||||||
@ -48,9 +49,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; } = [];
|
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
|
||||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
||||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
|
||||||
[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; }
|
||||||
@ -58,7 +59,7 @@ 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 { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
|
|
||||||
public Challenge Normalize()
|
public Challenge Normalize()
|
||||||
{
|
{
|
@ -1,6 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -33,7 +32,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Dictionary<Guid, ChatSummaryResponse>>> GetChatSummary()
|
public async Task<ActionResult<Dictionary<Guid, ChatSummaryResponse>>> GetChatSummary()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var unreadMessages = await cs.CountUnreadMessageForUser(currentUser.Id);
|
var unreadMessages = await cs.CountUnreadMessageForUser(currentUser.Id);
|
||||||
var lastMessages = await cs.ListLastMessageForUser(currentUser.Id);
|
var lastMessages = await cs.ListLastMessageForUser(currentUser.Id);
|
||||||
@ -66,7 +65,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset,
|
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset,
|
||||||
[FromQuery] int take = 20)
|
[FromQuery] int take = 20)
|
||||||
{
|
{
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account;
|
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||||
|
|
||||||
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
if (room is null) return NotFound();
|
if (room is null) return NotFound();
|
||||||
@ -103,7 +102,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
[HttpGet("{roomId:guid}/messages/{messageId:guid}")]
|
[HttpGet("{roomId:guid}/messages/{messageId:guid}")]
|
||||||
public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId)
|
public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId)
|
||||||
{
|
{
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account;
|
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||||
|
|
||||||
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
if (room is null) return NotFound();
|
if (room is null) return NotFound();
|
||||||
@ -140,7 +139,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
[RequiredPermission("global", "chat.messages.create")]
|
[RequiredPermission("global", "chat.messages.create")]
|
||||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
request.Content = TextSanitizer.Sanitize(request.Content);
|
request.Content = TextSanitizer.Sanitize(request.Content);
|
||||||
if (string.IsNullOrWhiteSpace(request.Content) &&
|
if (string.IsNullOrWhiteSpace(request.Content) &&
|
||||||
@ -217,7 +216,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId)
|
public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
request.Content = TextSanitizer.Sanitize(request.Content);
|
request.Content = TextSanitizer.Sanitize(request.Content);
|
||||||
|
|
||||||
@ -270,7 +269,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> DeleteMessage(Guid roomId, Guid messageId)
|
public async Task<ActionResult> DeleteMessage(Guid roomId, Guid messageId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var message = await db.ChatMessages
|
var message = await db.ChatMessages
|
||||||
.Include(m => m.Sender)
|
.Include(m => m.Sender)
|
||||||
@ -296,7 +295,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
|||||||
[HttpPost("{roomId:guid}/sync")]
|
[HttpPost("{roomId:guid}/sync")]
|
||||||
public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, Guid roomId)
|
public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var isMember = await db.ChatMembers
|
var isMember = await db.ChatMembers
|
||||||
|
@ -1,9 +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 NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public enum ChatRoomType
|
public enum ChatRoomType
|
||||||
{
|
{
|
||||||
@ -30,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 Shared.Models.Realm? Realm { get; set; }
|
public Realm.Realm? Realm { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
[JsonPropertyName("members")]
|
[JsonPropertyName("members")]
|
||||||
@ -72,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 Shared.Models.Account Account { get; set; } = null!;
|
public Account.Account Account { get; set; } = null!;
|
||||||
|
|
||||||
[MaxLength(1024)] public string? Nick { get; set; }
|
[MaxLength(1024)] public string? Nick { get; set; }
|
||||||
|
|
||||||
@ -104,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 Shared.Models.Account Account { get; set; } = null!;
|
public Account.Account Account { get; set; } = null!;
|
||||||
|
|
||||||
[MaxLength(1024)] public string? Nick { get; set; }
|
[MaxLength(1024)] public string? Nick { get; set; }
|
||||||
|
|
@ -1,7 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
@ -37,7 +36,7 @@ public class ChatRoomController(
|
|||||||
if (chatRoom is null) return NotFound();
|
if (chatRoom is null) return NotFound();
|
||||||
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
||||||
|
|
||||||
if (HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is Account.Account currentUser)
|
||||||
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, currentUser.Id);
|
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, currentUser.Id);
|
||||||
|
|
||||||
return Ok(chatRoom);
|
return Ok(chatRoom);
|
||||||
@ -47,7 +46,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
|
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
|
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||||
@ -135,7 +134,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid userId)
|
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid userId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var room = await db.ChatRooms
|
var room = await db.ChatRooms
|
||||||
@ -165,7 +164,7 @@ public class ChatRoomController(
|
|||||||
[RequiredPermission("global", "chat.create")]
|
[RequiredPermission("global", "chat.create")]
|
||||||
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
if (request.Name is null) return BadRequest("You cannot create a chat room without a name.");
|
if (request.Name is null) return BadRequest("You cannot create a chat room without a name.");
|
||||||
|
|
||||||
var chatRoom = new ChatRoom
|
var chatRoom = new ChatRoom
|
||||||
@ -237,7 +236,7 @@ public class ChatRoomController(
|
|||||||
[HttpPatch("{id:guid}")]
|
[HttpPatch("{id:guid}")]
|
||||||
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
|
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(e => e.Id == id)
|
.Where(e => e.Id == id)
|
||||||
@ -322,7 +321,7 @@ public class ChatRoomController(
|
|||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
public async Task<ActionResult> DeleteChatRoom(Guid id)
|
public async Task<ActionResult> DeleteChatRoom(Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(e => e.Id == id)
|
.Where(e => e.Id == id)
|
||||||
@ -357,7 +356,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId)
|
public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
@ -376,7 +375,7 @@ public class ChatRoomController(
|
|||||||
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
|
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
|
||||||
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
|
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
|
||||||
{
|
{
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account;
|
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||||
|
|
||||||
var room = await db.ChatRooms
|
var room = await db.ChatRooms
|
||||||
.FirstOrDefaultAsync(r => r.Id == roomId);
|
.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
@ -449,7 +448,7 @@ public class ChatRoomController(
|
|||||||
public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId,
|
public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId,
|
||||||
[FromBody] ChatMemberRequest request)
|
[FromBody] ChatMemberRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||||
@ -520,7 +519,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<ChatMember>>> ListChatInvites()
|
public async Task<ActionResult<List<ChatMember>>> ListChatInvites()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var members = await db.ChatMembers
|
var members = await db.ChatMembers
|
||||||
@ -545,7 +544,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId)
|
public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
@ -572,7 +571,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> DeclineChatInvite(Guid roomId)
|
public async Task<ActionResult> DeclineChatInvite(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
@ -601,7 +600,7 @@ public class ChatRoomController(
|
|||||||
[FromBody] ChatMemberNotifyRequest request
|
[FromBody] ChatMemberNotifyRequest request
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(r => r.Id == roomId)
|
.Where(r => r.Id == roomId)
|
||||||
@ -630,7 +629,7 @@ public class ChatRoomController(
|
|||||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
|
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
|
||||||
{
|
{
|
||||||
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
|
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(r => r.Id == roomId)
|
.Where(r => r.Id == roomId)
|
||||||
@ -689,7 +688,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId)
|
public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(r => r.Id == roomId)
|
.Where(r => r.Id == roomId)
|
||||||
@ -737,7 +736,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId)
|
public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(r => r.Id == roomId)
|
.Where(r => r.Id == roomId)
|
||||||
@ -775,7 +774,7 @@ public class ChatRoomController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> LeaveChat(Guid roomId)
|
public async Task<ActionResult> LeaveChat(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id)
|
.Where(m => m.AccountId == currentUser.Id)
|
||||||
@ -808,7 +807,7 @@ public class ChatRoomController(
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task _SendInviteNotify(ChatMember member, Shared.Models.Account sender)
|
private async Task _SendInviteNotify(ChatMember member, Account.Account sender)
|
||||||
{
|
{
|
||||||
string title = localizer["ChatInviteTitle"];
|
string title = localizer["ChatInviteTitle"];
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
using DysonNetwork.Shared.Cache;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Sphere.Connection;
|
||||||
@ -242,7 +241,7 @@ public partial class ChatService(
|
|||||||
Priority = 10,
|
Priority = 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
List<Shared.Models.Account> accountsToNotify = [];
|
List<Account.Account> accountsToNotify = [];
|
||||||
foreach (var member in members)
|
foreach (var member in members)
|
||||||
{
|
{
|
||||||
scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket
|
scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket
|
||||||
@ -253,7 +252,7 @@ public partial class ChatService(
|
|||||||
|
|
||||||
if (member.Account.Id == sender.AccountId) continue;
|
if (member.Account.Id == sender.AccountId) continue;
|
||||||
if (member.Notify == ChatMemberNotify.None) continue;
|
if (member.Notify == ChatMemberNotify.None) continue;
|
||||||
if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
|
// if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
|
||||||
if (message.MembersMentioned is null || !message.MembersMentioned.Contains(member.Account.Id))
|
if (message.MembersMentioned is null || !message.MembersMentioned.Contains(member.Account.Id))
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
@ -1,9 +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.Storage;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public class Message : ModelBase, IIdentifiedResource
|
public class Message : ModelBase, IIdentifiedResource
|
||||||
{
|
{
|
@ -36,7 +36,7 @@ public interface IRealtimeService
|
|||||||
/// <param name="sessionId">The session identifier</param>
|
/// <param name="sessionId">The session identifier</param>
|
||||||
/// <param name="isAdmin">The user is the admin of session</param>
|
/// <param name="isAdmin">The user is the admin of session</param>
|
||||||
/// <returns>User-specific token for the session</returns>
|
/// <returns>User-specific token for the session</returns>
|
||||||
string GetUserToken(Shared.Models.Account account, string sessionId, bool isAdmin = false);
|
string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes incoming webhook requests from the realtime service provider
|
/// Processes incoming webhook requests from the realtime service provider
|
||||||
|
@ -4,8 +4,6 @@ using Livekit.Server.Sdk.Dotnet;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||||
|
|
||||||
@ -113,7 +111,7 @@ public class LivekitRealtimeService : IRealtimeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetUserToken(Shared.Models.Account account, string sessionId, bool isAdmin = false)
|
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||||
{
|
{
|
||||||
var token = _accessToken.WithIdentity(account.Name)
|
var token = _accessToken.WithIdentity(account.Name)
|
||||||
.WithName(account.Nick)
|
.WithName(account.Nick)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
|
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.Shared.Models;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public class RealtimeCall : ModelBase
|
public class RealtimeCall : ModelBase
|
||||||
{
|
{
|
@ -1,4 +1,3 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using Livekit.Server.Sdk.Dotnet;
|
using Livekit.Server.Sdk.Dotnet;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -47,7 +46,7 @@ public class RealtimeCallController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId)
|
public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||||
@ -72,7 +71,7 @@ public class RealtimeCallController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId)
|
public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
// Check if the user is a member of the chat room
|
// Check if the user is a member of the chat room
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
@ -145,7 +144,7 @@ public class RealtimeCallController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId)
|
public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||||
@ -164,7 +163,7 @@ public class RealtimeCallController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId)
|
public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -20,7 +19,7 @@ public class MessageReadHandler(
|
|||||||
public const string ChatMemberCacheKey = "ChatMember_{0}_{1}";
|
public const string ChatMemberCacheKey = "ChatMember_{0}_{1}";
|
||||||
|
|
||||||
public async Task HandleAsync(
|
public async Task HandleAsync(
|
||||||
Shared.Models.Account currentUser,
|
Account.Account currentUser,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
WebSocketPacket packet,
|
WebSocketPacket packet,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
|
@ -10,7 +10,7 @@ public class MessageTypingHandler(ChatRoomService crs) : IWebSocketPacketHandler
|
|||||||
public string PacketType => "messages.typing";
|
public string PacketType => "messages.typing";
|
||||||
|
|
||||||
public async Task HandleAsync(
|
public async Task HandleAsync(
|
||||||
Shared.Models.Account currentUser,
|
Account.Account currentUser,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
WebSocketPacket packet,
|
WebSocketPacket packet,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
|
@ -9,7 +9,7 @@ public class MessagesSubscribeHandler(ChatRoomService crs) : IWebSocketPacketHan
|
|||||||
public string PacketType => "messages.subscribe";
|
public string PacketType => "messages.subscribe";
|
||||||
|
|
||||||
public async Task HandleAsync(
|
public async Task HandleAsync(
|
||||||
Shared.Models.Account currentUser,
|
Account.Account currentUser,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
WebSocketPacket packet,
|
WebSocketPacket packet,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
|
@ -8,7 +8,7 @@ public class MessagesUnsubscribeHandler() : IWebSocketPacketHandler
|
|||||||
public string PacketType => "messages.unsubscribe";
|
public string PacketType => "messages.unsubscribe";
|
||||||
|
|
||||||
public Task HandleAsync(
|
public Task HandleAsync(
|
||||||
Shared.Models.Account currentUser,
|
Account.Account currentUser,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
WebSocketPacket packet,
|
WebSocketPacket packet,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
|
@ -5,5 +5,5 @@ namespace DysonNetwork.Sphere.Connection;
|
|||||||
public interface IWebSocketPacketHandler
|
public interface IWebSocketPacketHandler
|
||||||
{
|
{
|
||||||
string PacketType { get; }
|
string PacketType { get; }
|
||||||
Task HandleAsync(Shared.Models.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
|
Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
|
||||||
}
|
}
|
@ -40,7 +40,7 @@ public class WebFeed : ModelBase
|
|||||||
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
|
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
|
||||||
|
|
||||||
public Guid PublisherId { get; set; }
|
public Guid PublisherId { get; set; }
|
||||||
public Shared.Models.Publisher Publisher { get; set; } = null!;
|
public Publisher.Publisher Publisher { get; set; } = null!;
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
|
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
|
||||||
}
|
}
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -44,7 +43,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> CreateWebFeed([FromRoute] string pubName, [FromBody] WebFeedRequest request)
|
public async Task<IActionResult> CreateWebFeed([FromRoute] string pubName, [FromBody] WebFeedRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Url) || string.IsNullOrWhiteSpace(request.Title))
|
if (string.IsNullOrWhiteSpace(request.Url) || string.IsNullOrWhiteSpace(request.Title))
|
||||||
return BadRequest("Url and title are required");
|
return BadRequest("Url and title are required");
|
||||||
@ -63,7 +62,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> UpdateFeed([FromRoute] string pubName, Guid id, [FromBody] WebFeedRequest request)
|
public async Task<IActionResult> UpdateFeed([FromRoute] string pubName, Guid id, [FromBody] WebFeedRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var publisher = await ps.GetPublisherByName(pubName);
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
if (publisher is null) return NotFound();
|
if (publisher is null) return NotFound();
|
||||||
@ -83,7 +82,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> DeleteFeed([FromRoute] string pubName, Guid id)
|
public async Task<IActionResult> DeleteFeed([FromRoute] string pubName, Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var publisher = await ps.GetPublisherByName(pubName);
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
if (publisher is null) return NotFound();
|
if (publisher is null) return NotFound();
|
||||||
@ -105,7 +104,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> Scrap([FromRoute] string pubName, Guid id)
|
public async Task<ActionResult> Scrap([FromRoute] string pubName, Guid id)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var publisher = await ps.GetPublisherByName(pubName);
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
if (publisher is null) return NotFound();
|
if (publisher is null) return NotFound();
|
||||||
|
@ -11,7 +11,7 @@ public class WebFeedService(
|
|||||||
WebReaderService webReaderService
|
WebReaderService webReaderService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<WebFeed> CreateWebFeedAsync(Shared.Models.Publisher publisher,
|
public async Task<WebFeed> CreateWebFeedAsync(Publisher.Publisher publisher,
|
||||||
WebFeedController.WebFeedRequest request)
|
WebFeedController.WebFeedRequest request)
|
||||||
{
|
{
|
||||||
var feed = new WebFeed
|
var feed = new WebFeed
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DysonNetwork.Shared.Cache;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||||
@ -19,8 +18,8 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
{
|
{
|
||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||||
if (currentUserValue is not Shared.Models.Account currentUser ||
|
if (currentUserValue is not Account.Account currentUser ||
|
||||||
currentSessionValue is not Session currentSession)
|
currentSessionValue is not Auth.Session currentSession)
|
||||||
{
|
{
|
||||||
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
return;
|
return;
|
||||||
@ -70,7 +69,7 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
|
|
||||||
private async Task _ConnectionEventLoop(
|
private async Task _ConnectionEventLoop(
|
||||||
string deviceId,
|
string deviceId,
|
||||||
Shared.Models.Account currentUser,
|
Account.Account currentUser,
|
||||||
WebSocket webSocket,
|
WebSocket webSocket,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
|
@ -106,7 +106,7 @@ public class WebSocketService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandlePacket(Shared.Models.Account currentUser, string deviceId, WebSocketPacket packet,
|
public async Task HandlePacket(Account.Account currentUser, string deviceId, WebSocketPacket packet,
|
||||||
WebSocket socket)
|
WebSocket socket)
|
||||||
{
|
{
|
||||||
if (_handlerMap.TryGetValue(packet.Type, out var handler))
|
if (_handlerMap.TryGetValue(packet.Type, out var handler))
|
||||||
|
4000
DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.Designer.cs
generated
Normal file
4000
DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,139 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddOidcProviderSupport : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "remarks",
|
||||||
|
table: "custom_app_secrets",
|
||||||
|
newName: "description");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "allow_offline_access",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "allowed_grant_types",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "allowed_scopes",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "client_uri",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(1024)",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "logo_uri",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "post_logout_redirect_uris",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "redirect_uris",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "require_pkce",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_oidc",
|
||||||
|
table: "custom_app_secrets",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_custom_app_secrets_secret",
|
||||||
|
table: "custom_app_secrets",
|
||||||
|
column: "secret",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_custom_app_secrets_secret",
|
||||||
|
table: "custom_app_secrets");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "allow_offline_access",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "allowed_grant_types",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "allowed_scopes",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "client_uri",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "logo_uri",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "post_logout_redirect_uris",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "redirect_uris",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "require_pkce",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_oidc",
|
||||||
|
table: "custom_app_secrets");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "description",
|
||||||
|
table: "custom_app_secrets",
|
||||||
|
newName: "remarks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4014
DysonNetwork.Sphere/Data/Migrations/20250629084150_AuthSessionWithApp.Designer.cs
generated
Normal file
4014
DysonNetwork.Sphere/Data/Migrations/20250629084150_AuthSessionWithApp.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AuthSessionWithApp : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "app_id",
|
||||||
|
table: "auth_sessions",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_auth_sessions_app_id",
|
||||||
|
table: "auth_sessions",
|
||||||
|
column: "app_id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_auth_sessions_custom_apps_app_id",
|
||||||
|
table: "auth_sessions",
|
||||||
|
column: "app_id",
|
||||||
|
principalTable: "custom_apps",
|
||||||
|
principalColumn: "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_auth_sessions_custom_apps_app_id",
|
||||||
|
table: "auth_sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_auth_sessions_app_id",
|
||||||
|
table: "auth_sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "app_id",
|
||||||
|
table: "auth_sessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user