✨ Done mixing
This commit is contained in:
1967
DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs
generated
Normal file
1967
DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Pass.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReinitalMigration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "background_id",
|
||||
table: "account_profiles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "picture_id",
|
||||
table: "account_profiles");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "background_id",
|
||||
table: "account_profiles",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "picture_id",
|
||||
table: "account_profiles",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
@@ -392,11 +392,6 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
b.Property<string>("BackgroundId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("background_id");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
@@ -451,11 +446,6 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("picture_id");
|
||||
|
||||
b.Property<string>("Pronouns")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using DysonNetwork.Pass;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Startup;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
|
154
DysonNetwork.Pass/Safety/AbuseReportController.cs
Normal file
154
DysonNetwork.Pass/Safety/AbuseReportController.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Pass.Safety;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/safety/reports")]
|
||||
public class AbuseReportController(
|
||||
SafetyService safety
|
||||
) : ControllerBase
|
||||
{
|
||||
public class CreateReportRequest
|
||||
{
|
||||
[Required] public string ResourceIdentifier { get; set; } = null!;
|
||||
|
||||
[Required] public AbuseReportType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(10)]
|
||||
[MaxLength(1000)]
|
||||
public string Reason { get; set; } = null!;
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[Authorize]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<AbuseReport>> CreateReport([FromBody] CreateReportRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
var report = await safety.CreateReport(
|
||||
request.ResourceIdentifier,
|
||||
request.Type,
|
||||
request.Reason,
|
||||
currentUser.Id
|
||||
);
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<List<AbuseReport>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<AbuseReport>>> GetReports(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] bool includeResolved = false
|
||||
)
|
||||
{
|
||||
var totalCount = await safety.CountReports(includeResolved);
|
||||
var reports = await safety.GetReports(offset, take, includeResolved);
|
||||
Response.Headers["X-Total"] = totalCount.ToString();
|
||||
return Ok(reports);
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
[ProducesResponseType<List<AbuseReport>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<AbuseReport>>> GetMyReports(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] bool includeResolved = false
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var totalCount = await safety.CountUserReports(currentUser.Id, includeResolved);
|
||||
var reports = await safety.GetUserReports(currentUser.Id, offset, take, includeResolved);
|
||||
Response.Headers["X-Total"] = totalCount.ToString();
|
||||
return Ok(reports);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AbuseReport>> GetReportById(Guid id)
|
||||
{
|
||||
var report = await safety.GetReportById(id);
|
||||
return report == null ? NotFound() : Ok(report);
|
||||
}
|
||||
|
||||
[HttpGet("me/{id}")]
|
||||
[Authorize]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<AbuseReport>> GetMyReportById(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var report = await safety.GetReportById(id);
|
||||
if (report == null) return NotFound();
|
||||
|
||||
// Ensure the user only accesses their own reports
|
||||
if (report.AccountId != currentUser.Id) return Forbid();
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
public class ResolveReportRequest
|
||||
{
|
||||
[Required]
|
||||
[MinLength(5)]
|
||||
[MaxLength(1000)]
|
||||
public string Resolution { get; set; } = null!;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/resolve")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.resolve")]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await safety.ResolveReport(id, request.Resolution);
|
||||
return Ok(report);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("count")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<object>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<object>> GetReportsCount()
|
||||
{
|
||||
var count = await safety.GetPendingReportsCount();
|
||||
return Ok(new { pendingCount = count });
|
||||
}
|
||||
}
|
105
DysonNetwork.Pass/Safety/SafetyService.cs
Normal file
105
DysonNetwork.Pass/Safety/SafetyService.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using DysonNetwork.Pass.Account;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Safety;
|
||||
|
||||
public class SafetyService(AppDatabase db, ILogger<SafetyService> logger)
|
||||
{
|
||||
public async Task<AbuseReport> CreateReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId)
|
||||
{
|
||||
// Check if a similar report already exists from this user
|
||||
var existingReport = await db.AbuseReports
|
||||
.Where(r => r.ResourceIdentifier == resourceIdentifier &&
|
||||
r.AccountId == accountId &&
|
||||
r.DeletedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingReport != null)
|
||||
{
|
||||
throw new InvalidOperationException("You have already reported this content.");
|
||||
}
|
||||
|
||||
var report = new AbuseReport
|
||||
{
|
||||
ResourceIdentifier = resourceIdentifier,
|
||||
Type = type,
|
||||
Reason = reason,
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
db.AbuseReports.Add(report);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.LogInformation("New abuse report created: {ReportId} for resource {ResourceId}",
|
||||
report.Id, resourceIdentifier);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<int> CountReports(bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<int> CountUserReports(Guid accountId, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.AccountId == accountId)
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetReports(int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.Include(r => r.Account)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetUserReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.AccountId == accountId)
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AbuseReport?> GetReportById(Guid id)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Include(r => r.Account)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport> ResolveReport(Guid id, string resolution)
|
||||
{
|
||||
var report = await db.AbuseReports.FindAsync(id);
|
||||
if (report == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Report not found");
|
||||
}
|
||||
|
||||
report.ResolvedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
report.Resolution = resolution;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<int> GetPendingReportsCount()
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
}
|
@@ -217,7 +217,7 @@ public class PaymentService(
|
||||
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
||||
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
|
||||
readableOrderRemark],
|
||||
IsSavable = false
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -360,7 +360,7 @@ public class SubscriptionService(
|
||||
Topic = "subscriptions.begun",
|
||||
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
|
||||
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
||||
IsSavable = false,
|
||||
IsSavable = true
|
||||
};
|
||||
notification.Meta.Add("subscription_id", Value.ForString(subscription.Id.ToString()));
|
||||
await pusher.SendPushNotificationToUserAsync(
|
||||
|
Reference in New Issue
Block a user