Compare commits

...

3 Commits

Author SHA1 Message Date
554f73b550 🔨 Add develop build 2025-08-07 20:33:37 +08:00
ee8e9df12e Complete the develop service 2025-08-07 20:30:34 +08:00
00cdd1bc5d ♻️ Extract the Developer to new service, add PublisherServiceGrpc 2025-08-07 17:16:38 +08:00
40 changed files with 1154 additions and 452 deletions

View File

@@ -1,158 +1,189 @@
name: Build and Push Microservices
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build-sphere:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Sphere Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Sphere/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest
platforms: linux/amd64
build-pass:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Pass Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Pass/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pass:latest
platforms: linux/amd64
build-pusher:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Pusher Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Pusher/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pusher:latest
platforms: linux/amd64
build-drive:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Drive Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Drive/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-drive:latest
platforms: linux/amd64
build-gateway:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Gateway Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Gateway/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-gateway:latest
platforms: linux/amd64
name: Build and Push Microservices
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build-sphere:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Sphere Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Sphere/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest
platforms: linux/amd64
build-pass:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Pass Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Pass/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pass:latest
platforms: linux/amd64
build-pusher:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Pusher Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Pusher/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pusher:latest
platforms: linux/amd64
build-drive:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Drive Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Drive/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-drive:latest
platforms: linux/amd64
build-gateway:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Gateway Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Gateway/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-gateway:latest
platforms: linux/amd64
build-develop:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup NBGV
uses: dotnet/nbgv@master
id: nbgv
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push DysonNetwork.Develop Docker image
uses: docker/build-push-action@v6
with:
file: DysonNetwork.Develop/Dockerfile
context: .
push: true
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-develop:latest
platforms: linux/amd64

View File

@@ -0,0 +1,33 @@
using DysonNetwork.Develop.Identity;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Develop;
public class AppDatabase(
DbContextOptions<AppDatabase> options,
IConfiguration configuration
) : DbContext(options)
{
public DbSet<Developer> Developers { get; set; }
public DbSet<CustomApp> CustomApps { get; set; }
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(
configuration.GetConnectionString("App"),
opt => opt
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
.UseNodaTime()
).UseSnakeCaseNamingConvention();
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"]
RUN dotnet restore "DysonNetwork.Develop/DysonNetwork.Develop.csproj"
COPY . .
WORKDIR "/src/DysonNetwork.Develop"
RUN dotnet build "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DysonNetwork.Develop.dll"]

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
<PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
public enum CustomAppStatus
{
@@ -31,8 +31,8 @@ public class CustomApp : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
public Guid PublisherId { get; set; }
public Publisher.Publisher Developer { get; set; } = null!;
public Guid DeveloperId { get; set; }
public Developer Developer { get; set; } = null!;
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
}

View File

