♻️ Extract the Developer to new service, add PublisherServiceGrpc
This commit is contained in:
27
DysonNetwork.Develop/AppDatabase.cs
Normal file
27
DysonNetwork.Develop/AppDatabase.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop;
|
||||
|
||||
public class AppDatabase(
|
||||
DbContextOptions<AppDatabase> options,
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
23
DysonNetwork.Develop/Dockerfile
Normal file
23
DysonNetwork.Develop/Dockerfile
Normal 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"]
|
37
DysonNetwork.Develop/DysonNetwork.Develop.csproj
Normal file
37
DysonNetwork.Develop/DysonNetwork.Develop.csproj
Normal 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>
|
68
DysonNetwork.Develop/Identity/CustomApp.cs
Normal file
68
DysonNetwork.Develop/Identity/CustomApp.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
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!;
|
||||
}
|
128
DysonNetwork.Develop/Identity/CustomAppController.cs
Normal file
128
DysonNetwork.Develop/Identity/CustomAppController.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
171
DysonNetwork.Develop/Identity/CustomAppService.cs
Normal file
171
DysonNetwork.Develop/Identity/CustomAppService.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
153
DysonNetwork.Develop/Identity/DeveloperController.cs
Normal file
153
DysonNetwork.Develop/Identity/DeveloperController.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers")]
|
||||
public class DeveloperController(
|
||||
AppDatabase db,
|
||||
PublisherService.PublisherServiceClient 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; }
|
||||
}
|
||||
}
|
29
DysonNetwork.Develop/Program.cs
Normal file
29
DysonNetwork.Develop/Program.cs
Normal 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();
|
23
DysonNetwork.Develop/Properties/launchSettings.json
Normal file
23
DysonNetwork.Develop/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
DysonNetwork.Develop/Startup/ApplicationConfiguration.cs
Normal file
52
DysonNetwork.Develop/Startup/ApplicationConfiguration.cs
Normal 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);
|
||||
}
|
||||
}
|
68
DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs
Normal file
68
DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
30
DysonNetwork.Develop/appsettings.json
Normal file
30
DysonNetwork.Develop/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user