♻️ Extract the Developer to new service, add PublisherServiceGrpc
This commit is contained in:
@@ -10,7 +10,7 @@ namespace DysonNetwork.Sphere.Activity;
|
||||
|
||||
public class ActivityService(
|
||||
AppDatabase db,
|
||||
PublisherService pub,
|
||||
Publisher.PublisherService pub,
|
||||
PostService ps,
|
||||
DiscoveryService ds,
|
||||
AccountService.AccountServiceClient accounts
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
@@ -61,9 +60,6 @@ public class AppDatabase(
|
||||
public DbSet<StickerPack> StickerPacks { get; set; }
|
||||
public DbSet<StickerPackOwnership> StickerPackOwnerships { get; set; }
|
||||
|
||||
public DbSet<CustomApp> CustomApps { get; set; }
|
||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
|
||||
|
||||
public DbSet<WebReader.WebArticle> WebArticles { get; set; }
|
||||
public DbSet<WebReader.WebFeed> WebFeeds { get; set; }
|
||||
|
||||
@@ -103,16 +99,6 @@ public class AppDatabase(
|
||||
.HasIndex(p => p.SearchVector)
|
||||
.HasMethod("GIN");
|
||||
|
||||
modelBuilder.Entity<CustomAppSecret>()
|
||||
.HasIndex(s => s.Secret)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<CustomApp>()
|
||||
.HasMany(c => c.Secrets)
|
||||
.WithOne(s => s.App)
|
||||
.HasForeignKey(s => s.AppId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasOne(p => p.RepliedPost)
|
||||
.WithMany()
|
||||
|
@@ -1,68 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
|
||||
public enum CustomAppStatus
|
||||
{
|
||||
Developing,
|
||||
Staging,
|
||||
Production,
|
||||
Suspended
|
||||
}
|
||||
|
||||
public class CustomApp : ModelBase, IIdentifiedResource
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Slug { get; set; } = null!;
|
||||
[MaxLength(1024)] public string Name { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
|
||||
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
|
||||
public Guid PublisherId { get; set; }
|
||||
public Publisher.Publisher Developer { get; set; } = null!;
|
||||
|
||||
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
|
||||
}
|
||||
|
||||
public class CustomAppLinks
|
||||
{
|
||||
[MaxLength(8192)] public string? HomePage { get; set; }
|
||||
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
|
||||
[MaxLength(8192)] public string? TermsOfService { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAppOauthConfig
|
||||
{
|
||||
[MaxLength(1024)] public string? ClientUri { get; set; }
|
||||
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
||||
[MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
|
||||
[MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"];
|
||||
[MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
|
||||
public bool RequirePkce { get; set; } = true;
|
||||
public bool AllowOfflineAccess { get; set; } = false;
|
||||
}
|
||||
|
||||
public class CustomAppSecret : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Secret { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Description { get; set; } = null!;
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth
|
||||
|
||||
public Guid AppId { get; set; }
|
||||
public CustomApp App { get; set; } = null!;
|
||||
}
|
@@ -1,129 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/apps")]
|
||||
public class CustomAppController(CustomAppService customApps, PublisherService ps) : ControllerBase
|
||||
{
|
||||
public record CustomAppRequest(
|
||||
[MaxLength(1024)] string? Slug,
|
||||
[MaxLength(1024)] string? Name,
|
||||
[MaxLength(4096)] string? Description,
|
||||
string? PictureId,
|
||||
string? BackgroundId,
|
||||
CustomAppStatus? Status,
|
||||
CustomAppLinks? Links,
|
||||
CustomAppOauthConfig? OauthConfig
|
||||
);
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListApps([FromRoute] string pubName)
|
||||
{
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
var apps = await customApps.GetAppsByPublisherAsync(publisher.Id);
|
||||
return Ok(apps);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetApp([FromRoute] string pubName, Guid id)
|
||||
{
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(app);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CreateApp([FromRoute] string pubName, [FromBody] CustomAppRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug))
|
||||
return BadRequest("Name and slug are required");
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to create a custom app");
|
||||
if (!await ps.HasFeature(publisher.Id, PublisherFeatureFlag.Develop))
|
||||
return StatusCode(403, "Publisher must be a developer to create a custom app");
|
||||
|
||||
try
|
||||
{
|
||||
var app = await customApps.CreateAppAsync(publisher, request);
|
||||
return Ok(app);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{id:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> UpdateApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] CustomAppRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to update a custom app");
|
||||
|
||||
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
app = await customApps.UpdateAppAsync(app, request);
|
||||
return Ok(app);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DeleteApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid id
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to delete a custom app");
|
||||
|
||||
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var result = await customApps.DeleteAppAsync(id);
|
||||
if (!result)
|
||||
return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
@@ -1,172 +0,0 @@
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
|
||||
public class CustomAppService(
|
||||
AppDatabase db,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
FileService.FileServiceClient files
|
||||
)
|
||||
{
|
||||
public async Task<CustomApp?> CreateAppAsync(
|
||||
Publisher.Publisher pub,
|
||||
CustomAppController.CustomAppRequest request
|
||||
)
|
||||
{
|
||||
var app = new CustomApp
|
||||
{
|
||||
Slug = request.Slug!,
|
||||
Name = request.Name!,
|
||||
Description = request.Description,
|
||||
Status = request.Status ?? CustomAppStatus.Developing,
|
||||
Links = request.Links,
|
||||
OauthConfig = request.OauthConfig,
|
||||
PublisherId = pub.Id
|
||||
};
|
||||
|
||||
if (request.PictureId is not null)
|
||||
{
|
||||
var picture = await files.GetFileAsync(
|
||||
new GetFileRequest
|
||||
{
|
||||
Id = request.PictureId
|
||||
}
|
||||
);
|
||||
if (picture is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = picture.Id,
|
||||
Usage = "custom-apps.picture",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
}
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
var background = await files.GetFileAsync(
|
||||
new GetFileRequest { Id = request.BackgroundId }
|
||||
);
|
||||
if (background is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = CloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = background.Id,
|
||||
Usage = "custom-apps.background",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
db.CustomApps.Add(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? publisherId = null)
|
||||
{
|
||||
var query = db.CustomApps.Where(a => a.Id == id).AsQueryable();
|
||||
if (publisherId.HasValue)
|
||||
query = query.Where(a => a.PublisherId == publisherId.Value);
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
|
||||
{
|
||||
return await db.CustomApps.Where(a => a.PublisherId == publisherId).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)
|
||||
{
|
||||
if (request.Slug is not null)
|
||||
app.Slug = request.Slug;
|
||||
if (request.Name is not null)
|
||||
app.Name = request.Name;
|
||||
if (request.Description is not null)
|
||||
app.Description = request.Description;
|
||||
if (request.Status is not null)
|
||||
app.Status = request.Status.Value;
|
||||
if (request.Links is not null)
|
||||
app.Links = request.Links;
|
||||
if (request.OauthConfig is not null)
|
||||
app.OauthConfig = request.OauthConfig;
|
||||
|
||||
if (request.PictureId is not null)
|
||||
{
|
||||
var picture = await files.GetFileAsync(
|
||||
new GetFileRequest
|
||||
{
|
||||
Id = request.PictureId
|
||||
}
|
||||
);
|
||||
if (picture is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = picture.Id,
|
||||
Usage = "custom-apps.picture",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
}
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
var background = await files.GetFileAsync(
|
||||
new GetFileRequest { Id = request.BackgroundId }
|
||||
);
|
||||
if (background is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = CloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = background.Id,
|
||||
Usage = "custom-apps.background",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
db.Update(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAppAsync(Guid id)
|
||||
{
|
||||
var app = await db.CustomApps.FindAsync(id);
|
||||
if (app == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
db.CustomApps.Remove(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers")]
|
||||
public class DeveloperController(
|
||||
AppDatabase db,
|
||||
PublisherService ps,
|
||||
ActionLogService.ActionLogServiceClient als
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<Publisher.Publisher>> GetDeveloper(string name)
|
||||
{
|
||||
var publisher = await db.Publishers
|
||||
.Where(e => e.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
return Ok(publisher);
|
||||
}
|
||||
|
||||
[HttpGet("{name}/stats")]
|
||||
public async Task<ActionResult<DeveloperStats>> GetDeveloperStats(string name)
|
||||
{
|
||||
var publisher = await db.Publishers
|
||||
.Where(p => p.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
// Check if publisher has developer feature
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var hasDeveloperFeature = await db.PublisherFeatures
|
||||
.Where(f => f.PublisherId == publisher.Id)
|
||||
.Where(f => f.Flag == PublisherFeatureFlag.Develop)
|
||||
.Where(f => f.ExpiredAt == null || f.ExpiredAt > now)
|
||||
.AnyAsync();
|
||||
|
||||
if (!hasDeveloperFeature) return NotFound("Not a developer account");
|
||||
|
||||
// Get custom apps count
|
||||
var customAppsCount = await db.CustomApps
|
||||
.Where(a => a.PublisherId == publisher.Id)
|
||||
.CountAsync();
|
||||
|
||||
var stats = new DeveloperStats
|
||||
{
|
||||
TotalCustomApps = customAppsCount
|
||||
};
|
||||
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Publisher.Publisher>>> ListJoinedDevelopers()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var members = await db.PublisherMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.JoinedAt != null)
|
||||
.Include(e => e.Publisher)
|
||||
.ToListAsync();
|
||||
|
||||
// Filter to only include publishers with the developer feature flag
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var publisherIds = members.Select(m => m.Publisher.Id).ToList();
|
||||
var developerPublisherIds = await db.PublisherFeatures
|
||||
.Where(f => publisherIds.Contains(f.PublisherId))
|
||||
.Where(f => f.Flag == PublisherFeatureFlag.Develop)
|
||||
.Where(f => f.ExpiredAt == null || f.ExpiredAt > now)
|
||||
.Select(f => f.PublisherId)
|
||||
.ToListAsync();
|
||||
|
||||
return members
|
||||
.Where(m => developerPublisherIds.Contains(m.Publisher.Id))
|
||||
.Select(m => m.Publisher)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
[HttpPost("{name}/enroll")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "developers.create")]
|
||||
public async Task<ActionResult<Publisher.Publisher>> EnrollDeveloperProgram(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var publisher = await db.Publishers
|
||||
.Where(p => p.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
// Check if the user is an owner of the publisher
|
||||
var isOwner = await db.PublisherMembers
|
||||
.AnyAsync(m =>
|
||||
m.PublisherId == publisher.Id &&
|
||||
m.AccountId == accountId &&
|
||||
m.Role == PublisherMemberRole.Owner &&
|
||||
m.JoinedAt != null);
|
||||
|
||||
if (!isOwner) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
|
||||
|
||||
// Check if already has a developer feature
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var hasDeveloperFeature = await db.PublisherFeatures
|
||||
.AnyAsync(f =>
|
||||
f.PublisherId == publisher.Id &&
|
||||
f.Flag == PublisherFeatureFlag.Develop &&
|
||||
(f.ExpiredAt == null || f.ExpiredAt > now));
|
||||
|
||||
if (hasDeveloperFeature) return BadRequest("Publisher is already in the developer program");
|
||||
|
||||
// Add developer feature flag
|
||||
var feature = new PublisherFeature
|
||||
{
|
||||
PublisherId = publisher.Id,
|
||||
Flag = PublisherFeatureFlag.Develop,
|
||||
ExpiredAt = null
|
||||
};
|
||||
|
||||
db.PublisherFeatures.Add(feature);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "developers.enroll",
|
||||
Meta =
|
||||
{
|
||||
{ "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(publisher.Id.ToString()) },
|
||||
{ "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(publisher.Name) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(publisher);
|
||||
}
|
||||
|
||||
public class DeveloperStats
|
||||
{
|
||||
public int TotalCustomApps { get; set; }
|
||||
}
|
||||
}
|
@@ -36,6 +36,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
|
||||
|
@@ -4,7 +4,6 @@ using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -397,20 +396,12 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
@@ -248,8 +247,6 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
|
||||
oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
|
||||
links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
|
||||
publisher_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
|
@@ -4,7 +4,6 @@ using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -397,20 +396,12 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
@@ -5,7 +5,6 @@ using System.Text.Json;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -399,20 +398,12 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
@@ -5,7 +5,6 @@ using System.Text.Json;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -399,20 +398,12 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
@@ -5,7 +5,6 @@ using System.Text.Json;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -396,20 +395,12 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
@@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.PageData;
|
||||
public class PostPageData(
|
||||
AppDatabase db,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
PublisherService pub,
|
||||
Publisher.PublisherService pub,
|
||||
PostService ps,
|
||||
IConfiguration configuration
|
||||
)
|
||||
|
@@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Poll;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/polls")]
|
||||
public class PollController(AppDatabase db, PollService polls, PublisherService pub) : ControllerBase
|
||||
public class PollController(AppDatabase db, PollService polls, Publisher.PublisherService pub) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<PollWithStats>> GetPoll(Guid id)
|
||||
@@ -85,7 +85,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (poll is null) return NotFound("Poll not found");
|
||||
|
||||
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, PublisherMemberRole.Viewer))
|
||||
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You need to be a viewer to view this poll's feedback.");
|
||||
|
||||
var answerQuery = db.PollAnswers
|
||||
@@ -186,7 +186,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
|
||||
|
||||
var publisher = await pub.GetPublisherByName(pubName);
|
||||
if (publisher is null) return BadRequest("Publisher was not found.");
|
||||
if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need at least be an editor to create polls as this publisher.");
|
||||
|
||||
var poll = new Poll
|
||||
@@ -231,7 +231,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
|
||||
if (poll == null) return NotFound("Poll not found");
|
||||
|
||||
// Check if user is an editor of the publisher that owns the poll
|
||||
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need to be at least an editor to update this poll.");
|
||||
|
||||
// Update properties if they are provided in the request
|
||||
@@ -294,7 +294,7 @@ public class PollController(AppDatabase db, PollService polls, PublisherService
|
||||
if (poll == null) return NotFound("Poll not found");
|
||||
|
||||
// Check if user is an editor of the publisher that owns the poll
|
||||
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need to be at least an editor to delete this poll.");
|
||||
|
||||
// Delete all answers for this poll
|
||||
|
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using PublisherService = DysonNetwork.Sphere.Publisher.PublisherService;
|
||||
|
||||
namespace DysonNetwork.Sphere.Post;
|
||||
|
||||
@@ -301,13 +302,13 @@ public class PostController(
|
||||
{
|
||||
// Use the first personal publisher
|
||||
publisher = await db.Publishers.FirstOrDefaultAsync(e =>
|
||||
e.AccountId == accountId && e.Type == PublisherType.Individual);
|
||||
e.AccountId == accountId && e.Type == Publisher.PublisherType.Individual);
|
||||
}
|
||||
else
|
||||
{
|
||||
publisher = await pub.GetPublisherByName(pubName);
|
||||
if (publisher is null) return BadRequest("Publisher was not found.");
|
||||
if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need at least be an editor to post as this publisher.");
|
||||
}
|
||||
|
||||
@@ -473,14 +474,14 @@ public class PostController(
|
||||
if (post is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await pub.IsMemberWithRole(post.Publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(post.Publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need at least be an editor to edit this publisher's post.");
|
||||
|
||||
if (pubName is not null)
|
||||
{
|
||||
var publisher = await pub.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need at least be an editor to transfer this post to this publisher.");
|
||||
post.PublisherId = publisher.Id;
|
||||
post.Publisher = publisher;
|
||||
@@ -552,7 +553,8 @@ public class PostController(
|
||||
.FirstOrDefaultAsync();
|
||||
if (post is null) return NotFound();
|
||||
|
||||
if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id),
|
||||
Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You need at least be an editor to delete the publisher's post.");
|
||||
|
||||
await ps.DeletePostAsync(post);
|
||||
|
@@ -161,7 +161,7 @@ public partial class PostService(
|
||||
{
|
||||
var sender = post.Publisher;
|
||||
using var scope = factory.CreateScope();
|
||||
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
|
||||
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
|
||||
var nty = scope.ServiceProvider.GetRequiredService<PusherService.PusherServiceClient>();
|
||||
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
|
||||
try
|
||||
@@ -455,7 +455,7 @@ public partial class PostService(
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using var scope = factory.CreateScope();
|
||||
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
|
||||
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
|
||||
var nty = scope.ServiceProvider.GetRequiredService<PusherService.PusherServiceClient>();
|
||||
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
|
||||
try
|
||||
|
@@ -5,6 +5,7 @@ using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
|
||||
using Account = DysonNetwork.Pass.Account.Account;
|
||||
|
||||
@@ -49,6 +50,49 @@ public class Publisher : ModelBase, IIdentifiedResource
|
||||
[NotMapped] public Account? Account { get; set; }
|
||||
|
||||
public string ResourceIdentifier => $"publisher:{Id}";
|
||||
|
||||
public Shared.Proto.Publisher ToProto(AppDatabase db)
|
||||
{
|
||||
var p = new Shared.Proto.Publisher()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Type = Type == PublisherType.Individual
|
||||
? Shared.Proto.PublisherType.Individual
|
||||
: Shared.Proto.PublisherType.Organizational,
|
||||
Name = Name,
|
||||
Nick = Nick,
|
||||
Bio = Bio,
|
||||
AccountId = AccountId?.ToString() ?? string.Empty,
|
||||
RealmId = RealmId?.ToString() ?? string.Empty,
|
||||
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
|
||||
UpdatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
|
||||
};
|
||||
if (Picture is not null)
|
||||
{
|
||||
p.Picture = new Shared.Proto.CloudFile
|
||||
{
|
||||
Id = Picture.Id,
|
||||
Name = Picture.Name,
|
||||
MimeType = Picture.MimeType,
|
||||
Hash = Picture.Hash,
|
||||
Size = Picture.Size,
|
||||
};
|
||||
}
|
||||
|
||||
if (Background is not null)
|
||||
{
|
||||
p.Background = new Shared.Proto.CloudFile
|
||||
{
|
||||
Id = Background.Id,
|
||||
Name = Background.Name,
|
||||
MimeType = Background.MimeType,
|
||||
Hash = Background.Hash,
|
||||
Size = Background.Size,
|
||||
};
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PublisherMemberRole
|
||||
@@ -68,6 +112,25 @@ public class PublisherMember : ModelBase
|
||||
|
||||
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
|
||||
public Instant? JoinedAt { get; set; }
|
||||
|
||||
|
||||
public Shared.Proto.PublisherMember ToProto()
|
||||
{
|
||||
return new Shared.Proto.PublisherMember()
|
||||
{
|
||||
PublisherId = PublisherId.ToString(),
|
||||
AccountId = AccountId.ToString(),
|
||||
Role = Role switch
|
||||
{
|
||||
PublisherMemberRole.Owner => Shared.Proto.PublisherMemberRole.Owner,
|
||||
PublisherMemberRole.Manager => Shared.Proto.PublisherMemberRole.Manager,
|
||||
PublisherMemberRole.Editor => Shared.Proto.PublisherMemberRole.Editor,
|
||||
PublisherMemberRole.Viewer => Shared.Proto.PublisherMemberRole.Viewer,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(Role), Role, null)
|
||||
},
|
||||
JoinedAt = JoinedAt?.ToTimestamp()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum PublisherSubscriptionStatus
|
||||
|
68
DysonNetwork.Sphere/Publisher/PublisherServiceGrpc.cs
Normal file
68
DysonNetwork.Sphere/Publisher/PublisherServiceGrpc.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Publisher;
|
||||
|
||||
public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
||||
: Shared.Proto.PublisherService.PublisherServiceBase
|
||||
{
|
||||
public override async Task<GetPublisherResponse> GetPublisher(GetPublisherRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var p = await service.GetPublisherByName(request.Name);
|
||||
if (p is null) throw new RpcException(new Status(StatusCode.NotFound, "publisher not found"));
|
||||
return new GetPublisherResponse { Publisher = p.ToProto(db) };
|
||||
}
|
||||
|
||||
public override async Task<ListPublishersResponse> ListPublishers(ListPublishersRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
IQueryable<Publisher> query = db.Publishers.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(request.AccountId) && Guid.TryParse(request.AccountId, out var aid))
|
||||
{
|
||||
var ids = await db.PublisherMembers.Where(m => m.AccountId == aid).Select(m => m.PublisherId).ToListAsync();
|
||||
query = query.Where(p => ids.Contains(p.Id));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
|
||||
{
|
||||
query = query.Where(p => p.RealmId == rid);
|
||||
}
|
||||
|
||||
var list = await query.Take(request.PageSize > 0 ? request.PageSize : 100).ToListAsync();
|
||||
var resp = new ListPublishersResponse();
|
||||
resp.Publishers.AddRange(list.Select(p => p.ToProto(db)));
|
||||
resp.TotalSize = list.Count;
|
||||
return resp;
|
||||
}
|
||||
|
||||
public override async Task<ListPublisherMembersResponse> ListPublisherMembers(ListPublisherMembersRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
if (!Guid.TryParse(request.PublisherId, out var pid))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
|
||||
var members = await service.GetPublisherMembers(pid);
|
||||
var resp = new ListPublisherMembersResponse();
|
||||
resp.Members.AddRange(members.Select(m => m.ToProto()));
|
||||
return resp;
|
||||
}
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.StringValue> SetPublisherFeatureFlag(
|
||||
SetPublisherFeatureFlagRequest request, ServerCallContext context)
|
||||
{
|
||||
if (!Guid.TryParse(request.PublisherId, out var pid))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
|
||||
await service.SetFeatureFlag(pid, request.Flag);
|
||||
return new Google.Protobuf.WellKnownTypes.StringValue { Value = request.Flag };
|
||||
}
|
||||
|
||||
public override async Task<HasPublisherFeatureResponse> HasPublisherFeature(HasPublisherFeatureRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
if (!Guid.TryParse(request.PublisherId, out var pid))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
|
||||
var enabled = await service.HasFeature(pid, request.Flag);
|
||||
return new HasPublisherFeatureResponse { Enabled = enabled };
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Prometheus;
|
||||
|
||||
@@ -29,6 +30,7 @@ public static class ApplicationConfiguration
|
||||
|
||||
// Map gRPC services
|
||||
app.MapGrpcService<WebSocketHandlerGrpc>();
|
||||
app.MapGrpcService<PublisherServiceGrpc>();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@ using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.GeoIp;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.Discovery;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
using DysonNetwork.Sphere.Translation;
|
||||
@@ -169,7 +168,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<WebReaderService>();
|
||||
services.AddScoped<WebFeedService>();
|
||||
services.AddScoped<DiscoveryService>();
|
||||
services.AddScoped<CustomAppService>();
|
||||
services.AddScoped<PollService>();
|
||||
|
||||
var translationProvider = configuration["Translation:Provider"]?.ToLower();
|
||||
|
@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -16,7 +15,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
|
||||
private async Task<IActionResult> _CheckStickerPackPermissions(
|
||||
Guid packId,
|
||||
Account currentUser,
|
||||
PublisherMemberRole requiredRole
|
||||
Publisher.PublisherMemberRole requiredRole
|
||||
)
|
||||
{
|
||||
var pack = await db.StickerPacks
|
||||
@@ -149,7 +148,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
|
||||
.FirstOrDefaultAsync(m => m.AccountId == accountId && m.PublisherId == pack.PublisherId);
|
||||
if (member is null)
|
||||
return StatusCode(403, "You are not a member of this publisher");
|
||||
if (member.Role < PublisherMemberRole.Editor)
|
||||
if (member.Role < Publisher.PublisherMemberRole.Editor)
|
||||
return StatusCode(403, "You need to be at least an editor to update sticker packs");
|
||||
|
||||
if (request.Name is not null)
|
||||
@@ -181,7 +180,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
|
||||
.FirstOrDefaultAsync(m => m.AccountId == accountId && m.PublisherId == pack.PublisherId);
|
||||
if (member is null)
|
||||
return StatusCode(403, "You are not a member of this publisher");
|
||||
if (member.Role < PublisherMemberRole.Editor)
|
||||
if (member.Role < Publisher.PublisherMemberRole.Editor)
|
||||
return StatusCode(403, "You need to be an editor to delete sticker packs");
|
||||
|
||||
await st.DeleteStickerPackAsync(pack);
|
||||
@@ -242,7 +241,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, PublisherMemberRole.Editor);
|
||||
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, Publisher.PublisherMemberRole.Editor);
|
||||
if (permissionCheck is not OkResult)
|
||||
return permissionCheck;
|
||||
|
||||
@@ -278,7 +277,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, PublisherMemberRole.Editor);
|
||||
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, Publisher.PublisherMemberRole.Editor);
|
||||
if (permissionCheck is not OkResult)
|
||||
return permissionCheck;
|
||||
|
||||
@@ -309,7 +308,7 @@ public class StickerController(AppDatabase db, StickerService st, FileService.Fi
|
||||
if (request.ImageId is null)
|
||||
return BadRequest("Image is required.");
|
||||
|
||||
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, PublisherMemberRole.Editor);
|
||||
var permissionCheck = await _CheckStickerPackPermissions(packId, currentUser, Publisher.PublisherMemberRole.Editor);
|
||||
if (permissionCheck is not OkResult)
|
||||
return permissionCheck;
|
||||
|
||||
|
@@ -9,7 +9,7 @@ namespace DysonNetwork.Sphere.WebReader;
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("/api/publishers/{pubName}/feeds")]
|
||||
public class WebFeedController(WebFeedService webFeed, PublisherService ps) : ControllerBase
|
||||
public class WebFeedController(WebFeedService webFeed, Publisher.PublisherService ps) : ControllerBase
|
||||
{
|
||||
public record WebFeedRequest(
|
||||
[MaxLength(8192)] string? Url,
|
||||
@@ -53,7 +53,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to create a web feed");
|
||||
|
||||
var feed = await webFeed.CreateWebFeedAsync(publisher, request);
|
||||
@@ -70,7 +70,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to update a web feed");
|
||||
|
||||
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
|
||||
@@ -91,7 +91,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to delete a web feed");
|
||||
|
||||
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
|
||||
@@ -114,7 +114,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor))
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to scrape a web feed");
|
||||
|
||||
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
|
||||
|
Reference in New Issue
Block a user