@@ -1,14 +1,13 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
[ApiController]
[Route("/api/developers/{pubName}/apps")]
public class CustomAppController(CustomAppService customApps, PublisherService ps) : ControllerBase
public class CustomAppController(CustomAppService customApps, DeveloperService ds) : ControllerBase
{
public record CustomAppRequest(
[MaxLength(1024)] string? Slug,
@@ -24,19 +23,19 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
[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);
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
var apps = await customApps.GetAppsByPublisherAsync(developer.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 developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
if (app == null)
return NotFound();
@@ -52,17 +51,15 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
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();
var developer = await ds.GetDeveloperByName(pubName);
if (developer 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");
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to create a custom app");
try
{
var app = await customApps.CreateAppAsync(publisher, request);
var app = await customApps.CreateAppAsync(developer, request);
return Ok(app);
}
catch (InvalidOperationException ex)
@@ -81,13 +78,13 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
var developer = await ds.GetDeveloperByName(pubName);
if (developer 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");
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to update a custom app");
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
if (app == null)
return NotFound();
@@ -111,13 +108,13 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
var developer = await ds.GetDeveloperByName(pubName);
if (developer 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");
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to delete a custom app");
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
if (app == null)
return NotFound();

View File

@@ -2,7 +2,7 @@ using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Develop.Identity;
public class CustomAppService(
AppDatabase db,
@@ -11,7 +11,7 @@ public class CustomAppService(
)
{
public async Task<CustomApp?> CreateAppAsync(
Publisher.Publisher pub,
Developer pub,
CustomAppController.CustomAppRequest request
)
{
@@ -23,7 +23,7 @@ public class CustomAppService(
Status = request.Status ?? CustomAppStatus.Developing,
Links = request.Links,
OauthConfig = request.OauthConfig,
PublisherId = pub.Id
DeveloperId = pub.Id
};
if (request.PictureId is not null)
@@ -74,17 +74,17 @@ public class CustomAppService(
return app;
}
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? publisherId = null)
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? developerId = null)
{
var query = db.CustomApps.Where(a => a.Id == id).AsQueryable();
if (publisherId.HasValue)
query = query.Where(a => a.PublisherId == publisherId.Value);
if (developerId.HasValue)
query = query.Where(a => a.DeveloperId == developerId.Value);
return await query.FirstOrDefaultAsync();
}
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
{
return await db.CustomApps.Where(a => a.PublisherId == publisherId).ToListAsync();
return await db.CustomApps.Where(a => a.DeveloperId == publisherId).ToListAsync();
}
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)

View File

@@ -0,0 +1,75 @@
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Data;
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
namespace DysonNetwork.Develop.Identity;
public class Developer
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PublisherId { get; set; }
[NotMapped] public PublisherInfo? Publisher { get; set; }
}
public class PublisherInfo
{
public Guid Id { get; set; }
public PublisherType Type { get; set; }
public string Name { get; set; } = string.Empty;
public string Nick { get; set; } = string.Empty;
public string? Bio { get; set; }
public CloudFileReferenceObject? Picture { get; set; }
public CloudFileReferenceObject? Background { get; set; }
public VerificationMark? Verification { get; set; }
public Guid? AccountId { get; set; }
public Guid? RealmId { get; set; }
public static PublisherInfo FromProto(Publisher proto)
{
var info = new PublisherInfo
{
Id = Guid.Parse(proto.Id),
Type = proto.Type == PublisherType.PubIndividual
? PublisherType.PubIndividual
: PublisherType.PubOrganizational,
Name = proto.Name,
Nick = proto.Nick,
Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio,
Verification = proto.VerificationMark is not null
? VerificationMark.FromProtoValue(proto.VerificationMark)
: null,
AccountId = string.IsNullOrEmpty(proto.AccountId) ? null : Guid.Parse(proto.AccountId),
RealmId = string.IsNullOrEmpty(proto.RealmId) ? null : Guid.Parse(proto.RealmId)
};
if (proto.Picture != null)
{
info.Picture = new CloudFileReferenceObject
{
Id = proto.Picture.Id,
Name = proto.Picture.Name,
MimeType = proto.Picture.MimeType,
Hash = proto.Picture.Hash,
Size = proto.Picture.Size
};
}
if (proto.Background != null)
{
info.Background = new CloudFileReferenceObject
{
Id = proto.Background.Id,
Name = proto.Background.Name,
MimeType = proto.Background.MimeType,
Hash = proto.Background.Hash,
Size = (long)proto.Background.Size
};
}
return info;
}
}

View File

@@ -0,0 +1,128 @@
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Develop.Identity;
[ApiController]
[Route("/api/developers")]
public class DeveloperController(
AppDatabase db,
PublisherService.PublisherServiceClient ps,
ActionLogService.ActionLogServiceClient als,
DeveloperService ds
)
: ControllerBase
{
[HttpGet("{name}")]
public async Task<ActionResult<Developer>> GetDeveloper(string name)
{
var developer = await ds.GetDeveloperByName(name);
if (developer is null) return NotFound();
return Ok(developer);
}
[HttpGet("{name}/stats")]
public async Task<ActionResult<DeveloperStats>> GetDeveloperStats(string name)
{
var developer = await ds.GetDeveloperByName(name);
if (developer is null) return NotFound();
// Get custom apps count
var customAppsCount = await db.CustomApps
.Where(a => a.DeveloperId == developer.Id)
.CountAsync();
var stats = new DeveloperStats
{
TotalCustomApps = customAppsCount
};
return Ok(stats);
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id });
var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
var developerQuery = db.Developers
.Where(d => pubIds.Contains(d.PublisherId))
.AsQueryable();
var totalCount = await developerQuery.CountAsync();
Response.Headers.Append("X-Total", totalCount.ToString());
var developers = await developerQuery.ToListAsync();
return Ok(developers);
}
[HttpPost("{name}/enroll")]
[Authorize]
[RequiredPermission("global", "developers.create")]
public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
PublisherInfo? pub;
try
{
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
pub = PublisherInfo.FromProto(pubResponse.Publisher);
} catch (RpcException ex)
{
return NotFound(ex.Status.Detail);
}
// Check if the user is an owner of the publisher
var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest
{
PublisherId = pub.Id.ToString(),
AccountId = currentUser.Id,
Role = PublisherMemberRole.Owner
});
if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id);
if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
var developer = new Developer
{
Id = Guid.NewGuid(),
PublisherId = pub.Id
};
db.Developers.Add(developer);
await db.SaveChangesAsync();
_ = als.CreateActionLogAsync(new CreateActionLogRequest
{
Action = "developers.enroll",
Meta =
{
{ "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Id.ToString()) },
{ "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Name) }
},
AccountId = currentUser.Id,
UserAgent = Request.Headers.UserAgent,
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
});
return Ok(developer);
}
public class DeveloperStats
{
public int TotalCustomApps { get; set; }
}
}

View File

@@ -0,0 +1,49 @@
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Develop.Identity;
public class DeveloperService(AppDatabase db, PublisherService.PublisherServiceClient ps)
{
public async Task<Developer> LoadDeveloperPublisher(Developer developer)
{
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher);
return developer;
}
public async Task<Developer?> GetDeveloperByName(string name)
{
try
{
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
var pubId = Guid.Parse(pubResponse.Publisher.Id);
var developer = await db.Developers.FirstOrDefaultAsync(d => d.Id == pubId);
return developer;
}
catch (RpcException)
{
return null;
}
}
public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role)
{
try
{
var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest
{
PublisherId = pubId.ToString(),
AccountId = accountId.ToString(),
Role = role
});
return permResponse.Valid;
}
catch (RpcException)
{
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
using DysonNetwork.Develop;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Develop.Startup;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.ConfigureAppKestrel(builder.Configuration);
builder.Services.AddRegistryService(builder.Configuration);
builder.Services.AddAppServices(builder.Configuration);
builder.Services.AddAppAuthentication();
builder.Services.AddAppSwagger();
builder.Services.AddDysonAuth();
builder.Services.AddPublisherService();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
await db.Database.MigrateAsync();
}
app.ConfigureAppMiddleware(builder.Configuration);
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5156",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7192;http://localhost:5156",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Net;
using DysonNetwork.Shared.Auth;
using Microsoft.AspNetCore.HttpOverrides;
using Prometheus;
namespace DysonNetwork.Develop.Startup;
public static class ApplicationConfiguration
{
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
{
app.MapMetrics();
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
app.UseRequestLocalization();
ConfigureForwardedHeaders(app, configuration);
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<PermissionMiddleware>();
app.MapControllers();
return app;
}
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
{
var knownProxiesSection = configuration.GetSection("KnownProxies");
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
if (knownProxiesSection.Exists())
{
var proxyAddresses = knownProxiesSection.Get<string[]>();
if (proxyAddresses != null)
foreach (var proxy in proxyAddresses)
if (IPAddress.TryParse(proxy, out var ipAddress))
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
}
else
{
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
}
app.UseForwardedHeaders(forwardedHeadersOptions);
}
}

View File

@@ -0,0 +1,72 @@
using System.Globalization;
using Microsoft.OpenApi.Models;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
using System.Text.Json;
using DysonNetwork.Develop.Identity;
using DysonNetwork.Shared.Cache;
namespace DysonNetwork.Develop.Startup;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddLocalization();
services.AddDbContext<AppDatabase>();
services.AddSingleton<IClock>(SystemClock.Instance);
services.AddHttpContextAccessor();
services.AddSingleton<ICacheService, CacheServiceRedis>();
services.AddHttpClient();
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
});
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("zh-Hans"),
};
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
services.AddScoped<DeveloperService>();
services.AddScoped<CustomAppService>();
return services;
}
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
{
services.AddCors();
services.AddAuthorization();
return services;
}
public static IServiceCollection AddAppSwagger(this IServiceCollection services)
{
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Develop API",
});
});
services.AddOpenApi();
return services;
}
}

