Account flags

This commit is contained in:
2024-08-26 13:33:41 +03:00
parent 3e99c53fb3
commit 56feac7f9d
11 changed files with 631 additions and 25 deletions

View File

@ -5,5 +5,11 @@ namespace NostrStreamer.ApiModel;
public class PatchAccount public class PatchAccount
{ {
[JsonProperty("accept_tos")] [JsonProperty("accept_tos")]
public bool AcceptTos { get; init; } public bool? AcceptTos { get; init; }
[JsonProperty("blocked")]
public bool? Blocked { get; init; }
[JsonProperty("admin")]
public bool? Admin { get; init; }
} }

View File

@ -1,18 +1,28 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NostrStreamer.ApiModel;
using NostrStreamer.Services;
using NostrStreamer.Services.Dvr;
using NostrStreamer.Services.StreamManager; using NostrStreamer.Services.StreamManager;
namespace NostrStreamer.Controllers; namespace NostrStreamer.Controllers;
[Authorize(AuthenticationSchemes = NostrAuth.Scheme, Roles = NostrAuth.RoleAdmin)]
[Route("/api/admin")] [Route("/api/admin")]
public class AdminController : Controller public class AdminController : Controller
{ {
private readonly ILogger<AdminController> _logger; private readonly ILogger<AdminController> _logger;
private readonly StreamManagerFactory _streamManagerFactory; private readonly StreamManagerFactory _streamManagerFactory;
private readonly IDvrStore _dvrStore;
private readonly UserService _userService;
public AdminController(ILogger<AdminController> logger, StreamManagerFactory streamManagerFactory) public AdminController(ILogger<AdminController> logger, StreamManagerFactory streamManagerFactory, IDvrStore dvrStore,
UserService userService)
{ {
_logger = logger; _logger = logger;
_streamManagerFactory = streamManagerFactory; _streamManagerFactory = streamManagerFactory;
_dvrStore = dvrStore;
_userService = userService;
} }
[HttpPatch("stream/{id:guid}")] [HttpPatch("stream/{id:guid}")]
@ -21,4 +31,26 @@ public class AdminController : Controller
var stream = await _streamManagerFactory.ForStream(id); var stream = await _streamManagerFactory.ForStream(id);
await stream.UpdateEvent(); await stream.UpdateEvent();
} }
[HttpDelete("stream/{id:guid}")]
public async Task DeleteEvent([FromRoute] Guid id)
{
var mgr = await _streamManagerFactory.ForStream(id);
var stream = mgr.GetStream();
await _dvrStore.DeleteRecordings(stream);
}
[HttpPatch("account/{pubkey}")]
public async Task UpdateAccount([FromRoute] string pubkey, [FromBody] PatchAccount req)
{
if (req.Blocked.HasValue)
{
await _userService.SetBlocked(pubkey, req.Blocked.Value);
}
if (req.Admin.HasValue)
{
await _userService.SetAdmin(pubkey, req.Admin.Value);
}
}
} }

View File

@ -136,7 +136,7 @@ public class NostrController : Controller
return NotFound(); return NotFound();
} }
if (patch.AcceptTos) if (patch.AcceptTos.HasValue && patch.AcceptTos.Value)
{ {
await _userService.AcceptTos(user.PubKey); await _userService.AcceptTos(user.PubKey);
} }
@ -267,7 +267,7 @@ public class NostrController : Controller
var existing = await _db.PushSubscriptions.FirstOrDefaultAsync(a => a.Key == sub.Key); var existing = await _db.PushSubscriptions.FirstOrDefaultAsync(a => a.Key == sub.Key);
if (existing != default) if (existing != default)
{ {
return Json(new { id = existing.Id }); return Json(new {id = existing.Id});
} }
var newId = Guid.NewGuid(); var newId = Guid.NewGuid();
@ -298,7 +298,7 @@ public class NostrController : Controller
var sub = await _db.PushSubscriptionTargets var sub = await _db.PushSubscriptionTargets
.Join(_db.PushSubscriptions, a => a.SubscriberPubkey, b => b.Pubkey, .Join(_db.PushSubscriptions, a => a.SubscriberPubkey, b => b.Pubkey,
(a, b) => new { a.SubscriberPubkey, a.TargetPubkey, b.Auth }) (a, b) => new {a.SubscriberPubkey, a.TargetPubkey, b.Auth})
.Where(a => a.SubscriberPubkey == userPubkey && a.Auth == auth) .Where(a => a.SubscriberPubkey == userPubkey && a.Auth == auth)
.Select(a => a.TargetPubkey) .Select(a => a.TargetPubkey)
.ToListAsync(); .ToListAsync();

