♻️ Rebuilt own auth infra
This commit is contained in:
parent
aabe8269f5
commit
88977ccda3
@ -20,28 +20,32 @@ public class ActionLogService(AppDatabase db, GeoIpService geo) : IDisposable
|
||||
|
||||
_creationQueue.Enqueue(log);
|
||||
}
|
||||
|
||||
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request)
|
||||
|
||||
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
|
||||
Account? account = null)
|
||||
{
|
||||
if (request.HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
throw new ArgumentException("No user context was found");
|
||||
if (request.HttpContext.Items["CurrentSession"] is not Auth.Session currentSession)
|
||||
throw new ArgumentException("No session context was found");
|
||||
|
||||
var log = new ActionLog
|
||||
{
|
||||
Action = action,
|
||||
AccountId = currentUser.Id,
|
||||
SessionId = currentSession.Id,
|
||||
Meta = meta,
|
||||
UserAgent = request.Headers.UserAgent,
|
||||
IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
|
||||
};
|
||||
|
||||
|
||||
if (request.HttpContext.Items["CurrentUser"] is Account currentUser)
|
||||
log.AccountId = currentUser.Id;
|
||||
else if (account != null)
|
||||
log.AccountId = account.Id;
|
||||
else
|
||||
throw new ArgumentException("No user context was found");
|
||||
|
||||
if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession)
|
||||
log.SessionId = currentSession.Id;
|
||||
|
||||
_creationQueue.Enqueue(log);
|
||||
}
|
||||
|
||||
|
||||
public async Task FlushQueue()
|
||||
{
|
||||
var workingQueue = new List<ActionLog>();
|
||||
|
@ -17,11 +17,14 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var totalCount = await db.AccountRelationships
|
||||
.CountAsync(r => r.Account.Id == userId);
|
||||
var relationships = await db.AccountRelationships
|
||||
.Where(r => r.Account.Id == userId)
|
||||
var query = db.AccountRelationships.AsQueryable()
|
||||
.Where(r => r.RelatedId == userId);
|
||||
var totalCount = await query.CountAsync();
|
||||
var relationships = await query
|
||||
.Include(r => r.Related)
|
||||
.Include(r => r.Related.Profile)
|
||||
.Include(r => r.Account)
|
||||
.Include(r => r.Account.Profile)
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
@ -30,21 +33,37 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
|
||||
return relationships;
|
||||
}
|
||||
|
||||
public class RelationshipCreateRequest
|
||||
|
||||
[HttpGet("requests")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Relationship>>> ListSentRequests()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relationships = await db.AccountRelationships
|
||||
.Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending)
|
||||
.Include(r => r.Related)
|
||||
.Include(r => r.Related.Profile)
|
||||
.Include(r => r.Account)
|
||||
.Include(r => r.Account.Profile)
|
||||
.ToListAsync();
|
||||
|
||||
return relationships;
|
||||
}
|
||||
|
||||
public class RelationshipRequest
|
||||
{
|
||||
[Required] public long UserId { get; set; }
|
||||
[Required] public RelationshipStatus Status { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("{userId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> CreateRelationship([FromBody] RelationshipCreateRequest request)
|
||||
public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId, [FromBody] RelationshipRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.UserId);
|
||||
if (relatedUser is null) return BadRequest("Invalid related user");
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
@ -58,4 +77,105 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{userId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId, [FromBody] RelationshipRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
var relationship = await rels.UpdateRelationship(currentUser.Id, userId, request.Status);
|
||||
return relationship;
|
||||
}
|
||||
catch (ArgumentException err)
|
||||
{
|
||||
return NotFound(err.Message);
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{userId:guid}/friends")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
var relationship = await rels.SendFriendRequest(currentUser, relatedUser);
|
||||
return relationship;
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{userId:guid}/friends/accept")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
|
||||
if (relationship is null) return NotFound("Friend request was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
relationship = await rels.AcceptFriendRelationship(relationship);
|
||||
return relationship;
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{userId:guid}/friends/decline")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
|
||||
if (relationship is null) return NotFound("Friend request was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
relationship = await rels.AcceptFriendRelationship(relationship, status: RelationshipStatus.Blocked);
|
||||
return relationship;
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{userId:guid}/block")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> BlockUser(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
var relationship = await rels.BlockAccount(currentUser, relatedUser);
|
||||
return relationship;
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +1,31 @@
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCache cache)
|
||||
public class RelationshipService(AppDatabase db, IMemoryCache cache)
|
||||
{
|
||||
public async Task<bool> HasExistingRelationship(Account userA, Account userB)
|
||||
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
||||
{
|
||||
var count = await db.AccountRelationships
|
||||
.Where(r => (r.AccountId == userA.Id && r.AccountId == userB.Id) ||
|
||||
(r.AccountId == userB.Id && r.AccountId == userA.Id))
|
||||
.Where(r => (r.AccountId == accountId && r.RelatedId == relatedId) ||
|
||||
(r.AccountId == relatedId && r.AccountId == accountId))
|
||||
.CountAsync();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task<Relationship?> GetRelationship(
|
||||
Account account,
|
||||
Account related,
|
||||
RelationshipStatus? status,
|
||||
Guid accountId,
|
||||
Guid relatedId,
|
||||
RelationshipStatus? status = null,
|
||||
bool ignoreExpired = false
|
||||
)
|
||||
{
|
||||
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
var queries = db.AccountRelationships
|
||||
.Where(r => r.AccountId == account.Id && r.AccountId == related.Id);
|
||||
if (ignoreExpired) queries = queries.Where(r => r.ExpiredAt > now);
|
||||
var queries = db.AccountRelationships.AsQueryable()
|
||||
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId);
|
||||
if (!ignoreExpired) queries = queries.Where(r => r.ExpiredAt > now);
|
||||
if (status is not null) queries = queries.Where(r => r.Status == status);
|
||||
var relationship = await queries.FirstOrDefaultAsync();
|
||||
return relationship;
|
||||
@ -37,7 +36,7 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa
|
||||
if (status == RelationshipStatus.Pending)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot create relationship with pending status, use SendFriendRequest instead.");
|
||||
if (await HasExistingRelationship(sender, target))
|
||||
if (await HasExistingRelationship(sender.Id, target.Id))
|
||||
throw new InvalidOperationException("Found existing relationship between you and target user.");
|
||||
|
||||
var relationship = new Relationship
|
||||
@ -49,17 +48,23 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa
|
||||
|
||||
db.AccountRelationships.Add(relationship);
|
||||
await db.SaveChangesAsync();
|
||||
await ApplyRelationshipPermissions(relationship);
|
||||
|
||||
|
||||
cache.Remove($"UserFriends_{relationship.AccountId}");
|
||||
cache.Remove($"UserFriends_{relationship.RelatedId}");
|
||||
|
||||
return relationship;
|
||||
}
|
||||
|
||||
public async Task<Relationship> BlockAccount(Account sender, Account target)
|
||||
{
|
||||
if (await HasExistingRelationship(sender.Id, target.Id))
|
||||
return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked);
|
||||
return await CreateRelationship(sender, target, RelationshipStatus.Blocked);
|
||||
}
|
||||
|
||||
public async Task<Relationship> SendFriendRequest(Account sender, Account target)
|
||||
{
|
||||
if (await HasExistingRelationship(sender, target))
|
||||
if (await HasExistingRelationship(sender.Id, target.Id))
|
||||
throw new InvalidOperationException("Found existing relationship between you and target user.");
|
||||
|
||||
var relationship = new Relationship
|
||||
@ -81,7 +86,9 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa
|
||||
RelationshipStatus status = RelationshipStatus.Friends
|
||||
)
|
||||
{
|
||||
if (relationship.Status == RelationshipStatus.Pending)
|
||||
if (relationship.Status != RelationshipStatus.Pending)
|
||||
throw new ArgumentException("Cannot accept friend request that not in pending status.");
|
||||
if (status == RelationshipStatus.Pending)
|
||||
throw new ArgumentException("Cannot accept friend request by setting the new status to pending.");
|
||||
|
||||
// Whatever the receiver decides to apply which status to the relationship,
|
||||
@ -100,27 +107,22 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await Task.WhenAll(
|
||||
ApplyRelationshipPermissions(relationship),
|
||||
ApplyRelationshipPermissions(relationshipBackward)
|
||||
);
|
||||
|
||||
cache.Remove($"UserFriends_{relationship.AccountId}");
|
||||
cache.Remove($"UserFriends_{relationship.RelatedId}");
|
||||
|
||||
return relationshipBackward;
|
||||
}
|
||||
|
||||
public async Task<Relationship> UpdateRelationship(Account account, Account related, RelationshipStatus status)
|
||||
public async Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status)
|
||||
{
|
||||
var relationship = await GetRelationship(account, related, status);
|
||||
var relationship = await GetRelationship(accountId, relatedId, status);
|
||||
if (relationship is null) throw new ArgumentException("There is no relationship between you and the user.");
|
||||
if (relationship.Status == status) return relationship;
|
||||
relationship.Status = status;
|
||||
db.Update(relationship);
|
||||
await db.SaveChangesAsync();
|
||||
await ApplyRelationshipPermissions(relationship);
|
||||
cache.Remove($"UserFriends_{related.Id}");
|
||||
cache.Remove($"UserFriends_{accountId}");
|
||||
cache.Remove($"UserFriends_{relatedId}");
|
||||
return relationship;
|
||||
}
|
||||
|
||||
@ -139,27 +141,10 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa
|
||||
return friends ?? [];
|
||||
}
|
||||
|
||||
private async Task ApplyRelationshipPermissions(Relationship relationship)
|
||||
public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
||||
RelationshipStatus status = RelationshipStatus.Friends)
|
||||
{
|
||||
// Apply the relationship permissions to casbin enforcer
|
||||
// domain: the user
|
||||
// status is friends: all permissions are allowed by default, expect specially specified
|
||||
// status is blocked: all permissions are disallowed by default, expect specially specified
|
||||
// others: use the default permissions by design
|
||||
|
||||
var domain = $"user:{relationship.AccountId.ToString()}";
|
||||
var target = $"user:{relationship.RelatedId.ToString()}";
|
||||
|
||||
await pm.RemovePermissionNode(target, domain, "*");
|
||||
|
||||
bool? value = relationship.Status switch
|
||||
{
|
||||
RelationshipStatus.Friends => true,
|
||||
RelationshipStatus.Blocked => false,
|
||||
_ => null,
|
||||
};
|
||||
if (value is null) return;
|
||||
|
||||
await pm.AddPermissionNode(target, domain, "*", value);
|
||||
var relationship = await GetRelationship(accountId, relatedId, status);
|
||||
return relationship is not null;
|
||||
}
|
||||
}
|
@ -81,15 +81,15 @@ public class AppDatabase(
|
||||
{
|
||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(configuration.GetConnectionString("App"));
|
||||
dataSourceBuilder.EnableDynamicJson();
|
||||
dataSourceBuilder.UseNetTopologySuite();
|
||||
dataSourceBuilder.UseNodaTime();
|
||||
var dataSource = dataSourceBuilder.Build();
|
||||
|
||||
optionsBuilder.UseNpgsql(
|
||||
dataSource,
|
||||
dataSourceBuilder.Build(),
|
||||
opt => opt
|
||||
.UseNodaTime()
|
||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||
.UseNetTopologySuite()
|
||||
.UseNodaTime()
|
||||
).UseSnakeCaseNamingConvention();
|
||||
|
||||
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||
|
77
DysonNetwork.Sphere/Auth/Auth.cs
Normal file
77
DysonNetwork.Sphere/Auth/Auth.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public static class AuthConstants
|
||||
{
|
||||
public const string SchemeName = "DysonToken";
|
||||
public const string TokenQueryParamName = "tk";
|
||||
}
|
||||
|
||||
public class DysonTokenAuthOptions : AuthenticationSchemeOptions;
|
||||
|
||||
public class DysonTokenAuthHandler : AuthenticationHandler<DysonTokenAuthOptions>
|
||||
{
|
||||
private TokenValidationParameters _tokenValidationParameters;
|
||||
|
||||
public DysonTokenAuthHandler(
|
||||
IOptionsMonitor<DysonTokenAuthOptions> options,
|
||||
IConfiguration configuration,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
var publicKey = File.ReadAllText(configuration["Jwt:PublicKeyPath"]!);
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKey);
|
||||
_tokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = "solar-network",
|
||||
IssuerSigningKey = new RsaSecurityKey(rsa)
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var token = _ExtractToken(Request);
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
return AuthenticateResult.Fail("No token was provided.");
|
||||
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var principal = tokenHandler.ValidateToken(token, _tokenValidationParameters, out var validatedToken);
|
||||
|
||||
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return AuthenticateResult.Fail($"Authentication failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? _ExtractToken(HttpRequest request)
|
||||
{
|
||||
if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken))
|
||||
return queryToken;
|
||||
|
||||
var authHeader = request.Headers.Authorization.ToString();
|
||||
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
return authHeader["Bearer ".Length..].Trim();
|
||||
|
||||
return request.Cookies.TryGetValue(AuthConstants.TokenQueryParamName, out var cookieToken)
|
||||
? cookieToken
|
||||
: null;
|
||||
}
|
||||
}
|
@ -66,11 +66,11 @@ public class AuthController(
|
||||
|
||||
await db.AuthChallenges.AddAsync(challenge);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
|
||||
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request
|
||||
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
|
||||
);
|
||||
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ public class AuthController(
|
||||
[FromBody] PerformChallengeRequest request
|
||||
)
|
||||
{
|
||||
var challenge = await db.AuthChallenges.FindAsync(id);
|
||||
var challenge = await db.AuthChallenges.Include(e => e.Account).FirstOrDefaultAsync(e => e.Id == id);
|
||||
if (challenge is null) return NotFound("Auth challenge was not found.");
|
||||
|
||||
var factor = await db.AccountAuthFactors.FindAsync(request.FactorId);
|
||||
@ -133,10 +133,11 @@ public class AuthController(
|
||||
challenge.BlacklistFactors.Add(factor.Id);
|
||||
db.Update(challenge);
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||
new Dictionary<string, object> {
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", challenge.Id },
|
||||
{ "factor_id", factor.Id }
|
||||
}, Request
|
||||
}, Request, challenge.Account
|
||||
);
|
||||
}
|
||||
else
|
||||
@ -149,10 +150,11 @@ public class AuthController(
|
||||
challenge.FailedAttempts++;
|
||||
db.Update(challenge);
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||
new Dictionary<string, object> {
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", challenge.Id },
|
||||
{ "factor_id", factor.Id }
|
||||
}, Request
|
||||
}, Request, challenge.Account
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
return BadRequest("Invalid password.");
|
||||
@ -161,10 +163,11 @@ public class AuthController(
|
||||
if (challenge.StepRemain == 0)
|
||||
{
|
||||
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||
new Dictionary<string, object> {
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", challenge.Id },
|
||||
{ "account_id", challenge.AccountId }
|
||||
}, Request
|
||||
}, Request, challenge.Account
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ using NetTopologySuite.Geometries;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using NpgsqlTypes;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
@ -137,7 +137,7 @@ public class PostController(
|
||||
[MaxLength(1024)] public string? Title { get; set; }
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
public string? Content { get; set; }
|
||||
public PostVisibility? Visibility { get; set; }
|
||||
public PostVisibility? Visibility { get; set; } = PostVisibility.Public;
|
||||
public PostType? Type { get; set; }
|
||||
[MaxLength(16)] public List<string>? Tags { get; set; }
|
||||
[MaxLength(8)] public List<string>? Categories { get; set; }
|
||||
|
@ -54,13 +54,15 @@ builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
}).AddDataAnnotationsLocalization(options => {
|
||||
}).AddDataAnnotationsLocalization(options =>
|
||||
{
|
||||
options.DataAnnotationLocalizerProvider = (type, factory) =>
|
||||
factory.Create(typeof(SharedResource));
|
||||
});
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
builder.Services.Configure<RequestLocalizationOptions>(options => {
|
||||
builder.Services.Configure<RequestLocalizationOptions>(options =>
|
||||
{
|
||||
var supportedCultures = new[]
|
||||
{
|
||||
new CultureInfo("en-us"),
|
||||
@ -82,21 +84,12 @@ builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed"
|
||||
}));
|
||||
builder.Services.AddCors();
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddAuthentication("Bearer").AddJwtBearer(options =>
|
||||
{
|
||||
var publicKey = File.ReadAllText(builder.Configuration["Jwt:PublicKeyPath"]!);
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKey);
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = "solar-network",
|
||||
IssuerSigningKey = new RsaSecurityKey(rsa)
|
||||
};
|
||||
});
|
||||
options.DefaultAuthenticateScheme = AuthConstants.SchemeName;
|
||||
options.DefaultChallengeScheme = AuthConstants.SchemeName;
|
||||
})
|
||||
.AddScheme<DysonTokenAuthOptions, DysonTokenAuthHandler>(AuthConstants.SchemeName, _ => { });
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
@ -155,7 +148,7 @@ builder.Services.AddScoped<EmailService>();
|
||||
builder.Services.AddScoped<PermissionService>();
|
||||
builder.Services.AddScoped<AccountService>();
|
||||
builder.Services.AddScoped<AccountEventService>();
|
||||
builder.Services.AddSingleton<ActionLogService>();
|
||||
builder.Services.AddScoped<ActionLogService>();
|
||||
builder.Services.AddScoped<RelationshipService>();
|
||||
builder.Services.AddScoped<MagicSpellService>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
@ -191,7 +184,7 @@ builder.Services.AddQuartz(q =>
|
||||
.WithIdentity("CloudFilesUnusedRecyclingTrigger")
|
||||
.WithSimpleSchedule(o => o.WithIntervalInHours(1).RepeatForever())
|
||||
);
|
||||
|
||||
|
||||
var actionLogFlushJob = new JobKey("ActionLogFlush");
|
||||
q.AddJob<ActionLogFlushJob>(opts => opts.WithIdentity(actionLogFlushJob));
|
||||
q.AddTrigger(opts => opts
|
||||
@ -283,34 +276,30 @@ app.MapTus("/files/tus", _ => Task.FromResult<DefaultTusConfiguration>(new()
|
||||
{
|
||||
using var scope = eventContext.HttpContext.RequestServices.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
|
||||
try
|
||||
{
|
||||
var httpContext = eventContext.HttpContext;
|
||||
if (httpContext.Items["CurrentUser"] is not Account user) return;
|
||||
|
||||
var file = await eventContext.GetFileAsync();
|
||||
var metadata = await file.GetMetadataAsync(eventContext.CancellationToken);
|
||||
var fileName = metadata.TryGetValue("filename", out var fn) ? fn.GetString(Encoding.UTF8) : "uploaded_file";
|
||||
var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null;
|
||||
|
||||
var fileStream = await file.GetContentAsync(eventContext.CancellationToken);
|
||||
|
||||
var fileService = services.GetRequiredService<FileService>();
|
||||
var info = await fileService.ProcessNewFileAsync(user, file.Id, fileStream, fileName, contentType);
|
||||
var httpContext = eventContext.HttpContext;
|
||||
if (httpContext.Items["CurrentUser"] is not Account user) return;
|
||||
|
||||
using var finalScope = eventContext.HttpContext.RequestServices.CreateScope();
|
||||
var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value.JsonSerializerOptions;
|
||||
var infoJson = JsonSerializer.Serialize(info, jsonOptions);
|
||||
eventContext.HttpContext.Response.Headers.Append("X-FileInfo", infoJson);
|
||||
var file = await eventContext.GetFileAsync();
|
||||
var metadata = await file.GetMetadataAsync(eventContext.CancellationToken);
|
||||
var fileName = metadata.TryGetValue("filename", out var fn)
|
||||
? fn.GetString(Encoding.UTF8)
|
||||
: "uploaded_file";
|
||||
var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null;
|
||||
|
||||
// Dispose the stream after all processing is complete
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
var fileStream = await file.GetContentAsync(eventContext.CancellationToken);
|
||||
|
||||
var fileService = services.GetRequiredService<FileService>();
|
||||
var info = await fileService.ProcessNewFileAsync(user, file.Id, fileStream, fileName, contentType);
|
||||
|
||||
using var finalScope = eventContext.HttpContext.RequestServices.CreateScope();
|
||||
var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value
|
||||
.JsonSerializerOptions;
|
||||
var infoJson = JsonSerializer.Serialize(info, jsonOptions);
|
||||
eventContext.HttpContext.Response.Headers.Append("X-FileInfo", infoJson);
|
||||
|
||||
// Dispose the stream after all processing is complete
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
@ -1,6 +1,7 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationMiddleware_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003F2f_003F7ab1cc57_003FAuthenticationMiddleware_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003Ff0_003F595b6eda_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthorizationAppBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ff26593f91746d7a53418a46dc419d1f200_003F4b_003F56550da2_003FAuthorizationAppBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABodyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc5c8aba04a29d49c65d772c9ffcd93ac7eb38ccbb49a5f506518a0b9bdcaa75_003FBodyBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -35,6 +36,8 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aa8ac544afb487082402c1fa422910f2e00_003F7f_003F8e728ed6_003FIStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializerOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5703920a18f94462b4354fab05326e6519a200_003F35_003F8536fc49_003FJsonSerializerOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff611a1225a3445458f2ca3f102eed5bdcd10_003F07_003F030df6ba_003FJwtBearerExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtSecurityTokenHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F477051138f1f40de9077b7b1cdc55c6215fb0_003Ff5_003Fd716e016_003FJwtSecurityTokenHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKestrelServerLimits_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e2e5dfcafad4407b569dd5df56a2fbf274e00_003Fa4_003F39445f62_003FKestrelServerLimits_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKnownResamplers_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003Fb3_003Fcdb3e080_003FKnownResamplers_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMailboxAddress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8e03e47c46b7469f97abc40667cbcf9b133000_003Fa6_003F83324248_003FMailboxAddress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -55,6 +58,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASafeHandle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F66_003Fde27c365_003FSafeHandle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASecuritySchemeType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F29898ce74e3763a786ac1bd9a6db2152e1af75769440b1e53b9cbdf1dda1bd99_003FSecuritySchemeType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc0e30e11d8f5456cb7a11b21ebee6c5a35c00_003F60_003F78b485f5_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fce37be1a06b16c6faa02038d2cc477dd3bca5b217ceeb41c5f2ad45c1bf9_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASetPropertyCalls_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F458b5f22476b4599b87176214d5e4026c2327b148f4d3f885ee92362b4dac3_003FSetPropertyCalls_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASourceCustom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdaa8d9c408cd4b4286bbef7e35f1a42e31c00_003F45_003F5839ca6c_003FSourceCustom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Loading…
x
Reference in New Issue
Block a user