View File

@@ -0,0 +1,30 @@
{
"Debug": true,
"BaseUrl": "http://localhost:5071",
"SiteUrl": "https://solian.app",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379",
"Etcd": "etcd.orb.local:2379"
},
"KnownProxies": [
"127.0.0.1",
"::1"
],
"Etcd": {
"Insecure": true
},
"Service": {
"Name": "DysonNetwork.Develop",
"Url": "https://localhost:7099",
"ClientCert": "../Certificates/client.crt",
"ClientKey": "../Certificates/client.key"
}
}

View File

@@ -14,20 +14,20 @@ public class VerificationMark
[MaxLength(8192)] public string? Description { get; set; }
[MaxLength(1024)] public string? VerifiedBy { get; set; }
public Shared.Proto.VerificationMark ToProtoValue()
public Proto.VerificationMark ToProtoValue()
{
var proto = new Shared.Proto.VerificationMark
var proto = new Proto.VerificationMark
{
Type = Type switch
{
VerificationMarkType.Official => Shared.Proto.VerificationMarkType.Official,
VerificationMarkType.Individual => Shared.Proto.VerificationMarkType.Individual,
VerificationMarkType.Organization => Shared.Proto.VerificationMarkType.Organization,
VerificationMarkType.Government => Shared.Proto.VerificationMarkType.Government,
VerificationMarkType.Creator => Shared.Proto.VerificationMarkType.Creator,
VerificationMarkType.Developer => Shared.Proto.VerificationMarkType.Developer,
VerificationMarkType.Parody => Shared.Proto.VerificationMarkType.Parody,
_ => Shared.Proto.VerificationMarkType.Unspecified
VerificationMarkType.Official => Proto.VerificationMarkType.Official,
VerificationMarkType.Individual => Proto.VerificationMarkType.Individual,
VerificationMarkType.Organization => Proto.VerificationMarkType.Organization,
VerificationMarkType.Government => Proto.VerificationMarkType.Government,
VerificationMarkType.Creator => Proto.VerificationMarkType.Creator,
VerificationMarkType.Developer => Proto.VerificationMarkType.Developer,
VerificationMarkType.Parody => Proto.VerificationMarkType.Parody,
_ => Proto.VerificationMarkType.Unspecified
},
Title = Title ?? string.Empty,
Description = Description ?? string.Empty,

View File

@@ -123,4 +123,16 @@ public static class GrpcClientHelper
return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
}
public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Sphere");
return new PublisherService.PublisherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
}

