Revert Merge pull request #89 from btcpayserver/gpg_key_encryption
This commit is contained in:
parent
86270bf8fc
commit
87bb60ebed
@ -39,7 +39,6 @@ public class PublicDirectoryUITests(ITestOutputHelper output) : PageTest
|
||||
// Listed should be visible
|
||||
await conn.SetPluginSettingsAndVisibility(slug, "{}", "listed");
|
||||
await tester.GoToUrl("/public/plugins");
|
||||
|
||||
await tester.Page!.WaitForSelectorAsync("a[href='/public/plugins/rockstar-stylist']");
|
||||
Assert.True(await tester.Page.Locator("a[href='/public/plugins/rockstar-stylist']").IsVisibleAsync());
|
||||
|
||||
@ -68,7 +67,7 @@ public class PublicDirectoryUITests(ITestOutputHelper output) : PageTest
|
||||
await tester.Page.WaitForSelectorAsync("a[href='/public/plugins/rockstar-stylist']", new PageWaitForSelectorOptions { State = WaitForSelectorState.Hidden });
|
||||
Assert.False(await tester.Page.Locator("a[href='/public/plugins/rockstar-stylist']").IsVisibleAsync());
|
||||
|
||||
// Try to access hidden plugin page without being owner or admin
|
||||
// Public page should not be accessible if hidden and owner is not logged in
|
||||
var response = await tester.GoToUrl("/public/plugins/rockstar-stylist");
|
||||
Assert.Equal(404, response?.Status);
|
||||
|
||||
|
||||
@ -102,14 +102,13 @@ public class UnitTest1 : UnitTestBase
|
||||
Assert.Equal("rockstar-stylist", version.ProjectSlug);
|
||||
|
||||
// Let's see what happen if there is two versions of the same plugin
|
||||
await conn.ExecuteAsync("INSERT INTO versions VALUES('rockstar-stylist', ARRAY[1,0,2,1], 0, ARRAY[1,4,6,0], 'f', CURRENT_TIMESTAMP, NULL)");
|
||||
await conn.ExecuteAsync("INSERT INTO versions VALUES('rockstar-stylist', ARRAY[1,0,2,1], 0, ARRAY[1,4,6,0], 'f', CURRENT_TIMESTAMP)");
|
||||
versions = await client.GetPublishedVersions("1.4.6.0", true);
|
||||
|
||||
version = Assert.Single(versions);
|
||||
Assert.Equal("1.0.2.1", version.Version);
|
||||
versions = await client.GetPublishedVersions("1.4.6.0", true, true);
|
||||
Assert.Equal("1.0.2.1", versions[1].Version);
|
||||
Assert.Equal("1.0.2.0", versions[0].Version);
|
||||
Assert.Equal("1.0.2.1", versions[0].Version);
|
||||
Assert.Equal("1.0.2.0", versions[1].Version);
|
||||
|
||||
// listed - always render
|
||||
await conn.ExecuteAsync("UPDATE plugins SET visibility = 'listed' WHERE slug = 'rockstar-stylist'");
|
||||
|
||||
@ -12,7 +12,6 @@ namespace PluginBuilder.Controllers;
|
||||
[Authorize]
|
||||
[Route("/account/")]
|
||||
public class AccountController(
|
||||
GPGKeyService _gpgService,
|
||||
DBConnectionFactory connectionFactory,
|
||||
UserManager<IdentityUser> userManager,
|
||||
ExternalAccountVerificationService externalAccountVerificationService,
|
||||
@ -71,7 +70,7 @@ public class AccountController(
|
||||
{
|
||||
if (!ModelState.IsValid) return View(model);
|
||||
|
||||
var user = await userManager.GetUserAsync(User);
|
||||
var user = await userManager.GetUserAsync(User)!;
|
||||
|
||||
await using var conn = await connectionFactory.Open();
|
||||
var accountSettings = await conn.GetAccountDetailSettings(user!.Id) ?? new AccountSettings();
|
||||
@ -79,21 +78,7 @@ public class AccountController(
|
||||
accountSettings.Nostr = model.Settings.Nostr;
|
||||
accountSettings.Twitter = model.Settings.Twitter;
|
||||
accountSettings.Email = model.Settings.Email;
|
||||
if (!string.IsNullOrEmpty(model.Settings.GPGKey?.PublicKey))
|
||||
{
|
||||
var isPublicKeyValid = _gpgService.ValidateArmouredPublicKey(model.Settings.GPGKey.PublicKey.Trim(), out var message, out var keyViewModel);
|
||||
if (!isPublicKeyValid)
|
||||
{
|
||||
TempData[TempDataConstant.WarningMessage] = $"GPG Key is not valid: {message}";
|
||||
return View(model);
|
||||
}
|
||||
accountSettings.GPGKey = keyViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
accountSettings.GPGKey = new();
|
||||
}
|
||||
await conn.SetAccountDetailSettings(accountSettings, user.Id);
|
||||
await conn.SetAccountDetailSettings(accountSettings, user!.Id);
|
||||
|
||||
TempData[TempDataConstant.SuccessMessage] = "Account details updated successfully";
|
||||
return RedirectToAction(nameof(AccountDetails));
|
||||
|
||||
@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Npgsql;
|
||||
using PluginBuilder.Configuration;
|
||||
using PluginBuilder.Controllers.Logic;
|
||||
@ -92,7 +91,6 @@ public class AdminController(
|
||||
|
||||
model.Plugins = plugins;
|
||||
model.VerifiedEmailForPluginPublish = await conn.GetVerifiedEmailForPluginPublishSetting();
|
||||
model.VerifiedGPGSignatureForPluginRelease = await conn.RequiresGPGSignatureForPluginRelease();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -100,7 +98,7 @@ public class AdminController(
|
||||
public async Task<IActionResult> UpdateVerifiedEmailForPublishRequirement(bool verifiedEmailForPluginPublish)
|
||||
{
|
||||
await using var conn = await connectionFactory.Open();
|
||||
await conn.UpdatePluginAdminSettings(SettingsKeys.VerifiedEmailForPluginPublish, verifiedEmailForPluginPublish);
|
||||
await conn.UpdateVerifiedEmailForPluginPublishSetting(verifiedEmailForPluginPublish);
|
||||
await userVerifiedCache.RefreshIsVerifiedEmailRequiredForPublish(conn);
|
||||
TempData[TempDataConstant.SuccessMessage] = "Email requirement setting for publishing plugin updated successfully";
|
||||
return RedirectToAction("ListPlugins");
|
||||
|
||||
@ -46,7 +46,6 @@ public class UserVerifiedLogic(
|
||||
public class UserVerifiedCache
|
||||
{
|
||||
public bool IsEmailVerificationRequiredForPublish { get; private set; }
|
||||
public bool IsGPGSignatureRequiredForRelease { get; private set; }
|
||||
public bool IsEmailVerificationRequiredForLogin { get; private set; }
|
||||
public bool IsGithubVerificationRequired { get; private set; }
|
||||
|
||||
@ -56,11 +55,6 @@ public class UserVerifiedCache
|
||||
IsEmailVerificationRequiredForPublish = await conn.GetVerifiedEmailForPluginPublishSetting();
|
||||
}
|
||||
|
||||
public async Task RefreshIsGPGSignatureRequirementForRelease(NpgsqlConnection conn)
|
||||
{
|
||||
IsGPGSignatureRequiredForRelease = await conn.RequiresGPGSignatureForPluginRelease();
|
||||
}
|
||||
|
||||
public async Task RefreshIsVerifiedEmailRequiredForLogin(NpgsqlConnection conn)
|
||||
{
|
||||
IsEmailVerificationRequiredForLogin = await conn.GetVerifiedEmailForLoginSetting();
|
||||
@ -75,7 +69,6 @@ public class UserVerifiedCache
|
||||
public async Task RefreshAllUserVerifiedSettings(NpgsqlConnection conn)
|
||||
{
|
||||
await RefreshIsVerifiedEmailRequiredForPublish(conn);
|
||||
await RefreshIsGPGSignatureRequirementForRelease(conn);
|
||||
await RefreshIsVerifiedEmailRequiredForLogin(conn);
|
||||
await RefreshIsVerifiedGithubRequired(conn);
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -22,7 +21,6 @@ public class PluginController(
|
||||
DBConnectionFactory connectionFactory,
|
||||
UserManager<IdentityUser> userManager,
|
||||
BuildService buildService,
|
||||
GPGKeyService gpgKeyService,
|
||||
AzureStorageClient azureStorageClient,
|
||||
UserVerifiedLogic userVerifiedLogic,
|
||||
FirstBuildEvent firstBuildEvent,
|
||||
@ -195,58 +193,32 @@ public class PluginController(
|
||||
[ModelBinder(typeof(PluginSlugModelBinder))]
|
||||
PluginSlug pluginSlug,
|
||||
[ModelBinder(typeof(PluginVersionModelBinder))]
|
||||
PluginVersion version, string command, IFormFile? signatureFile)
|
||||
PluginVersion version, string command)
|
||||
{
|
||||
await using var conn = await connectionFactory.Open();
|
||||
|
||||
var pluginBuild = await conn.QueryFirstOrDefaultAsync<(long buildId, string identifier)>(
|
||||
if (command == "remove")
|
||||
{
|
||||
var pluginBuild = await conn.QueryFirstOrDefaultAsync<(long buildId, string identifier)>(
|
||||
"SELECT v.build_id, p.identifier FROM versions v JOIN plugins p ON v.plugin_slug = p.slug WHERE plugin_slug=@pluginSlug AND ver=@version",
|
||||
new { pluginSlug = pluginSlug.ToString(), version = version.VersionParts });
|
||||
|
||||
var requireGPGSignature = await conn.RequiresGPGSignatureForPluginRelease();
|
||||
switch (command)
|
||||
{
|
||||
case "remove":
|
||||
FullBuildId fullBuildId = new(pluginSlug, pluginBuild.buildId);
|
||||
await conn.ExecuteAsync("DELETE FROM versions WHERE plugin_slug=@pluginSlug AND ver=@version",
|
||||
new { pluginSlug = pluginSlug.ToString(), version = version.VersionParts });
|
||||
await buildService.UpdateBuild(fullBuildId, BuildStates.Removed, null);
|
||||
return RedirectToAction(nameof(Build), new { pluginSlug = pluginSlug.ToString(), pluginBuild.buildId });
|
||||
|
||||
case "sign_release":
|
||||
var build_info = await conn.QueryFirstOrDefaultAsync<string>("SELECT build_info FROM builds b WHERE b.plugin_slug=@pluginSlug AND b.id=@buildId LIMIT 1",
|
||||
new { pluginSlug = pluginSlug.ToString(), pluginBuild.buildId });
|
||||
|
||||
if (build_info is null || BuildInfo.Parse(build_info) is not { } buildInfo || string.IsNullOrEmpty(buildInfo.GitCommit))
|
||||
{
|
||||
TempData[TempDataConstant.WarningMessage] = "Build information for plugin not available";
|
||||
return RedirectToAction(nameof(Version), new { pluginSlug = pluginSlug.ToString(), version = version.ToString() });
|
||||
}
|
||||
|
||||
var rawSignedBytes = Encoding.UTF8.GetBytes(buildInfo.GitCommit);
|
||||
var signatureVerification = await gpgKeyService.VerifyDetachedSignature(pluginSlug.ToString(), userManager.GetUserId(User)!,
|
||||
rawSignedBytes, signatureFile);
|
||||
|
||||
if (!signatureVerification.valid)
|
||||
{
|
||||
TempData[TempDataConstant.WarningMessage] = signatureVerification.message;
|
||||
return RedirectToAction(nameof(Version), new { pluginSlug = pluginSlug.ToString(), version = version.ToString() });
|
||||
}
|
||||
await conn.UpdateVersionReleaseStatus(pluginSlug, command, version, signatureVerification.proof);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (requireGPGSignature && command == "release")
|
||||
{
|
||||
TempData[TempDataConstant.WarningMessage] = "A verified GPG signature is required to release this version";
|
||||
return RedirectToAction(nameof(Version), new { pluginSlug = pluginSlug.ToString(), version = version.ToString() });
|
||||
}
|
||||
await conn.UpdateVersionReleaseStatus(pluginSlug, command, version);
|
||||
break;
|
||||
FullBuildId fullBuildId = new(pluginSlug, pluginBuild.buildId);
|
||||
await conn.ExecuteAsync("DELETE FROM versions WHERE plugin_slug=@pluginSlug AND ver=@version",
|
||||
new { pluginSlug = pluginSlug.ToString(), version = version.VersionParts });
|
||||
await buildService.UpdateBuild(fullBuildId, BuildStates.Removed, null);
|
||||
return RedirectToAction(nameof(Build), new { pluginSlug = pluginSlug.ToString(), pluginBuild.buildId });
|
||||
}
|
||||
// Email notifications are now handled on first build creation, not on release.
|
||||
|
||||
await conn.ExecuteAsync("UPDATE versions SET pre_release=@preRelease WHERE plugin_slug=@pluginSlug AND ver=@version",
|
||||
new
|
||||
{
|
||||
pluginSlug = pluginSlug.ToString(),
|
||||
version = version.VersionParts,
|
||||
preRelease = command == "unrelease"
|
||||
});
|
||||
TempData[TempDataConstant.SuccessMessage] =
|
||||
$"Version {version} {(command is "release" or "sign_release" ? "released" : "unreleased")}";
|
||||
$"Version {version} {(command == "release" ? "released" : "unreleased")}";
|
||||
return RedirectToAction(nameof(Version), new { pluginSlug = pluginSlug.ToString(), version = version.ToString() });
|
||||
}
|
||||
|
||||
@ -272,8 +244,8 @@ public class PluginController(
|
||||
await using var conn = await connectionFactory.Open();
|
||||
var row =
|
||||
await conn
|
||||
.QueryFirstOrDefaultAsync<(string manifest_info, string build_info, string state, DateTimeOffset created_at, bool published, bool pre_release, string signatureproof)>(
|
||||
"SELECT manifest_info, build_info, state, created_at, v.ver IS NOT NULL, v.pre_release, v.signatureproof FROM builds b " +
|
||||
.QueryFirstOrDefaultAsync<(string manifest_info, string build_info, string state, DateTimeOffset created_at, bool published, bool pre_release)>(
|
||||
"SELECT manifest_info, build_info, state, created_at, v.ver IS NOT NULL, v.pre_release FROM builds b " +
|
||||
"LEFT JOIN versions v ON b.plugin_slug=v.plugin_slug AND b.id=v.build_id " +
|
||||
"WHERE b.plugin_slug=@pluginSlug AND id=@buildId " +
|
||||
"LIMIT 1",
|
||||
@ -284,25 +256,21 @@ public class PluginController(
|
||||
"ORDER BY created_at;",
|
||||
new { pluginSlug = pluginSlug.ToString(), buildId });
|
||||
var logs = string.Join("\r\n", logLines);
|
||||
var signatureProof = string.IsNullOrWhiteSpace(row.signatureproof)
|
||||
? new SignatureProof() : JsonConvert.DeserializeObject<SignatureProof>(row.signatureproof, CamelCaseSerializerSettings.Instance) ?? new SignatureProof();
|
||||
|
||||
BuildViewModel vm = new();
|
||||
var buildInfo = row.build_info is null ? null : BuildInfo.Parse(row.build_info);
|
||||
var manifest = row.manifest_info is null ? null : PluginManifest.Parse(row.manifest_info);
|
||||
vm.FullBuildId = new FullBuildId(pluginSlug, buildId);
|
||||
vm.ManifestInfo = NiceJson(row.manifest_info, signatureProof?.Fingerprint);
|
||||
vm.ManifestInfo = NiceJson(row.manifest_info);
|
||||
vm.BuildInfo = buildInfo?.ToString(Formatting.Indented);
|
||||
vm.DownloadLink = buildInfo?.Url;
|
||||
vm.State = row.state;
|
||||
vm.CreatedDate = (DateTimeOffset.UtcNow - row.created_at).ToTimeAgo();
|
||||
vm.Commit = buildInfo?.GitCommit;
|
||||
vm.Commit = buildInfo?.GitCommit?.Substring(0, 8);
|
||||
vm.Repository = buildInfo?.GitRepository;
|
||||
vm.GitRef = buildInfo?.GitRef;
|
||||
vm.Version = PluginVersionViewModel.CreateOrNull(manifest?.Version?.ToString(), row.published, row.pre_release, row.state, pluginSlug.ToString());
|
||||
vm.RepositoryLink = GetUrl(buildInfo);
|
||||
vm.DownloadLink = buildInfo?.Url;
|
||||
vm.RequireGPGSignatureForRelease = await conn.RequiresGPGSignatureForPluginRelease();
|
||||
//vm.Error = buildInfo?.Error;
|
||||
vm.Published = row.published;
|
||||
//var buildId = await conn.NewBuild(pluginSlug);
|
||||
@ -312,14 +280,12 @@ public class PluginController(
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private string? NiceJson(string? json, string? fingerprint = null)
|
||||
private string? NiceJson(string? json)
|
||||
{
|
||||
if (json is null)
|
||||
return null;
|
||||
var data = JObject.Parse(json);
|
||||
data = new JObject(data.Properties().OrderBy(p => p.Name));
|
||||
if (!string.IsNullOrWhiteSpace(fingerprint))
|
||||
data["SignatureFingerprint"] = fingerprint;
|
||||
return data.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
ALTER TABLE versions
|
||||
ADD COLUMN signatureproof JSONB NULL;
|
||||
@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using PluginBuilder.ViewModels;
|
||||
|
||||
namespace PluginBuilder.DataModels;
|
||||
|
||||
@ -17,5 +16,4 @@ public class AccountSettings
|
||||
[Display(Name = "Public Email address")]
|
||||
public string? Email { get; set; }
|
||||
public string? PendingNewEmail { get; set; }
|
||||
public PgpKeyViewModel? GPGKey { get; set; }
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ public static class SettingsKeys
|
||||
{
|
||||
public const string EmailSettings = nameof(EmailSettings);
|
||||
public const string VerifiedEmailForPluginPublish = nameof(VerifiedEmailForPluginPublish);
|
||||
public const string VerifiedGPGSignatureForPluginRelease = nameof(VerifiedGPGSignatureForPluginRelease);
|
||||
public const string VerifiedEmailForLogin = nameof(VerifiedEmailForLogin);
|
||||
public const string FirstPluginBuildReviewers = nameof(FirstPluginBuildReviewers);
|
||||
public const string VerifiedGithub = nameof(VerifiedGithub);
|
||||
|
||||
@ -14,102 +14,102 @@
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.10" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.10" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.10"/>
|
||||
<PackageReference Include="MailKit" Version="4.8.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.10"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.10"/>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="Npgsql" Version="8.0.4"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8"/>
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
|
||||
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3"/>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<DisableScopedCssBundling>true</DisableScopedCssBundling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Data\Scripts\*.sql" />
|
||||
<EmbeddedResource Include="Data\Scripts\*.sql"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Data\Scripts\01.migrations.sql" />
|
||||
<None Remove="Data\Scripts\02.AspNetIdentity.sql" />
|
||||
<None Remove="Data\Scripts\03.Init.sql" />
|
||||
<None Remove="Data\Scripts\04.FixGetLatestVersions.sql" />
|
||||
<None Remove="Data\Scripts\05.ProtectPluginIdentifier.sql" />
|
||||
<None Remove="Data\Scripts\06.Events.sql" />
|
||||
<None Remove="Data\Scripts\07.Settings.sql" />
|
||||
<None Remove="Data\Scripts\01.migrations.sql"/>
|
||||
<None Remove="Data\Scripts\02.AspNetIdentity.sql"/>
|
||||
<None Remove="Data\Scripts\03.Init.sql"/>
|
||||
<None Remove="Data\Scripts\04.FixGetLatestVersions.sql"/>
|
||||
<None Remove="Data\Scripts\05.ProtectPluginIdentifier.sql"/>
|
||||
<None Remove="Data\Scripts\06.Events.sql"/>
|
||||
<None Remove="Data\Scripts\07.Settings.sql"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Components\Icon\Default.cshtml" />
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-300.woff2" />
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-300italic.woff2" />
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-700.woff2" />
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-700italic.woff2" />
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-italic.woff2" />
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-regular.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-300.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-300italic.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-600.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-600italic.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-700.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-700italic.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-800.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-800italic.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-italic.woff2" />
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-regular.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-mono-v12-vietnamese_latin-ext_latin_greek_cyrillic-ext_cyrillic-regular.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2" />
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2" />
|
||||
<None Include="wwwroot\vendor\bootstrap\bootstrap.bundle.min.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\bootstrap.bundle.min.js.map" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\bordered-pulled.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\core.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\fixed-width.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\font-awesome.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\icons.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\larger.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\list.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\mixins.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\path.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\rotated-flipped.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\screen-reader.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\stacked.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\variables.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\font-awesome.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_animated.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_bordered-pulled.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_core.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_fixed-width.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_icons.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_larger.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_list.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_mixins.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_path.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_rotated-flipped.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
|
||||
<None Include="Components\Icon\Default.cshtml"/>
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-300.woff2"/>
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-300italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-700.woff2"/>
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-700italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\montserrat-v14-latin-ext_latin-regular.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-300.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-300italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-600.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-600italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-700.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-700italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-800.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-800italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\open-sans-v17-latin-ext_latin-regular.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-mono-v12-vietnamese_latin-ext_latin_greek_cyrillic-ext_cyrillic-regular.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2"/>
|
||||
<None Include="wwwroot\fonts\roboto-v20-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2"/>
|
||||
<None Include="wwwroot\vendor\bootstrap\bootstrap.bundle.min.js"/>
|
||||
<None Include="wwwroot\vendor\bootstrap\bootstrap.bundle.min.js.map"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\bordered-pulled.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\core.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\fixed-width.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\font-awesome.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\icons.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\larger.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\list.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\mixins.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\path.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\rotated-flipped.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\screen-reader.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\stacked.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\less\variables.less"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\font-awesome.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_animated.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_bordered-pulled.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_core.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_fixed-width.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_icons.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_larger.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_list.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_mixins.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_path.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_rotated-flipped.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss"/>
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss"/>
|
||||
<None Include="wwwroot\vendor\jquery\jquery.js"/>
|
||||
<None Include="wwwroot\vendor\jquery\jquery.min.js"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -122,7 +122,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\vendor\highlight.js\" />
|
||||
<Folder Include="wwwroot\vendor\signalr\" />
|
||||
<Folder Include="wwwroot\vendor\highlight.js\"/>
|
||||
<Folder Include="wwwroot\vendor\signalr\"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -174,7 +174,6 @@ public class Program
|
||||
services.AddSingleton<AzureStorageClient>();
|
||||
services.AddSingleton<ServerEnvironment>();
|
||||
services.AddSingleton<EventAggregator>();
|
||||
services.AddSingleton<GPGKeyService>();
|
||||
services.AddHttpClient();
|
||||
services.AddSingleton<ExternalAccountVerificationService>();
|
||||
services.AddSingleton<EmailService>();
|
||||
|
||||
@ -1,166 +0,0 @@
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Org.BouncyCastle.Bcpg;
|
||||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||
using PluginBuilder.DataModels;
|
||||
using PluginBuilder.Util;
|
||||
using PluginBuilder.Util.Extensions;
|
||||
using PluginBuilder.ViewModels;
|
||||
|
||||
namespace PluginBuilder.Services;
|
||||
|
||||
public class GPGKeyService(DBConnectionFactory connectionFactory)
|
||||
{
|
||||
public bool ValidateArmouredPublicKey(string publicKey, out string message, out PgpKeyViewModel? vm)
|
||||
{
|
||||
publicKey = publicKey.Trim();
|
||||
vm = null;
|
||||
if (publicKey.Contains("-----BEGIN PGP PRIVATE KEY BLOCK-----", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
message = "Private key block detected; upload only the public key";
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
using var publicKeyStream = new MemoryStream(Encoding.ASCII.GetBytes(publicKey));
|
||||
using var decoderStream = PgpUtilities.GetDecoderStream(publicKeyStream);
|
||||
var keyRingBundle = new PgpPublicKeyRingBundle(decoderStream);
|
||||
PgpPublicKey? key = null;
|
||||
|
||||
foreach (PgpPublicKeyRing keyRing in keyRingBundle.GetKeyRings())
|
||||
{
|
||||
foreach (PgpPublicKey k in keyRing.GetPublicKeys())
|
||||
{
|
||||
if (k.GetSignatures().All(sig => ((sig.GetHashedSubPackets()?.GetKeyFlags()) & PgpKeyFlags.CanSign) == 0)) continue;
|
||||
key = k;
|
||||
break;
|
||||
}
|
||||
|
||||
if (key != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
message = "Signing key is required";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool canSign = key.Algorithm switch
|
||||
{
|
||||
PublicKeyAlgorithmTag.RsaGeneral or
|
||||
PublicKeyAlgorithmTag.RsaSign or
|
||||
PublicKeyAlgorithmTag.Dsa or
|
||||
PublicKeyAlgorithmTag.ECDsa or
|
||||
PublicKeyAlgorithmTag.EdDsa => true,
|
||||
_ => false
|
||||
};
|
||||
if (!canSign)
|
||||
{
|
||||
message = "Public key provided does not support signing";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key.GetValidSeconds() != 0 && key.CreationTime.AddSeconds(key.GetValidSeconds()) <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
message = "Key has expired";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key.IsRevoked() || key.GetSignatures().Any(sig => sig.SignatureType == PgpSignature.KeyRevocation))
|
||||
{
|
||||
message = "Key is revoked";
|
||||
return false;
|
||||
}
|
||||
vm = new PgpKeyViewModel
|
||||
{
|
||||
KeyId = key.KeyId.ToString("X"),
|
||||
Fingerprint = BitConverter.ToString(key.GetFingerprint()).Replace("-", ""),
|
||||
PublicKey = publicKey,
|
||||
CreatedDate = key.CreationTime,
|
||||
AddedDate = DateTimeOffset.UtcNow,
|
||||
ValidDays = key.GetValidSeconds() == 0 ? -1 : (long)(key.CreationTime.AddSeconds(key.GetValidSeconds()) - key.CreationTime).TotalDays,
|
||||
Version = key.Version
|
||||
};
|
||||
message = "Public Key validated successfully";
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message = "An error occurred while validating public key";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SignatureProofResponse> VerifyDetachedSignature(string pluginslug, string userId, byte[] rawSignedBytes, IFormFile? signatureFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (signatureFile is not { Length: > 0 })
|
||||
return new SignatureProofResponse(false, "Please upload a valid GPG signature file (.asc)");
|
||||
|
||||
string signatureText;
|
||||
using (var reader = new StreamReader(signatureFile.OpenReadStream()))
|
||||
signatureText = await reader.ReadToEndAsync();
|
||||
|
||||
var publicKey = await GetPluginOwnerPublicKeys(pluginslug, userId);
|
||||
if (string.IsNullOrEmpty(publicKey))
|
||||
{
|
||||
return new SignatureProofResponse(false, "No public keys found for this user. Kindly update your account profile with your GPG public key");
|
||||
}
|
||||
await using Stream pubIn = new MemoryStream(Encoding.ASCII.GetBytes(publicKey));
|
||||
PgpPublicKeyRingBundle pubBundle = new PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(pubIn));
|
||||
|
||||
await using Stream sigIn = new MemoryStream(Encoding.ASCII.GetBytes(signatureText));
|
||||
PgpObjectFactory sigFact = new PgpObjectFactory(PgpUtilities.GetDecoderStream(sigIn));
|
||||
PgpSignatureList sigList = (PgpSignatureList)sigFact.NextPgpObject();
|
||||
|
||||
if (sigList.Count <= 0)
|
||||
{
|
||||
return new SignatureProofResponse(false, "No signature found in armoured file uploaded");
|
||||
}
|
||||
|
||||
PgpSignature signature = sigList[0];
|
||||
PgpPublicKey signingKey = pubBundle.GetPublicKey(signature.KeyId);
|
||||
if (signingKey == null)
|
||||
{
|
||||
return new SignatureProofResponse(false, "File was signed with a key not associated with the user's public key");
|
||||
}
|
||||
signature.InitVerify(signingKey);
|
||||
signature.Update(rawSignedBytes);
|
||||
bool ok = signature.Verify();
|
||||
if (!ok)
|
||||
{
|
||||
return new SignatureProofResponse(false, "Unable to verify signature. Commit mismatch");
|
||||
}
|
||||
var signatureProof = new SignatureProof
|
||||
{
|
||||
Armour = signatureText,
|
||||
KeyId = signature.KeyId.ToString("X"),
|
||||
Fingerprint = BitConverter.ToString(signingKey.GetFingerprint()).Replace("-", ""),
|
||||
SignedAt = signature.CreationTime,
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
return new SignatureProofResponse(true, "Signature verified successfully", signatureProof);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new SignatureProofResponse(false, $"Verification failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetPluginOwnerPublicKeys(string pluginSlug, string userId)
|
||||
{
|
||||
await using var conn = await connectionFactory.Open();
|
||||
var pluginOwners = await conn.GetPluginOwners(pluginSlug);
|
||||
if (pluginOwners == null || pluginOwners.Count != 0 == false) return string.Empty;
|
||||
|
||||
var owner = pluginOwners.FirstOrDefault(o => o.UserId == userId);
|
||||
if (owner == null || string.IsNullOrEmpty(owner.AccountDetail)) return string.Empty;
|
||||
|
||||
var accountSettings = JsonConvert.DeserializeObject<AccountSettings>(owner.AccountDetail, CamelCaseSerializerSettings.Instance);
|
||||
if (accountSettings?.GPGKey == null || string.IsNullOrEmpty(accountSettings.GPGKey.PublicKey)) return string.Empty;
|
||||
|
||||
return accountSettings.GPGKey.PublicKey;
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,6 @@ using Newtonsoft.Json.Linq;
|
||||
using Npgsql;
|
||||
using PluginBuilder.DataModels;
|
||||
using PluginBuilder.Services;
|
||||
using PluginBuilder.ViewModels;
|
||||
using PluginBuilder.ViewModels.Plugin;
|
||||
|
||||
namespace PluginBuilder.Util.Extensions;
|
||||
@ -151,9 +150,8 @@ public static class NpgsqlConnectionExtensions
|
||||
const string sql = """
|
||||
SELECT
|
||||
u."Id" AS "UserId",
|
||||
up.is_primary_owner AS "IsPrimary",
|
||||
u."Email",
|
||||
u."AccountDetail"
|
||||
u."Email" AS "Email",
|
||||
up.is_primary_owner AS "IsPrimary"
|
||||
FROM users_plugins up
|
||||
JOIN "AspNetUsers" u ON u."Id" = up.user_id
|
||||
WHERE up.plugin_slug = @slug
|
||||
@ -199,22 +197,6 @@ public static class NpgsqlConnectionExtensions
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<bool> UpdateVersionReleaseStatus(this NpgsqlConnection connection, PluginSlug pluginSlug, string command, PluginVersion version, SignatureProof? signatureProof = null)
|
||||
{
|
||||
var updated = await connection.ExecuteAsync(
|
||||
"UPDATE versions SET pre_release = @preRelease, signatureproof = CASE WHEN @hasSignature THEN @signatureproof::JSONB WHEN @preRelease THEN NULL ELSE signatureproof END " +
|
||||
"WHERE plugin_slug = @pluginSlug AND ver = @version",
|
||||
new
|
||||
{
|
||||
signatureproof = signatureProof != null ? JsonConvert.SerializeObject(signatureProof, CamelCaseSerializerSettings.Instance) : null,
|
||||
hasSignature = signatureProof != null,
|
||||
pluginSlug = pluginSlug.ToString(),
|
||||
version = version.VersionParts,
|
||||
preRelease = command == "unrelease"
|
||||
});
|
||||
return updated == 1;
|
||||
}
|
||||
|
||||
public static async Task<PluginSlug[]> GetPluginsByUserId(this NpgsqlConnection connection, string userId)
|
||||
{
|
||||
return (await connection.QueryAsync<string>(
|
||||
@ -381,12 +363,6 @@ public static class NpgsqlConnectionExtensions
|
||||
"INSERT INTO settings (key, value) VALUES (@key, @value)",
|
||||
new { key = SettingsKeys.VerifiedEmailForPluginPublish, value = "true" });
|
||||
}
|
||||
if (result.All(r => r.key != SettingsKeys.VerifiedGPGSignatureForPluginRelease))
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
"INSERT INTO settings (key, value) VALUES (@key, @value)",
|
||||
new { key = SettingsKeys.VerifiedGPGSignatureForPluginRelease, value = "false" });
|
||||
}
|
||||
if (result.All(r => r.key != SettingsKeys.VerifiedEmailForLogin))
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
@ -408,17 +384,11 @@ public static class NpgsqlConnectionExtensions
|
||||
return bool.TryParse(settingValue, out var result) && result;
|
||||
}
|
||||
|
||||
public static async Task<bool> RequiresGPGSignatureForPluginRelease(this NpgsqlConnection connection)
|
||||
{
|
||||
var settingValue = await SettingsGetAsync(connection, SettingsKeys.VerifiedGPGSignatureForPluginRelease);
|
||||
return bool.TryParse(settingValue, out var result) && result;
|
||||
}
|
||||
|
||||
public static async Task UpdatePluginAdminSettings(this NpgsqlConnection connection, string settingKey, bool newValue)
|
||||
public static async Task UpdateVerifiedEmailForPluginPublishSetting(this NpgsqlConnection connection, bool newValue)
|
||||
{
|
||||
var stringValue = newValue.ToString().ToLowerInvariant();
|
||||
await connection.ExecuteAsync("UPDATE settings SET value = @Value WHERE key = @Key",
|
||||
new { Value = stringValue, Key = settingKey });
|
||||
new { Value = stringValue, Key = SettingsKeys.VerifiedEmailForPluginPublish });
|
||||
}
|
||||
|
||||
public static async Task<bool> GetVerifiedEmailForLoginSetting(this NpgsqlConnection connection)
|
||||
|
||||
@ -18,7 +18,6 @@ public class AdminPluginSettingViewModel : BasePagingViewModel
|
||||
{
|
||||
public IEnumerable<AdminPluginViewModel> Plugins { get; set; } = new List<AdminPluginViewModel>();
|
||||
public bool VerifiedEmailForPluginPublish { get; set; }
|
||||
public bool VerifiedGPGSignatureForPluginRelease { get; set; }
|
||||
public string SearchText { get; set; } = null!;
|
||||
public string? Status { get; set; }
|
||||
public override int CurrentPageCount => Plugins.Count();
|
||||
|
||||
@ -18,5 +18,4 @@ public class BuildViewModel
|
||||
public string GitRef { get; internal set; }
|
||||
public string RepositoryLink { get; internal set; }
|
||||
public string Logs { get; set; }
|
||||
public bool RequireGPGSignatureForRelease { get; set; }
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
namespace PluginBuilder.ViewModels;
|
||||
|
||||
public class PgpKeyViewModel
|
||||
{
|
||||
public string? KeyId { get; set; }
|
||||
public string? Fingerprint { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
public DateTimeOffset AddedDate { get; set; }
|
||||
public long ValidDays { get; set; }
|
||||
public int Version { get; set; }
|
||||
}
|
||||
|
||||
public record SignatureProofResponse(bool valid, string message, SignatureProof? proof = null);
|
||||
|
||||
public class SignatureProof
|
||||
{
|
||||
public string Armour { get; init; }
|
||||
public string KeyId { get; init; }
|
||||
public string Fingerprint { get; init; }
|
||||
public DateTime SignedAt { get; init; }
|
||||
public DateTimeOffset VerifiedAt { get; init; }
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PluginBuilder.ViewModels.Plugin;
|
||||
|
||||
public class PluginOwnersPageViewModel
|
||||
@ -13,8 +11,6 @@ public class PluginOwnersPageViewModel
|
||||
|
||||
public record OwnerVm(
|
||||
string UserId,
|
||||
bool IsPrimary,
|
||||
string? Email,
|
||||
string? AccountDetail
|
||||
bool IsPrimary
|
||||
);
|
||||
|
||||
|
||||
@ -15,15 +15,13 @@
|
||||
{
|
||||
<span class="badge bg-success ml-2">Confirmed</span>
|
||||
}
|
||||
<div class="d-flex align-items-center">
|
||||
<input asp-for="AccountEmail" class="form-control" readonly />
|
||||
<div class="input-group">
|
||||
<input asp-for="AccountEmail" class="form-control" placeholder="" readonly="readonly" />
|
||||
@if (Model.NeedToVerifyEmail)
|
||||
{
|
||||
<a asp-controller="Account"
|
||||
asp-action="VerifyEmail"
|
||||
class="btn btn-sm btn-outline-success ms-2">
|
||||
Verify
|
||||
</a>
|
||||
<div class="input-group-append">
|
||||
<a asp-controller="Account" asp-action="VerifyEmail" class="btn btn-secondary">Verify Email</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -33,15 +31,13 @@
|
||||
{
|
||||
<span class="badge bg-success ml-2">Confirmed</span>
|
||||
}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="input-group">
|
||||
<input asp-for="Settings.Github" class="form-control" placeholder="https://github.com/satoshi" readonly="readonly" />
|
||||
@if (!Model.GithubAccountVerified)
|
||||
{
|
||||
<a asp-controller="Account"
|
||||
asp-action="VerifyGithubAccount"
|
||||
class="btn btn-sm btn-outline-success ms-2">
|
||||
Verify
|
||||
</a>
|
||||
<div class="input-group-append">
|
||||
<a asp-controller="Account" asp-action="VerifyGithubAccount" class="btn btn-secondary">Verify GitHub</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -60,77 +56,9 @@
|
||||
<input asp-for="Settings.Email" class="form-control" type="email" placeholder="plugins@satoshi.com" />
|
||||
<span asp-validation-for="Settings.Email" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="Settings.GpgPublicKey" class="form-label">GPG Public Key</label>
|
||||
@{
|
||||
DateTimeOffset? expiryDate = null;
|
||||
if (Model.Settings.GPGKey?.ValidDays > 0)
|
||||
{
|
||||
expiryDate = Model.Settings.GPGKey.CreatedDate.AddDays(Model.Settings.GPGKey.ValidDays);
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Settings.GPGKey?.Fingerprint) && (!expiryDate.HasValue || expiryDate > DateTimeOffset.UtcNow))
|
||||
{
|
||||
<span class="badge bg-success ms-2">Confirmed</span>
|
||||
}
|
||||
|
||||
@if (expiryDate.HasValue)
|
||||
{
|
||||
if (expiryDate.Value < DateTimeOffset.UtcNow)
|
||||
{
|
||||
<div class="alert alert-danger mt-2">
|
||||
Your GPG key expired on @expiryDate.Value.ToString("yyyy-MM-dd").
|
||||
</div>
|
||||
}
|
||||
else if (expiryDate.Value <= DateTimeOffset.UtcNow.AddMonths(1))
|
||||
{
|
||||
<div class="alert alert-warning mt-2">
|
||||
Your GPG key will expire on @expiryDate.Value.ToString("yyyy-MM-dd").
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div>
|
||||
@if (!string.IsNullOrEmpty(Model.Settings.GPGKey?.Fingerprint))
|
||||
{
|
||||
<div id="existing-key-section" class="d-flex align-items-center">
|
||||
<input class="form-control" readonly
|
||||
value="Fingerprint: @Model.Settings.GPGKey?.Fingerprint" />
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-danger ms-2"
|
||||
onclick="showResetKeyForm();">
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="reset-key-section" class="mt-3" style="display:none;">
|
||||
<textarea asp-for="Settings.GPGKey.PublicKey" class="form-control" rows="8"
|
||||
placeholder="Paste your armored GPG public key here..."></textarea>
|
||||
<span asp-validation-for="Settings.GPGKey.PublicKey" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<textarea asp-for="Settings.GPGKey.PublicKey" class="form-control" rows="8"
|
||||
placeholder="Paste your armored GPG public key here..."></textarea>
|
||||
<span asp-validation-for="Settings.GPGKey.PublicKey" class="text-danger"></span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<input type="submit" class="btn btn-primary" id="Save" value="Save" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@section FooterScripts {
|
||||
<script>
|
||||
function showResetKeyForm() {
|
||||
document.getElementById("existing-key-section").style.display = "none";
|
||||
document.getElementById("reset-key-section").style.display = "block";
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
@ -9,17 +9,17 @@
|
||||
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center gap-3">
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<form asp-controller="Admin" asp-action="UpdateVerifiedEmailForPublishRequirement" method="post" class="d-flex align-items-center">
|
||||
<input type="checkbox" class="btcpay-toggle me-2" id="featureToggle" asp-for="@Model.VerifiedEmailForPluginPublish"
|
||||
onchange="this.form.submit()" />
|
||||
<label class="form-check-label text-wrap" for="featureToggle">
|
||||
Require Verified Email for publishing
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<form asp-controller="Admin" asp-action="UpdateVerifiedEmailForPublishRequirement" method="post" class="d-flex align-items-center">
|
||||
<input type="checkbox" class="btcpay-toggle me-2" id="featureToggle" asp-for="@Model.VerifiedEmailForPluginPublish"
|
||||
onchange="this.form.submit()" />
|
||||
<label class="form-check-label text-wrap" for="featureToggle">
|
||||
Require Verified Email for publishing
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (Model.Plugins.Any(m => !m.PreRelease && !string.IsNullOrEmpty(m.Version)))
|
||||
@if (Model.Plugins.Where(m => !m.PreRelease && !string.IsNullOrEmpty(m.Version)).Any())
|
||||
{
|
||||
<a asp-controller="Admin" asp-action="EmailSender"
|
||||
asp-route-to="@string.Join(",", Model.Plugins.Where(m => !m.PreRelease && !string.IsNullOrEmpty(m.Version)).Select(m => m.PrimaryOwnerEmail))"
|
||||
|
||||
@ -26,18 +26,9 @@
|
||||
case "uploaded":
|
||||
if (Model.Version is null || Model.Version?.PreRelease is true)
|
||||
{
|
||||
if (Model.RequireGPGSignatureForRelease)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#signReleaseModal">
|
||||
Sign and Release
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="Release" asp-route-pluginSlug="@Model.FullBuildId.PluginSlug" asp-route-version="@Model.Version.Version" method="post">
|
||||
<button type="submit" name="command" value="release" class="btn btn-primary">Release</button>
|
||||
</form>
|
||||
}
|
||||
<form asp-action="Release" asp-route-pluginSlug="@Model.FullBuildId.PluginSlug" asp-route-version="@Model.Version.Version" method="post">
|
||||
<button type="submit" name="command" value="release" class="btn btn-primary">Release</button>
|
||||
</form>
|
||||
<a asp-action="CreateBuild" asp-route-pluginSlug="@Model.FullBuildId.PluginSlug" asp-route-copyBuild="@Model.FullBuildId.BuildId"
|
||||
class="btn btn-secondary">Retry</a>
|
||||
}
|
||||
@ -61,39 +52,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="signReleaseModal" tabindex="-1" aria-labelledby="signReleaseModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="signReleaseModalLabel">Sign and Release Plugin Version</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<p>Please sign the commit number shown below using your GPG keys, then upload a detached GPG signature (.asc)</p>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<p class="mb-0">
|
||||
Commit: <code id="commitHash">@Model.Commit</code>
|
||||
</p>
|
||||
<span role="button" style="cursor:pointer" onclick="downloadCommitFile()" title="Download commit txt file">
|
||||
<vc:icon symbol="actions-download" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form asp-action="Release" asp-route-pluginSlug="@Model.FullBuildId.PluginSlug" asp-route-version="@(Model.Version?.Version ?? "")" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="command" value="sign_release" />
|
||||
<div class="mb-3">
|
||||
<label for="signatureFile" class="form-label">Upload Signature (.asc)</label>
|
||||
<input type="file" class="form-control" id="signatureFile" name="signatureFile" accept=".asc" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Verify & Release</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table">
|
||||
@if (Model.Version != null)
|
||||
{
|
||||
@ -116,7 +74,7 @@
|
||||
else
|
||||
{
|
||||
@("@")
|
||||
<a href="@Model.RepositoryLink" target="_blank" rel="noreferrer noopener">@Model.Commit?.Substring(0, 8)</a>
|
||||
<a href="@Model.RepositoryLink" target="_blank" rel="noreferrer noopener">@Model.Commit</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@ -183,20 +141,4 @@
|
||||
<script src="~/vendor/highlight.js/highlight.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/signalr/signalr.min.js" asp-append-version="true"></script>
|
||||
<script src="~/scripts/Build.js" asp-append-version="true"></script>
|
||||
|
||||
<script>
|
||||
function downloadCommitFile() {
|
||||
const commit = document.getElementById('commitHash').textContent.trim();
|
||||
const blob = new Blob([commit], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `commit-${commit}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user