🐛 Bug fixes and improvements
This commit is contained in:
parent
758186f674
commit
84a88222bd
@ -4,7 +4,6 @@ using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
@ -15,8 +14,8 @@ public class AccountController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
AuthService auth,
|
||||
MagicSpellService spells,
|
||||
IMemoryCache memCache
|
||||
AccountService accounts,
|
||||
MagicSpellService spells
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
@ -138,7 +137,7 @@ public class AccountController(
|
||||
if (request.Nick is not null) account.Nick = request.Nick;
|
||||
if (request.Language is not null) account.Language = request.Language;
|
||||
|
||||
memCache.Remove($"user_${account.Id}");
|
||||
await accounts.PurgeAccountCache(account);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return account;
|
||||
@ -199,7 +198,7 @@ public class AccountController(
|
||||
db.Update(profile);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
memCache.Remove($"user_${userId}");
|
||||
await accounts.PurgeAccountCache(currentUser);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
@ -1,12 +1,23 @@
|
||||
using Casbin;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class AccountService(AppDatabase db, PermissionService pm)
|
||||
public class AccountService(AppDatabase db, PermissionService pm, IMemoryCache cache)
|
||||
{
|
||||
public async Task PurgeAccountCache(Account account)
|
||||
{
|
||||
var sessions = await db.AuthSessions.Where(e => e.Account.Id == account.Id).Select(e => e.Id)
|
||||
.ToListAsync();
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
cache.Remove($"dyn_auth_{session}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Account?> LookupAccount(string probe)
|
||||
{
|
||||
var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync();
|
||||
|
@ -45,6 +45,8 @@ public class MagicSpellService(AppDatabase db, EmailService email, ILogger<Magic
|
||||
|
||||
// TODO replace the baseurl
|
||||
var link = $"https://api.sn.solsynth.dev/spells/{Uri.EscapeDataString(spell.Spell)}";
|
||||
|
||||
logger.LogError($"Sending magic spell... {link}");
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -36,7 +37,6 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
|
||||
public class PushNotificationSubscribeRequest
|
||||
{
|
||||
[MaxLength(4096)] public string DeviceId { get; set; } = null!;
|
||||
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
|
||||
public NotificationPushProvider Provider { get; set; }
|
||||
}
|
||||
@ -47,12 +47,16 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
[FromBody] PushNotificationSubscribeRequest request
|
||||
)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Account;
|
||||
if (currentUser == null) return Unauthorized();
|
||||
var currentSession = currentSessionValue as Session;
|
||||
if (currentSession == null) return Unauthorized();
|
||||
|
||||
var result =
|
||||
await nty.SubscribePushNotification(currentUser, request.Provider, request.DeviceId, request.DeviceToken);
|
||||
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
|
||||
request.DeviceToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
@ -72,11 +72,10 @@ public class NotificationService
|
||||
DeviceId = deviceId,
|
||||
DeviceToken = deviceToken,
|
||||
Provider = provider,
|
||||
Account = account,
|
||||
AccountId = account.Id,
|
||||
};
|
||||
|
||||
_db.Add(subscription);
|
||||
_db.NotificationPushSubscriptions.Add(subscription);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return subscription;
|
||||
|
@ -20,8 +20,9 @@ public class AuthController(
|
||||
{
|
||||
public class ChallengeRequest
|
||||
{
|
||||
[Required] public ChallengePlatform Platform { get; set; }
|
||||
[Required] [MaxLength(256)] public string Account { get; set; } = string.Empty;
|
||||
[MaxLength(512)] public string? DeviceId { get; set; }
|
||||
[Required] [MaxLength(512)] public string DeviceId { get; set; }
|
||||
public List<string> Audiences { get; set; } = new();
|
||||
public List<string> Scopes { get; set; } = new();
|
||||
}
|
||||
@ -52,6 +53,7 @@ public class AuthController(
|
||||
Account = account,
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
|
||||
StepTotal = 1,
|
||||
Platform = request.Platform,
|
||||
Audiences = request.Audiences,
|
||||
Scopes = request.Scopes,
|
||||
IpAddress = ipAddress,
|
||||
@ -125,6 +127,8 @@ public class AuthController(
|
||||
}
|
||||
catch
|
||||
{
|
||||
challenge.FailedAttempts++;
|
||||
await db.SaveChangesAsync();
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
@ -8,6 +9,7 @@ namespace DysonNetwork.Sphere.Auth;
|
||||
public class Session : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string? Label { get; set; }
|
||||
public Instant? LastGrantedAt { get; set; }
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
||||
@ -15,6 +17,23 @@ public class Session : ModelBase
|
||||
[JsonIgnore] public Challenge Challenge { get; set; } = null!;
|
||||
}
|
||||
|
||||
public enum ChallengeType
|
||||
{
|
||||
Login,
|
||||
OAuth
|
||||
}
|
||||
|
||||
public enum ChallengePlatform
|
||||
{
|
||||
Unidentified,
|
||||
Web,
|
||||
Ios,
|
||||
Android,
|
||||
MacOs,
|
||||
Windows,
|
||||
Linux
|
||||
}
|
||||
|
||||
public class Challenge : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
@ -22,6 +41,8 @@ public class Challenge : ModelBase
|
||||
public int StepRemain { get; set; }
|
||||
public int StepTotal { get; set; }
|
||||
public int FailedAttempts { get; set; }
|
||||
public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
|
||||
public ChallengeType Type { get; set; } = ChallengeType.Login;
|
||||
[Column(TypeName = "jsonb")] public List<long> BlacklistFactors { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
|
||||
|
@ -7,29 +7,30 @@ public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, AppDatabase db)
|
||||
{
|
||||
var userIdClaim = context.User.FindFirst("user_id")?.Value;
|
||||
if (userIdClaim is not null && long.TryParse(userIdClaim, out var userId))
|
||||
var sessionIdClaim = context.User.FindFirst("session_id")?.Value;
|
||||
if (sessionIdClaim is not null && Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
{
|
||||
if (!cache.TryGetValue($"user_{userId}", out Account.Account? user))
|
||||
if (!cache.TryGetValue($"dyn_auth_{sessionId}", out Session? session))
|
||||
{
|
||||
user = await db.Accounts
|
||||
.Include(e => e.Profile)
|
||||
.Include(e => e.Profile.Picture)
|
||||
.Include(e => e.Profile.Background)
|
||||
.Where(e => e.Id == userId)
|
||||
session = await db.AuthSessions
|
||||
.Include(e => e.Challenge)
|
||||
.Include(e => e.Account)
|
||||
.Include(e => e.Account.Profile)
|
||||
.Include(e => e.Account.Profile.Picture)
|
||||
.Include(e => e.Account.Profile.Background)
|
||||
.Where(e => e.Id == sessionId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (user is not null)
|
||||
if (session is not null)
|
||||
{
|
||||
cache.Set($"user_{userId}", user, TimeSpan.FromMinutes(10));
|
||||
cache.Set($"dyn_auth_{sessionId}", session, TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (user is not null)
|
||||
if (session is not null)
|
||||
{
|
||||
context.Items["CurrentUser"] = user;
|
||||
var prefix = user.IsSuperuser ? "super:" : "";
|
||||
context.Items["CurrentIdentity"] = $"{prefix}{userId}";
|
||||
context.Items["CurrentUser"] = session.Account;
|
||||
context.Items["CurrentSession"] = session;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
@ -11,13 +10,10 @@ namespace DysonNetwork.Sphere.Connection;
|
||||
[Route("/ws")]
|
||||
public class WebSocketController : ControllerBase
|
||||
{
|
||||
// Concurrent dictionary to store active WebSocket connections.
|
||||
// Key: Tuple (AccountId, DeviceId); Value: WebSocket and CancellationTokenSource
|
||||
private static readonly ConcurrentDictionary<
|
||||
(long AccountId, string DeviceId),
|
||||
(WebSocket Socket, CancellationTokenSource Cts)
|
||||
> ActiveConnections =
|
||||
new ConcurrentDictionary<(long, string), (WebSocket, CancellationTokenSource)>();
|
||||
> ActiveConnections = new();
|
||||
|
||||
[Route("/ws")]
|
||||
[Authorize]
|
||||
@ -26,18 +22,18 @@ public class WebSocketController : ControllerBase
|
||||
{
|
||||
if (HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
// Get AccountId from HttpContext
|
||||
if (
|
||||
!HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue)
|
||||
|| currentUserValue is not Account.Account currentUser
|
||||
)
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
if (currentUserValue is not Account.Account currentUser ||
|
||||
currentSessionValue is not Auth.Session currentSession)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
long accountId = currentUser.Id;
|
||||
|
||||
// Verify deviceId
|
||||
var accountId = currentUser.Id;
|
||||
var deviceId = currentSession.Challenge.DeviceId;
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
@ -45,14 +41,11 @@ public class WebSocketController : ControllerBase
|
||||
}
|
||||
|
||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
// Create a CancellationTokenSource for this connection
|
||||
var cts = new CancellationTokenSource();
|
||||
var connectionKey = (accountId, deviceId);
|
||||
|
||||
// Add the connection to the active connections dictionary
|
||||
if (!ActiveConnections.TryAdd(connectionKey, (webSocket, cts)))
|
||||
{
|
||||
// Failed to add
|
||||
await webSocket.CloseAsync(
|
||||
WebSocketCloseStatus.InternalServerError,
|
||||
"Failed to establish connection.",
|
||||
@ -71,7 +64,6 @@ public class WebSocketController : ControllerBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Connection is closed, remove it from the active connections dictionary
|
||||
ActiveConnections.TryRemove(connectionKey, out _);
|
||||
cts.Dispose();
|
||||
}
|
||||
@ -88,25 +80,21 @@ public class WebSocketController : ControllerBase
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Buffer for receiving messages.
|
||||
var buffer = new byte[1024 * 4];
|
||||
try
|
||||
{
|
||||
// We don't handle receiving data, so we ignore the return.
|
||||
var receiveResult = await webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cancellationToken
|
||||
);
|
||||
while (!receiveResult.CloseStatus.HasValue)
|
||||
{
|
||||
// Keep connection alive and wait for close requests
|
||||
receiveResult = await webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
// Close connection
|
||||
await webSocket.CloseAsync(
|
||||
receiveResult.CloseStatus.Value,
|
||||
receiveResult.CloseStatusDescription,
|
||||
@ -144,5 +132,4 @@ public class WebSocketController : ControllerBase
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -15,7 +15,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20250429115700_InitialMigration")]
|
||||
[Migration("20250430163514_InitialMigration")]
|
||||
partial class InitialMigration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@ -508,6 +508,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("nonce");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("platform");
|
||||
|
||||
b.Property<List<string>>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
@ -521,6 +525,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("step_total");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
@ -566,6 +574,11 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("label");
|
||||
|
||||
b.Property<Instant?>("LastGrantedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_granted_at");
|
@ -171,6 +171,8 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
step_remain = table.Column<int>(type: "integer", nullable: false),
|
||||
step_total = table.Column<int>(type: "integer", nullable: false),
|
||||
failed_attempts = table.Column<int>(type: "integer", nullable: false),
|
||||
platform = table.Column<int>(type: "integer", nullable: false),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
blacklist_factors = table.Column<List<long>>(type: "jsonb", nullable: false),
|
||||
audiences = table.Column<List<string>>(type: "jsonb", nullable: false),
|
||||
scopes = table.Column<List<string>>(type: "jsonb", nullable: false),
|
||||
@ -326,6 +328,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
last_granted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
account_id = table.Column<long>(type: "bigint", nullable: false),
|
@ -505,6 +505,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("nonce");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("platform");
|
||||
|
||||
b.Property<List<string>>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
@ -518,6 +522,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("step_total");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
@ -563,6 +571,11 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("label");
|
||||
|
||||
b.Property<Instant?>("LastGrantedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_granted_at");
|
||||
|
@ -24,6 +24,7 @@ using NodaTime.Serialization.SystemTextJson;
|
||||
using Quartz;
|
||||
using tusdotnet;
|
||||
using tusdotnet.Models;
|
||||
using tusdotnet.Models.Configuration;
|
||||
using File = System.IO.File;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -34,7 +35,6 @@ builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize =
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddDbContext<AppDatabase>();
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
@ -189,7 +189,7 @@ var tusDiskStore = new tusdotnet.Stores.TusDiskStore(
|
||||
app.MapTus("/files/tus", (_) => Task.FromResult<DefaultTusConfiguration>(new()
|
||||
{
|
||||
Store = tusDiskStore,
|
||||
Events = new()
|
||||
Events = new Events
|
||||
{
|
||||
OnAuthorizeAsync = async eventContext =>
|
||||
{
|
||||
@ -203,8 +203,8 @@ app.MapTus("/files/tus", (_) => Task.FromResult<DefaultTusConfiguration>(new()
|
||||
}
|
||||
|
||||
var httpContext = eventContext.HttpContext;
|
||||
var user = httpContext.User;
|
||||
if (!user.Identity?.IsAuthenticated ?? true)
|
||||
var user = httpContext.Items["CurrentUser"] as Account;
|
||||
if (user is null)
|
||||
{
|
||||
eventContext.FailRequest(HttpStatusCode.Unauthorized);
|
||||
return;
|
||||
@ -223,12 +223,7 @@ app.MapTus("/files/tus", (_) => Task.FromResult<DefaultTusConfiguration>(new()
|
||||
OnFileCompleteAsync = async eventContext =>
|
||||
{
|
||||
var httpContext = eventContext.HttpContext;
|
||||
var user = httpContext.User;
|
||||
var userId = long.Parse(user.FindFirst("user_id")!.Value);
|
||||
|
||||
var db = httpContext.RequestServices.GetRequiredService<AppDatabase>();
|
||||
var account = await db.Accounts.FindAsync(userId);
|
||||
if (account is null) return;
|
||||
if (httpContext.Items["CurrentUser"] is not Account user) return;
|
||||
|
||||
var file = await eventContext.GetFileAsync();
|
||||
var metadata = await file.GetMetadataAsync(eventContext.CancellationToken);
|
||||
@ -238,7 +233,7 @@ app.MapTus("/files/tus", (_) => Task.FromResult<DefaultTusConfiguration>(new()
|
||||
|
||||
var fileService = eventContext.HttpContext.RequestServices.GetRequiredService<FileService>();
|
||||
|
||||
var info = await fileService.AnalyzeFileAsync(account, file.Id, fileStream, fileName, contentType);
|
||||
var info = await fileService.AnalyzeFileAsync(user, file.Id, fileStream, fileName, contentType);
|
||||
|
||||
var jsonOptions = httpContext.RequestServices.GetRequiredService<IOptions<JsonOptions>>().Value
|
||||
.JsonSerializerOptions;
|
||||
|
Loading…
x
Reference in New Issue
Block a user