View File

@@ -0,0 +1,123 @@
syntax = "proto3";
package proto;
option csharp_namespace = "DysonNetwork.Shared.Proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "file.proto";
import "account.proto";
enum PublisherType {
PUBLISHER_TYPE_UNSPECIFIED = 0;
PUB_INDIVIDUAL = 1;
PUB_ORGANIZATIONAL = 2;
}
enum PublisherMemberRole {
PUBLISHER_MEMBER_ROLE_UNSPECIFIED = 0;
OWNER = 100;
MANAGER = 75;
EDITOR = 50;
VIEWER = 25;
}
message PublisherFeature {
string id = 1;
string flag = 2;
google.protobuf.Timestamp expired_at = 3;
string publisher_id = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
message PublisherMember {
string publisher_id = 1;
string account_id = 2;
PublisherMemberRole role = 3;
google.protobuf.Timestamp joined_at = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
message Publisher {
string id = 1;
PublisherType type = 2;
string name = 3;
string nick = 4;
google.protobuf.StringValue bio = 5;
CloudFile picture = 8;
CloudFile background = 9;
optional VerificationMark verification_mark = 10;
string account_id = 11;
optional string realm_id = 12;
google.protobuf.Timestamp created_at = 13;
google.protobuf.Timestamp updated_at = 14;
}
message GetPublisherRequest {
oneof query {
string name = 1;
string id = 2;
}
}
message GetPublisherResponse {
Publisher publisher = 1;
}
message GetPublisherBatchRequest {
repeated string ids = 1;
}
message ListPublishersRequest {
string account_id = 1; // filter by owner/member account
string realm_id = 2; // filter by realm
}
message ListPublishersResponse {
repeated Publisher publishers = 1;
}
message ListPublisherMembersRequest {
string publisher_id = 1;
}
message ListPublisherMembersResponse {
repeated PublisherMember members = 1;
}
message SetPublisherFeatureFlagRequest {
string publisher_id = 1;
string flag = 2;
}
message HasPublisherFeatureRequest {
string publisher_id = 1;
string flag = 2;
}
message HasPublisherFeatureResponse {
bool enabled = 1;
}
message IsPublisherMemberRequest {
string publisher_id = 1;
string account_id = 2;
PublisherMemberRole role = 3;
}
message IsPublisherMemberResponse {
bool valid = 1;
}
service PublisherService {
rpc GetPublisher(GetPublisherRequest) returns (GetPublisherResponse);
rpc GetPublisherBatch(GetPublisherBatchRequest) returns (ListPublishersResponse);
rpc ListPublishers(ListPublishersRequest) returns (ListPublishersResponse);
rpc ListPublisherMembers(ListPublisherMembersRequest) returns (ListPublisherMembersResponse);
rpc SetPublisherFeatureFlag(SetPublisherFeatureFlagRequest) returns (google.protobuf.StringValue); // returns optional message
rpc HasPublisherFeature(HasPublisherFeatureRequest) returns (HasPublisherFeatureResponse);
rpc IsPublisherMember(IsPublisherMemberRequest) returns (IsPublisherMemberResponse);
}

View File

@@ -92,4 +92,23 @@ public static class ServiceInjectionHelper
return services;
}
public static IServiceCollection AddPublisherService(this IServiceCollection services)
{
services.AddSingleton<PublisherService.PublisherServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreatePublisherServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
});
return services;
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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; }
}
}