View File

@ -22,5 +22,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
builder.Property(a => a.Tags); builder.Property(a => a.Tags);
builder.Property(a => a.ContentWarning); builder.Property(a => a.ContentWarning);
builder.Property(a => a.Goal); builder.Property(a => a.Goal);
builder.Property(a => a.IsAdmin);
builder.Property(a => a.IsBlocked);
} }
} }

View File

@ -54,6 +54,16 @@ public class User
/// </summary> /// </summary>
public uint Version { get; set; } public uint Version { get; set; }
/// <summary>
/// User is service admin
/// </summary>
public bool IsAdmin { get; set; }
/// <summary>
/// User is blocked and cannot stream
/// </summary>
public bool IsBlocked { get; set; }
public List<Payment> Payments { get; init; } = new(); public List<Payment> Payments { get; init; } = new();
public List<UserStream> Streams { get; init; } = new(); public List<UserStream> Streams { get; init; } = new();
public List<UserStreamForwards> Forwards { get; init; } = new(); public List<UserStreamForwards> Forwards { get; init; } = new();

View File

@ -0,0 +1,475 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NostrStreamer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NostrStreamer.Migrations
{
[DbContext(typeof(StreamerContext))]
[Migration("20240826103113_AccountFlags")]
partial class AccountFlags
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NostrStreamer.Database.IngestEndpoint", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("App")
.IsRequired()
.HasColumnType("text");
b.Property<List<string>>("Capabilities")
.IsRequired()
.HasColumnType("text[]");
b.Property<int>("Cost")
.HasColumnType("integer");
b.Property<string>("Forward")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("App")
.IsUnique();
b.ToTable("Endpoints");
});
modelBuilder.Entity("NostrStreamer.Database.Payment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("text");
b.Property<decimal>("Amount")
.HasColumnType("numeric(20,0)");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Fee")
.HasColumnType("numeric(20,0)");
b.Property<string>("Invoice")
.HasColumnType("text");
b.Property<bool>("IsPaid")
.HasColumnType("boolean");
b.Property<string>("Nostr")
.HasColumnType("text");
b.Property<string>("PubKey")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("PaymentHash");
b.HasIndex("PubKey");
b.ToTable("Payments");
});
modelBuilder.Entity("NostrStreamer.Database.PushSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("LastUsed")
.HasColumnType("timestamp with time zone");
b.Property<string>("Pubkey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Scope")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PushSubscriptions");
});
modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("SubscriberPubkey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("TargetPubkey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Id");
b.HasIndex("TargetPubkey");
b.HasIndex("SubscriberPubkey", "TargetPubkey")
.IsUnique();
b.ToTable("PushSubscriptionTargets");
});
modelBuilder.Entity("NostrStreamer.Database.User", b =>
{
b.Property<string>("PubKey")
.HasColumnType("text");
b.Property<long>("Balance")
.HasColumnType("bigint");
b.Property<string>("ContentWarning")
.HasColumnType("text");
b.Property<string>("Goal")
.HasColumnType("text");
b.Property<string>("Image")
.HasColumnType("text");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<bool>("IsBlocked")
.HasColumnType("boolean");
b.Property<string>("StreamKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.HasColumnType("text");
b.Property<string>("Tags")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<DateTime?>("TosAccepted")
.HasColumnType("timestamp with time zone");
b.Property<uint>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("PubKey");
b.ToTable("Users");
});
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("EdgeIp")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("EndpointId")
.HasColumnType("uuid");
b.Property<DateTime?>("Ends")
.HasColumnType("timestamp with time zone");
b.Property<string>("Event")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ForwardClientId")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("LastSegment")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Length")
.HasColumnType("numeric");
b.Property<decimal>("MilliSatsCollected")
.HasColumnType("numeric");
b.Property<string>("PubKey")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Starts")
.HasColumnType("timestamp with time zone");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("StreamId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Thumbnail")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("EndpointId");
b.HasIndex("PubKey");
b.ToTable("Streams");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("TakenByPubkey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("Clips");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Target")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UserPubkey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserPubkey");
b.ToTable("Forwards");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("PubKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Relay")
.HasColumnType("text");
b.Property<string>("Role")
.HasColumnType("text");
b.Property<string>("Sig")
.HasColumnType("text");
b.Property<Guid>("StreamId")
.HasColumnType("uuid");
b.Property<decimal>("ZapSplit")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("StreamId", "PubKey")
.IsUnique();
b.ToTable("Guests");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("Duration")
.HasColumnType("double precision");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("Recordings");
});
modelBuilder.Entity("NostrStreamer.Database.Payment", b =>
{
b.HasOne("NostrStreamer.Database.User", "User")
.WithMany("Payments")
.HasForeignKey("PubKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
{
b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint")
.WithMany()
.HasForeignKey("EndpointId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("NostrStreamer.Database.User", "User")
.WithMany("Streams")
.HasForeignKey("PubKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Endpoint");
b.Navigation("User");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
.WithMany()
.HasForeignKey("UserStreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UserStream");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
{
b.HasOne("NostrStreamer.Database.User", "User")
.WithMany("Forwards")
.HasForeignKey("UserPubkey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "Stream")
.WithMany("Guests")
.HasForeignKey("StreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Stream");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "Stream")
.WithMany("Recordings")
.HasForeignKey("UserStreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Stream");
});
modelBuilder.Entity("NostrStreamer.Database.User", b =>
{
b.Navigation("Forwards");
b.Navigation("Payments");
b.Navigation("Streams");
});
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
{
b.Navigation("Guests");
b.Navigation("Recordings");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NostrStreamer.Migrations
{
/// <inheritdoc />
public partial class AccountFlags : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsAdmin",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsBlocked",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsAdmin",
table: "Users");
migrationBuilder.DropColumn(
name: "IsBlocked",
table: "Users");
}
}
}

View File

@ -174,6 +174,12 @@ namespace NostrStreamer.Migrations
b.Property<string>("Image") b.Property<string>("Image")
.HasColumnType("text"); .HasColumnType("text");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<bool>("IsBlocked")
.HasColumnType("boolean");
b.Property<string>("StreamKey") b.Property<string>("StreamKey")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");

View File

@ -2,16 +2,19 @@ using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Nostr.Client.Json; using Nostr.Client.Json;
using Nostr.Client.Messages; using Nostr.Client.Messages;
using NostrStreamer.Database;
namespace NostrStreamer; namespace NostrStreamer;
public static class NostrAuth public static class NostrAuth
{ {
public const string Scheme = "Nostr"; public const string Scheme = "Nostr";
public const string RoleAdmin = "admin";
} }
public class NostrAuthOptions : AuthenticationSchemeOptions public class NostrAuthOptions : AuthenticationSchemeOptions
@ -20,9 +23,12 @@ public class NostrAuthOptions : AuthenticationSchemeOptions
public class NostrAuthHandler : AuthenticationHandler<NostrAuthOptions> public class NostrAuthHandler : AuthenticationHandler<NostrAuthOptions>
{ {
public NostrAuthHandler(IOptionsMonitor<NostrAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : private readonly StreamerContext _db;
base(options, logger, encoder, clock) public NostrAuthHandler(IOptionsMonitor<NostrAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock,
StreamerContext db)
: base(options, logger, encoder, clock)
{ {
_db = db;
} }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@ -69,14 +75,14 @@ public class NostrAuthHandler : AuthenticationHandler<NostrAuthOptions>
var urlTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "u"); var urlTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "u");
var methodTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "method"); var methodTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "method");
if (string.IsNullOrEmpty(urlTag?.AdditionalData[0] as string) || if (string.IsNullOrEmpty(urlTag?.AdditionalData[0]) ||
!new Uri((urlTag.AdditionalData[0] as string)!).AbsolutePath.Equals(Request.Path, StringComparison.InvariantCultureIgnoreCase)) !new Uri(urlTag.AdditionalData[0]).AbsolutePath.Equals(Request.Path, StringComparison.InvariantCultureIgnoreCase))
{ {
return AuthenticateResult.Fail("Invalid nostr event, url tag invalid"); return AuthenticateResult.Fail("Invalid nostr event, url tag invalid");
} }
if (string.IsNullOrEmpty(methodTag?.AdditionalData[0] as string) || if (string.IsNullOrEmpty(methodTag?.AdditionalData[0]) ||
!((methodTag.AdditionalData[0] as string)?.Equals(Request.Method, StringComparison.InvariantCultureIgnoreCase) ?? false)) !methodTag.AdditionalData[0].Equals(Request.Method, StringComparison.InvariantCultureIgnoreCase))
{ {
return AuthenticateResult.Fail("Invalid nostr event, method tag invalid"); return AuthenticateResult.Fail("Invalid nostr event, method tag invalid");
} }
@ -86,6 +92,15 @@ public class NostrAuthHandler : AuthenticationHandler<NostrAuthOptions>
new Claim(ClaimTypes.Name, ev.Pubkey!) new Claim(ClaimTypes.Name, ev.Pubkey!)
}); });
var user = await _db.Users
.AsNoTracking()
.SingleOrDefaultAsync(a => a.PubKey == ev.Pubkey!);
if (user is {IsAdmin: true})
{
principal.AddClaim(new Claim(ClaimTypes.Role, NostrAuth.RoleAdmin));
}
return AuthenticateResult.Success(new(new ClaimsPrincipal(new[] {principal}), Scheme.Name)); return AuthenticateResult.Success(new(new ClaimsPrincipal(new[] {principal}), Scheme.Name));
} }
} }

View File

@ -48,6 +48,11 @@ public class StreamManagerFactory
throw new Exception("TOS not accepted"); throw new Exception("TOS not accepted");
} }
if (user.IsBlocked)
{
throw new Exception("User account blocked");
}
var existingLive = await _db.Streams var existingLive = await _db.Streams
.SingleOrDefaultAsync(a => a.State == UserStreamState.Live && a.PubKey == user.PubKey); .SingleOrDefaultAsync(a => a.State == UserStreamState.Live && a.PubKey == user.PubKey);

View File

@ -105,6 +105,7 @@ public class UserService
await _db.Users await _db.Users
.Where(a => a.PubKey == pubkey) .Where(a => a.PubKey == pubkey)
.ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance - pr.MinimumAmount.MilliSatoshi)); .ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance - pr.MinimumAmount.MilliSatoshi));
_db.Payments.Add(new() _db.Payments.Add(new()
{ {
PubKey = pubkey, PubKey = pubkey,
@ -134,6 +135,7 @@ public class UserService
await _db.Users await _db.Users
.Where(a => a.PubKey == pubkey) .Where(a => a.PubKey == pubkey)
.ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance - result.FeeSat)); .ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance - result.FeeSat));
return (result.FeeMsat, result.PaymentPreimage); return (result.FeeMsat, result.PaymentPreimage);
} }
@ -145,6 +147,7 @@ public class UserService
await _db.Users await _db.Users
.Where(a => a.PubKey == pubkey) .Where(a => a.PubKey == pubkey)
.ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance + pr.MinimumAmount.MilliSatoshi)); .ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance + pr.MinimumAmount.MilliSatoshi));
throw; throw;
} }
} }
@ -218,6 +221,18 @@ public class UserService
if (change != 1) throw new Exception($"Failed to accept TOS, {change} rows updated."); if (change != 1) throw new Exception($"Failed to accept TOS, {change} rows updated.");
} }
public async Task SetBlocked(string pubkey, bool val)
{
await _db.Users.Where(a => a.PubKey.Equals(pubkey))
.ExecuteUpdateAsync(o => o.SetProperty(v => v.IsBlocked, val));
}
public async Task SetAdmin(string pubkey, bool val)
{
await _db.Users.Where(a => a.PubKey.Equals(pubkey))
.ExecuteUpdateAsync(o => o.SetProperty(v => v.IsAdmin, val));
}
public async Task AddForward(string pubkey, string name, string dest) public async Task AddForward(string pubkey, string name, string dest)
{ {
var protector = _dataProtector.CreateProtector("forward-targets"); var protector = _dataProtector.CreateProtector("forward-targets");