View File

@@ -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"/>

View File

@@ -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");

View File

@@ -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),

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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");

View File

@@ -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
)

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
@@ -19,7 +20,7 @@ public enum PublisherType
[Index(nameof(Name), IsUnique = true)]
public class Publisher : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
public PublisherType Type { get; set; }
[MaxLength(256)] public string Name { get; set; } = string.Empty;
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
@@ -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.PubIndividual
: Shared.Proto.PublisherType.PubOrganizational,
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

View File

@@ -0,0 +1,115 @@
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
)
{
Publisher? p = null;
switch (request.QueryCase)
{
case GetPublisherRequest.QueryOneofCase.Id:
if (!string.IsNullOrWhiteSpace(request.Id) && Guid.TryParse(request.Id, out var id))
p = await db.Publishers.FirstOrDefaultAsync(x => x.Id == id);
break;
case GetPublisherRequest.QueryOneofCase.Name:
if (!string.IsNullOrWhiteSpace(request.Name))
p = await service.GetPublisherByName(request.Name);
break;
}
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> GetPublisherBatch(
GetPublisherBatchRequest request,
ServerCallContext context
)
{
var ids = request.Ids
.Where(s => !string.IsNullOrWhiteSpace(s) && Guid.TryParse(s, out _))
.Select(Guid.Parse)
.ToList();
if (ids.Count == 0) return new ListPublishersResponse();
var list = await db.Publishers.Where(p => ids.Contains(p.Id)).ToListAsync();
var resp = new ListPublishersResponse();
resp.Publishers.AddRange(list.Select(p => p.ToProto(db)));
return resp;
}
public override async Task<ListPublishersResponse> ListPublishers(
ListPublishersRequest request,
ServerCallContext context
)
{
var 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.ToListAsync();
var resp = new ListPublishersResponse();
resp.Publishers.AddRange(list.Select(p => p.ToProto(db)));
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 };
}
public override async Task<IsPublisherMemberResponse> IsPublisherMember(IsPublisherMemberRequest request,
ServerCallContext context)
{
if (!Guid.TryParse(request.PublisherId, out var pid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
if (!Guid.TryParse(request.AccountId, out var aid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid account_id"));
var requiredRole = request.Role switch
{
Shared.Proto.PublisherMemberRole.Owner => PublisherMemberRole.Owner,
Shared.Proto.PublisherMemberRole.Manager => PublisherMemberRole.Manager,
Shared.Proto.PublisherMemberRole.Editor => PublisherMemberRole.Editor,
_ => PublisherMemberRole.Viewer
};
var valid = await service.IsMemberWithRole(pid, aid, requiredRole);
return new IsPublisherMemberResponse { Valid = valid };
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
#
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Sphere", "DysonNetwork.Sphere\DysonNetwork.Sphere.csproj", "{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A444D180-5B51-49C3-A35D-AA55832BBC66}"
@@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Drive", "Dyson
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Gateway", "DysonNetwork.Gateway\DysonNetwork.Gateway.csproj", "{19EB0086-4049-4B78-91C4-EAC37130A006}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Develop", "DysonNetwork.Develop\DysonNetwork.Develop.csproj", "{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,5 +50,9 @@ Global
{19EB0086-4049-4B78-91C4-EAC37130A006}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19EB0086-4049-4B78-91C4-EAC37130A006}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19EB0086-4049-4B78-91C4-EAC37130A006}.Release|Any CPU.Build.0 = Release|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C577AA78-B11D-4076-89A6-1C7F0ECC04E2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal