From 0174e7c64ca671fb15289d0cbd1203cc795b8ac7 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 28 Jan 2025 16:09:34 +0100 Subject: [PATCH 1/8] Add token reporting endpoint --- .../Tokens/InvalidateTokensController.cs | 26 +++++++++++++++++ API/Controller/Tokens/TokenController.cs | 28 ++----------------- API/Models/Requests/CreateTokenRequest.cs | 6 ++++ API/Models/Requests/EditTokenRequest.cs | 14 ++++++++++ .../Requests/InvalidateTokensRequest.cs | 6 ++++ API/Models/Response/TokenCreatedResponse.cs | 7 +++++ 6 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 API/Controller/Tokens/InvalidateTokensController.cs create mode 100644 API/Models/Requests/CreateTokenRequest.cs create mode 100644 API/Models/Requests/EditTokenRequest.cs create mode 100644 API/Models/Requests/InvalidateTokensRequest.cs create mode 100644 API/Models/Response/TokenCreatedResponse.cs diff --git a/API/Controller/Tokens/InvalidateTokensController.cs b/API/Controller/Tokens/InvalidateTokensController.cs new file mode 100644 index 00000000..6c31d3c6 --- /dev/null +++ b/API/Controller/Tokens/InvalidateTokensController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using OpenShock.API.Models.Requests; +using OpenShock.Common.Utils; + +namespace OpenShock.API.Controller.Tokens; + +public sealed partial class TokensController +{ + /// + /// Public reporting endpoint to invalidate potentially compromised tokens + /// + /// + /// The tokens were deleted if found + [HttpPost("invalidate")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task InvalidateTokens([FromBody] InvalidateTokensRequest body) + { + var hashes = body.Secrets.Select(HashingUtils.HashSha256).ToArray(); + await _db.ApiTokens.Where(x => hashes.Contains(x.TokenHash)).ExecuteDeleteAsync(); + + return Ok(); + } +} \ No newline at end of file diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 67d6ec79..87bcb219 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -1,17 +1,13 @@ -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Net.Mime; +using System.Net.Mime; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using OpenShock.API.Models.Requests; using OpenShock.API.Models.Response; -using OpenShock.API.Utils; using OpenShock.Common; using OpenShock.Common.Authentication; -using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Constants; using OpenShock.Common.Errors; -using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; using OpenShock.Common.Utils; @@ -154,24 +150,4 @@ public async Task EditToken([FromRoute] Guid tokenId, [FromBody] return Ok(); } - - public class EditTokenRequest - { - [StringLength(HardLimits.ApiKeyNameMaxLength, MinimumLength = 1, ErrorMessage = "API token length must be between {1} and {2}")] - public required string Name { get; set; } - - [MaxLength(HardLimits.ApiKeyMaxPermissions, ErrorMessage = "API token permissions must be between {1} and {2}")] - public List Permissions { get; set; } = [PermissionType.Shockers_Use]; - } - - public sealed class CreateTokenRequest : EditTokenRequest - { - public DateTime? ValidUntil { get; set; } = null; - } - - public sealed class TokenCreatedResponse - { - public required string Token { get; set; } - public required Guid Id { get; set; } - } } \ No newline at end of file diff --git a/API/Models/Requests/CreateTokenRequest.cs b/API/Models/Requests/CreateTokenRequest.cs new file mode 100644 index 00000000..654d79b5 --- /dev/null +++ b/API/Models/Requests/CreateTokenRequest.cs @@ -0,0 +1,6 @@ +namespace OpenShock.API.Models.Requests; + +public sealed class CreateTokenRequest : EditTokenRequest +{ + public DateTime? ValidUntil { get; set; } = null; +} \ No newline at end of file diff --git a/API/Models/Requests/EditTokenRequest.cs b/API/Models/Requests/EditTokenRequest.cs new file mode 100644 index 00000000..fd64005a --- /dev/null +++ b/API/Models/Requests/EditTokenRequest.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; +using OpenShock.Common.Models; + +namespace OpenShock.API.Models.Requests; + +public class EditTokenRequest +{ + [StringLength(HardLimits.ApiKeyNameMaxLength, MinimumLength = 1, ErrorMessage = "API token length must be between {1} and {2}")] + public required string Name { get; set; } + + [MaxLength(HardLimits.ApiKeyMaxPermissions, ErrorMessage = "API token permissions must be between {1} and {2}")] + public List Permissions { get; set; } = [PermissionType.Shockers_Use]; +} \ No newline at end of file diff --git a/API/Models/Requests/InvalidateTokensRequest.cs b/API/Models/Requests/InvalidateTokensRequest.cs new file mode 100644 index 00000000..d12adebf --- /dev/null +++ b/API/Models/Requests/InvalidateTokensRequest.cs @@ -0,0 +1,6 @@ +namespace OpenShock.API.Models.Requests; + +public class InvalidateTokensRequest +{ + public required IEnumerable Secrets { get; set; } +} \ No newline at end of file diff --git a/API/Models/Response/TokenCreatedResponse.cs b/API/Models/Response/TokenCreatedResponse.cs new file mode 100644 index 00000000..057f4fd9 --- /dev/null +++ b/API/Models/Response/TokenCreatedResponse.cs @@ -0,0 +1,7 @@ +namespace OpenShock.API.Models.Response; + +public sealed class TokenCreatedResponse +{ + public required string Token { get; set; } + public required Guid Id { get; set; } +} \ No newline at end of file From f2e67a8e2eeb344ba4962b510b839d7a343054be Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 29 Jan 2025 15:09:20 +0100 Subject: [PATCH 2/8] Rename and add functionality to ReportTokens endpoint --- .../Tokens/InvalidateTokensController.cs | 26 --------- .../Tokens/ReportTokensController.cs | 55 +++++++++++++++++++ .../Requests/InvalidateTokensRequest.cs | 6 -- API/Models/Requests/ReportTokensRequest.cs | 10 ++++ Common/OpenShockDb/ApiTokenReport.cs | 16 ++++++ Common/OpenShockDb/OpenShockContext.cs | 20 +++++++ 6 files changed, 101 insertions(+), 32 deletions(-) delete mode 100644 API/Controller/Tokens/InvalidateTokensController.cs create mode 100644 API/Controller/Tokens/ReportTokensController.cs delete mode 100644 API/Models/Requests/InvalidateTokensRequest.cs create mode 100644 API/Models/Requests/ReportTokensRequest.cs create mode 100644 Common/OpenShockDb/ApiTokenReport.cs diff --git a/API/Controller/Tokens/InvalidateTokensController.cs b/API/Controller/Tokens/InvalidateTokensController.cs deleted file mode 100644 index 6c31d3c6..00000000 --- a/API/Controller/Tokens/InvalidateTokensController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using OpenShock.API.Models.Requests; -using OpenShock.Common.Utils; - -namespace OpenShock.API.Controller.Tokens; - -public sealed partial class TokensController -{ - /// - /// Public reporting endpoint to invalidate potentially compromised tokens - /// - /// - /// The tokens were deleted if found - [HttpPost("invalidate")] - [AllowAnonymous] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task InvalidateTokens([FromBody] InvalidateTokensRequest body) - { - var hashes = body.Secrets.Select(HashingUtils.HashSha256).ToArray(); - await _db.ApiTokens.Where(x => hashes.Contains(x.TokenHash)).ExecuteDeleteAsync(); - - return Ok(); - } -} \ No newline at end of file diff --git a/API/Controller/Tokens/ReportTokensController.cs b/API/Controller/Tokens/ReportTokensController.cs new file mode 100644 index 00000000..bff1675c --- /dev/null +++ b/API/Controller/Tokens/ReportTokensController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using OpenShock.API.Models.Requests; +using OpenShock.Common.Authentication; +using OpenShock.Common.Errors; +using OpenShock.Common.Problems; +using OpenShock.Common.Services.Turnstile; +using OpenShock.Common.Utils; +using System.Net; + +namespace OpenShock.API.Controller.Tokens; + +public sealed partial class TokensController +{ + /// + /// Endpoint to delete potentially compromised api tokens + /// + /// + /// + /// + /// + /// The tokens were deleted if found + [HttpPost("report")] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task ReportTokens( + [FromBody] ReportTokensRequest body, + [FromServices] ICloudflareTurnstileService turnstileService, + [FromServices] ApiConfig apiConfig, + CancellationToken cancellationToken) + { + var remoteIP = HttpContext.GetRemoteIP(); + + var turnStile = await turnstileService.VerifyUserResponseToken(body.TurnstileResponse, remoteIP, cancellationToken); + if (!turnStile.IsT0) + { + var cfErrors = turnStile.AsT1.Value!; + if (cfErrors.All(err => err == CloduflareTurnstileError.InvalidResponse)) + return Problem(TurnstileError.InvalidTurnstile); + + return Problem(new OpenShockProblem("InternalServerError", "Internal Server Error", HttpStatusCode.InternalServerError)); + } + + _db.ApiTokensReports.Add(new Common.OpenShockDb.ApiTokenReport { Id = Guid.CreateVersion7(), ReportedByUserId = CurrentUser.Id, ReportedByIp = remoteIP, ReportedByUser = CurrentUser }); + await _db.SaveChangesAsync(cancellationToken); + + var hashes = body.Secrets.Select(HashingUtils.HashSha256).ToArray(); + await _db.ApiTokens.Where(x => hashes.Contains(x.TokenHash)).ExecuteDeleteAsync(cancellationToken); + + // TODO: Push to webhook + + return Ok(); + } +} \ No newline at end of file diff --git a/API/Models/Requests/InvalidateTokensRequest.cs b/API/Models/Requests/InvalidateTokensRequest.cs deleted file mode 100644 index d12adebf..00000000 --- a/API/Models/Requests/InvalidateTokensRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenShock.API.Models.Requests; - -public class InvalidateTokensRequest -{ - public required IEnumerable Secrets { get; set; } -} \ No newline at end of file diff --git a/API/Models/Requests/ReportTokensRequest.cs b/API/Models/Requests/ReportTokensRequest.cs new file mode 100644 index 00000000..be3e3b87 --- /dev/null +++ b/API/Models/Requests/ReportTokensRequest.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace OpenShock.API.Models.Requests; + +public class ReportTokensRequest +{ + [Required(AllowEmptyStrings = false)] + public required string TurnstileResponse { get; set; } + public required IEnumerable Secrets { get; set; } +} \ No newline at end of file diff --git a/Common/OpenShockDb/ApiTokenReport.cs b/Common/OpenShockDb/ApiTokenReport.cs new file mode 100644 index 00000000..d2e11403 --- /dev/null +++ b/Common/OpenShockDb/ApiTokenReport.cs @@ -0,0 +1,16 @@ +using System.Net; + +namespace OpenShock.Common.OpenShockDb; + +public class ApiTokenReport +{ + public Guid Id { get; set; } + + public DateTime ReportedAt { get; set; } + + public Guid ReportedByUserId { get; set; } + + public IPAddress ReportedByIp { get; set; } = null!; + + public virtual User ReportedByUser { get; set; } = null!; +} diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 3212bda5..bbf0c511 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -19,6 +19,8 @@ public OpenShockContext(DbContextOptions options) public virtual DbSet ApiTokens { get; set; } + public virtual DbSet ApiTokensReports { get; set; } + public virtual DbSet Devices { get; set; } public virtual DbSet DeviceOtaUpdates { get; set; } @@ -106,6 +108,24 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasConstraintName("fk_user_id"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("api_token_reports_pkey"); + + entity.ToTable("api_token_reports"); + + entity.Property(e => e.Id) + .ValueGeneratedNever() + .HasColumnName("id"); + entity.Property(e => e.ReportedByUserId) + .HasColumnName("reported_by_user_id"); + entity.Property(e => e.ReportedByIp) + .HasColumnName("reported_by_ip"); + entity.Property(e => e.ReportedAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasColumnName("reported_at"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("devices_pkey"); From 1624bae6c2c618b2856ea637d8dbd487bc3a13b2 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 15:12:41 +0100 Subject: [PATCH 3/8] Remove policy --- API/Controller/Tokens/ReportTokensController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/API/Controller/Tokens/ReportTokensController.cs b/API/Controller/Tokens/ReportTokensController.cs index bff1675c..48291fa8 100644 --- a/API/Controller/Tokens/ReportTokensController.cs +++ b/API/Controller/Tokens/ReportTokensController.cs @@ -22,7 +22,6 @@ public sealed partial class TokensController /// /// The tokens were deleted if found [HttpPost("report")] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task ReportTokens( [FromBody] ReportTokensRequest body, From abb6c0cb5c85070bd2a1c3757071d65784273253 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 16:16:04 +0100 Subject: [PATCH 4/8] Add missing fields to report submission --- API/Controller/Tokens/ReportTokensController.cs | 9 ++++++++- Common/OpenShockDb/ApiTokenReport.cs | 10 ++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/API/Controller/Tokens/ReportTokensController.cs b/API/Controller/Tokens/ReportTokensController.cs index 48291fa8..0a5e7e51 100644 --- a/API/Controller/Tokens/ReportTokensController.cs +++ b/API/Controller/Tokens/ReportTokensController.cs @@ -41,7 +41,14 @@ public async Task ReportTokens( return Problem(new OpenShockProblem("InternalServerError", "Internal Server Error", HttpStatusCode.InternalServerError)); } - _db.ApiTokensReports.Add(new Common.OpenShockDb.ApiTokenReport { Id = Guid.CreateVersion7(), ReportedByUserId = CurrentUser.Id, ReportedByIp = remoteIP, ReportedByUser = CurrentUser }); + _db.ApiTokensReports.Add(new Common.OpenShockDb.ApiTokenReport { + Id = Guid.CreateVersion7(), + ReportedAt = DateTimeOffset.UtcNow, + ReportedByUserId = CurrentUser.Id, + ReportedByIp = remoteIP, + ReportedByIpCountry = HttpContext.GetCFIPCountry(), + ReportedByUser = CurrentUser + }); await _db.SaveChangesAsync(cancellationToken); var hashes = body.Secrets.Select(HashingUtils.HashSha256).ToArray(); diff --git a/Common/OpenShockDb/ApiTokenReport.cs b/Common/OpenShockDb/ApiTokenReport.cs index d2e11403..98b4d36a 100644 --- a/Common/OpenShockDb/ApiTokenReport.cs +++ b/Common/OpenShockDb/ApiTokenReport.cs @@ -4,13 +4,15 @@ namespace OpenShock.Common.OpenShockDb; public class ApiTokenReport { - public Guid Id { get; set; } + public required Guid Id { get; set; } - public DateTime ReportedAt { get; set; } + public required DateTimeOffset ReportedAt { get; set; } - public Guid ReportedByUserId { get; set; } + public required Guid ReportedByUserId { get; set; } - public IPAddress ReportedByIp { get; set; } = null!; + public required IPAddress ReportedByIp { get; set; } + + public string? ReportedByIpCountry { get; set; } = null; public virtual User ReportedByUser { get; set; } = null!; } From 3a4d9dbc1efff2c8f4acacf0fc16e21085e19e53 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sun, 25 May 2025 15:34:07 +0200 Subject: [PATCH 5/8] Fix some issues --- .../Tokens/ReportTokensController.cs | 6 ++--- Common/OpenShockDb/ApiTokenReport.cs | 2 +- Common/OpenShockDb/OpenShockContext.cs | 25 ++++++++++++++++--- Common/OpenShockDb/User.cs | 1 + 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/API/Controller/Tokens/ReportTokensController.cs b/API/Controller/Tokens/ReportTokensController.cs index 0a5e7e51..daa3435e 100644 --- a/API/Controller/Tokens/ReportTokensController.cs +++ b/API/Controller/Tokens/ReportTokensController.cs @@ -18,7 +18,6 @@ public sealed partial class TokensController /// /// /// - /// /// /// The tokens were deleted if found [HttpPost("report")] @@ -26,7 +25,6 @@ public sealed partial class TokensController public async Task ReportTokens( [FromBody] ReportTokensRequest body, [FromServices] ICloudflareTurnstileService turnstileService, - [FromServices] ApiConfig apiConfig, CancellationToken cancellationToken) { var remoteIP = HttpContext.GetRemoteIP(); @@ -41,9 +39,9 @@ public async Task ReportTokens( return Problem(new OpenShockProblem("InternalServerError", "Internal Server Error", HttpStatusCode.InternalServerError)); } - _db.ApiTokensReports.Add(new Common.OpenShockDb.ApiTokenReport { + _db.ApiTokenReports.Add(new Common.OpenShockDb.ApiTokenReport { Id = Guid.CreateVersion7(), - ReportedAt = DateTimeOffset.UtcNow, + ReportedAt = DateTime.UtcNow, ReportedByUserId = CurrentUser.Id, ReportedByIp = remoteIP, ReportedByIpCountry = HttpContext.GetCFIPCountry(), diff --git a/Common/OpenShockDb/ApiTokenReport.cs b/Common/OpenShockDb/ApiTokenReport.cs index 98b4d36a..9f69584e 100644 --- a/Common/OpenShockDb/ApiTokenReport.cs +++ b/Common/OpenShockDb/ApiTokenReport.cs @@ -6,7 +6,7 @@ public class ApiTokenReport { public required Guid Id { get; set; } - public required DateTimeOffset ReportedAt { get; set; } + public required DateTime ReportedAt { get; set; } public required Guid ReportedByUserId { get; set; } diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 977be475..74e4afc4 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -78,7 +78,7 @@ public static void ConfigureOptionsBuilder(DbContextOptionsBuilder optionsBuilde public DbSet ApiTokens { get; set; } - public DbSet ApiTokensReports { get; set; } + public DbSet ApiTokenReports { get; set; } public DbSet Devices { get; set; } @@ -171,6 +171,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasConstraintName("fk_api_tokens_user_id"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("api_token_reports_pkey"); + + entity.ToTable("api_token_reports"); + + entity.Property(e => e.Id) + .ValueGeneratedNever() + .HasColumnName("id"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("api_token_reports_pkey"); @@ -180,13 +191,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Id) .ValueGeneratedNever() .HasColumnName("id"); + entity.Property(e => e.ReportedAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasColumnName("reported_at"); entity.Property(e => e.ReportedByUserId) .HasColumnName("reported_by_user_id"); entity.Property(e => e.ReportedByIp) .HasColumnName("reported_by_ip"); - entity.Property(e => e.ReportedAt) - .HasDefaultValueSql("CURRENT_TIMESTAMP") - .HasColumnName("reported_at"); + entity.Property(e => e.ReportedByIpCountry) + .HasColumnName("reported_by_ip_country"); + + entity.HasOne(r => r.ReportedByUser).WithMany(u => u.ReportedApiTokens) + .HasForeignKey(r => r.ReportedByUserId) + .HasConstraintName("fk_api_token_reports_reported_by_user_id"); }); modelBuilder.Entity(entity => diff --git a/Common/OpenShockDb/User.cs b/Common/OpenShockDb/User.cs index d429f632..6d120fc9 100644 --- a/Common/OpenShockDb/User.cs +++ b/Common/OpenShockDb/User.cs @@ -21,6 +21,7 @@ public sealed class User public UserActivationRequest? UserActivationRequest { get; set; } public UserDeactivation? UserDeactivation { get; set; } public ICollection ApiTokens { get; } = []; + public ICollection ReportedApiTokens { get; } = []; public ICollection Devices { get; } = []; public ICollection IncomingUserShares { get; } = []; public ICollection OutgoingUserShareInvites { get; } = []; From c67af2b6a89b8ac67c3ed1dac12ef63182a620e2 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sun, 25 May 2025 20:40:39 +0200 Subject: [PATCH 6/8] Add basic webhook test --- .../Tokens/{ReportTokensController.cs => ReportTokens.cs} | 8 ++++++-- API/Models/Requests/ReportTokensRequest.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename API/Controller/Tokens/{ReportTokensController.cs => ReportTokens.cs} (87%) diff --git a/API/Controller/Tokens/ReportTokensController.cs b/API/Controller/Tokens/ReportTokens.cs similarity index 87% rename from API/Controller/Tokens/ReportTokensController.cs rename to API/Controller/Tokens/ReportTokens.cs index daa3435e..50e0bb77 100644 --- a/API/Controller/Tokens/ReportTokensController.cs +++ b/API/Controller/Tokens/ReportTokens.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authorization; +using System.Drawing; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OpenShock.API.Models.Requests; @@ -8,6 +9,7 @@ using OpenShock.Common.Services.Turnstile; using OpenShock.Common.Utils; using System.Net; +using OpenShock.Common.Services.Webhook; namespace OpenShock.API.Controller.Tokens; @@ -25,6 +27,7 @@ public sealed partial class TokensController public async Task ReportTokens( [FromBody] ReportTokensRequest body, [FromServices] ICloudflareTurnstileService turnstileService, + [FromServices] IWebhookService webhookService, CancellationToken cancellationToken) { var remoteIP = HttpContext.GetRemoteIP(); @@ -52,7 +55,8 @@ public async Task ReportTokens( var hashes = body.Secrets.Select(HashingUtils.HashSha256).ToArray(); await _db.ApiTokens.Where(x => hashes.Contains(x.TokenHash)).ExecuteDeleteAsync(cancellationToken); - // TODO: Push to webhook + await webhookService.SendWebhook("TokensReported", + $"Someone reported {body.Secrets.Length} secret(s) as leaked", "AAA", Color.OrangeRed); return Ok(); } diff --git a/API/Models/Requests/ReportTokensRequest.cs b/API/Models/Requests/ReportTokensRequest.cs index be3e3b87..7b3dd97f 100644 --- a/API/Models/Requests/ReportTokensRequest.cs +++ b/API/Models/Requests/ReportTokensRequest.cs @@ -6,5 +6,5 @@ public class ReportTokensRequest { [Required(AllowEmptyStrings = false)] public required string TurnstileResponse { get; set; } - public required IEnumerable Secrets { get; set; } + public required string[] Secrets { get; set; } } \ No newline at end of file From 47506449b4764723c57818769c618b1cdf46218e Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sun, 25 May 2025 20:41:38 +0200 Subject: [PATCH 7/8] Add migrations --- ...50525183650_AddApiTokenReports.Designer.cs | 1273 +++++++++++++++++ .../20250525183650_AddApiTokenReports.cs | 49 + .../OpenShockContextModelSnapshot.cs | 47 + 3 files changed, 1369 insertions(+) create mode 100644 Common/Migrations/20250525183650_AddApiTokenReports.Designer.cs create mode 100644 Common/Migrations/20250525183650_AddApiTokenReports.cs diff --git a/Common/Migrations/20250525183650_AddApiTokenReports.Designer.cs b/Common/Migrations/20250525183650_AddApiTokenReports.Designer.cs new file mode 100644 index 00000000..5bb3b9a0 --- /dev/null +++ b/Common/Migrations/20250525183650_AddApiTokenReports.Designer.cs @@ -0,0 +1,1273 @@ +// +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + [DbContext(typeof(MigrationOpenShockContext))] + [Migration("20250525183650_AddApiTokenReports")] + partial class AddApiTokenReports + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "support", "staff", "admin", "system" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.AdminUsersView", b => + { + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("ApiTokenCount") + .HasColumnType("integer") + .HasColumnName("api_token_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeactivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deactivated_at"); + + b.Property("DeactivatedByUserId") + .HasColumnType("uuid") + .HasColumnName("deactivated_by_user_id"); + + b.Property("DeviceCount") + .HasColumnType("integer") + .HasColumnName("device_count"); + + b.Property("Email") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("email"); + + b.Property("EmailChangeRequestCount") + .HasColumnType("integer") + .HasColumnName("email_change_request_count"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("name"); + + b.Property("NameChangeRequestCount") + .HasColumnType("integer") + .HasColumnName("name_change_request_count"); + + b.Property("PasswordHashType") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("password_hash_type"); + + b.Property("PasswordResetCount") + .HasColumnType("integer") + .HasColumnName("password_reset_count"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("role_type[]") + .HasColumnName("roles"); + + b.Property("ShockerControlLogCount") + .HasColumnType("integer") + .HasColumnName("shocker_control_log_count"); + + b.Property("ShockerCount") + .HasColumnType("integer") + .HasColumnName("shocker_count"); + + b.Property("ShockerPublicShareCount") + .HasColumnType("integer") + .HasColumnName("shocker_public_share_count"); + + b.Property("ShockerUserShareCount") + .HasColumnType("integer") + .HasColumnName("shocker_user_share_count"); + + b.ToTable((string)null); + + b.ToView("admin_users_view", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedByIp") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("created_by_ip"); + + b.Property("LastUsed") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used") + .HasDefaultValueSql("'-infinity'::timestamp without time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.PrimitiveCollection>("Permissions") + .IsRequired() + .HasColumnType("permission_type[]") + .HasColumnName("permissions"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_until"); + + b.HasKey("Id") + .HasName("api_tokens_pkey"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("ValidUntil"); + + b.ToTable("api_tokens", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ReportedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("reported_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ReportedByIp") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("reported_by_ip"); + + b.Property("ReportedByIpCountry") + .HasColumnType("text") + .HasColumnName("reported_by_ip_country"); + + b.Property("ReportedByUserId") + .HasColumnType("uuid") + .HasColumnName("reported_by_user_id"); + + b.HasKey("Id") + .HasName("api_token_reports_pkey"); + + b.HasIndex("ReportedByUserId"); + + b.ToTable("api_token_reports", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("token"); + + b.HasKey("Id") + .HasName("devices_pkey"); + + b.HasIndex("OwnerId"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("devices", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.Property("DeviceId") + .HasColumnType("uuid") + .HasColumnName("device_id"); + + b.Property("UpdateId") + .HasColumnType("integer") + .HasColumnName("update_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("message"); + + b.Property("Status") + .HasColumnType("ota_update_status") + .HasColumnName("status"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("version"); + + b.HasKey("DeviceId", "UpdateId") + .HasName("device_ota_updates_pkey"); + + b.HasIndex(new[] { "CreatedAt" }, "device_ota_updates_created_at_idx"); + + b.ToTable("device_ota_updates", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DiscordWebhook", b => + { + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("WebhookId") + .HasColumnType("bigint") + .HasColumnName("webhook_id"); + + b.Property("WebhookToken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("webhook_token"); + + b.HasKey("Name") + .HasName("discord_webhooks_pkey"); + + b.ToTable("discord_webhooks", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.HasKey("Id") + .HasName("public_shares_pkey"); + + b.HasIndex("OwnerId"); + + b.ToTable("public_shares", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShareShocker", b => + { + b.Property("PublicShareId") + .HasColumnType("uuid") + .HasColumnName("public_share_id"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("AllowLiveControl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("allow_livecontrol"); + + b.Property("AllowShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_shock"); + + b.Property("AllowSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_sound"); + + b.Property("AllowVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_vibrate"); + + b.Property("Cooldown") + .HasColumnType("integer") + .HasColumnName("cooldown"); + + b.Property("IsPaused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_paused"); + + b.Property("MaxDuration") + .HasColumnType("integer") + .HasColumnName("max_duration"); + + b.Property("MaxIntensity") + .HasColumnType("smallint") + .HasColumnName("max_intensity"); + + b.HasKey("PublicShareId", "ShockerId") + .HasName("public_share_shockers_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("public_share_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DeviceId") + .HasColumnType("uuid") + .HasColumnName("device_id"); + + b.Property("IsPaused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_paused"); + + b.Property("Model") + .HasColumnType("shocker_model_type") + .HasColumnName("model"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("RfId") + .HasColumnType("integer") + .HasColumnName("rf_id"); + + b.HasKey("Id") + .HasName("shockers_pkey"); + + b.HasIndex("DeviceId"); + + b.ToTable("shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ControlledByUserId") + .HasColumnType("uuid") + .HasColumnName("controlled_by_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CustomName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("custom_name"); + + b.Property("Duration") + .HasColumnType("bigint") + .HasColumnName("duration"); + + b.Property("Intensity") + .HasColumnType("smallint") + .HasColumnName("intensity"); + + b.Property("LiveControl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("live_control"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("Type") + .HasColumnType("control_type") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("shocker_control_logs_pkey"); + + b.HasIndex("ControlledByUserId"); + + b.HasIndex("ShockerId"); + + b.ToTable("shocker_control_logs", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowLiveControl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_livecontrol"); + + b.Property("AllowShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_shock"); + + b.Property("AllowSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_sound"); + + b.Property("AllowVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_vibrate"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsPaused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_paused"); + + b.Property("MaxDuration") + .HasColumnType("integer") + .HasColumnName("max_duration"); + + b.Property("MaxIntensity") + .HasColumnType("smallint") + .HasColumnName("max_intensity"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.HasKey("Id") + .HasName("shocker_share_codes_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("shocker_share_codes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("name") + .UseCollation("ndcoll"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("password_hash"); + + b.PrimitiveCollection>("Roles") + .IsRequired() + .HasColumnType("role_type[]") + .HasColumnName("roles"); + + b.HasKey("Id") + .HasName("users_pkey"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.UseCollation(b.HasIndex("Name"), new[] { "ndcoll" }); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserActivationRequest", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EmailSendAttempts") + .HasColumnType("integer") + .HasColumnName("email_send_attempts"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.HasKey("UserId") + .HasName("user_activation_requests_pkey"); + + b.ToTable("user_activation_requests", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserDeactivation", b => + { + b.Property("DeactivatedUserId") + .HasColumnType("uuid") + .HasColumnName("deactivated_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DeactivatedByUserId") + .HasColumnType("uuid") + .HasColumnName("deactivated_by_user_id"); + + b.Property("DeleteLater") + .HasColumnType("boolean") + .HasColumnName("delete_later"); + + b.Property("UserModerationId") + .HasColumnType("uuid") + .HasColumnName("user_moderation_id"); + + b.HasKey("DeactivatedUserId") + .HasName("user_deactivations_pkey"); + + b.HasIndex("DeactivatedByUserId"); + + b.ToTable("user_deactivations", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserEmailChange", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("user_email_changes_pkey"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UsedAt"); + + b.HasIndex("UserId"); + + b.ToTable("user_email_changes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OldName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("old_name"); + + b.HasKey("Id", "UserId") + .HasName("user_name_changes_pkey"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("OldName"); + + b.HasIndex("UserId"); + + b.ToTable("user_name_changes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserPasswordReset", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("secret"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("user_password_resets_pkey"); + + b.HasIndex("UserId"); + + b.ToTable("user_password_resets", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShare", b => + { + b.Property("SharedWithUserId") + .HasColumnType("uuid") + .HasColumnName("shared_with_user_id"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("AllowLiveControl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_livecontrol"); + + b.Property("AllowShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_shock"); + + b.Property("AllowSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_sound"); + + b.Property("AllowVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_vibrate"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsPaused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_paused"); + + b.Property("MaxDuration") + .HasColumnType("integer") + .HasColumnName("max_duration"); + + b.Property("MaxIntensity") + .HasColumnType("smallint") + .HasColumnName("max_intensity"); + + b.HasKey("SharedWithUserId", "ShockerId") + .HasName("user_shares_pkey"); + + b.HasIndex("SharedWithUserId"); + + b.HasIndex("ShockerId"); + + b.ToTable("user_shares", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("user_share_invites_pkey"); + + b.HasIndex("OwnerId"); + + b.HasIndex("RecipientUserId"); + + b.ToTable("user_share_invites", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInviteShocker", b => + { + b.Property("InviteId") + .HasColumnType("uuid") + .HasColumnName("invite_id"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("AllowLiveControl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_livecontrol"); + + b.Property("AllowShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_shock"); + + b.Property("AllowSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_sound"); + + b.Property("AllowVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("allow_vibrate"); + + b.Property("IsPaused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_paused"); + + b.Property("MaxDuration") + .HasColumnType("integer") + .HasColumnName("max_duration"); + + b.Property("MaxIntensity") + .HasColumnType("smallint") + .HasColumnName("max_intensity"); + + b.HasKey("InviteId", "ShockerId") + .HasName("user_share_invite_shockers_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("user_share_invite_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("ApiTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_tokens_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "ReportedByUser") + .WithMany("ReportedApiTokens") + .HasForeignKey("ReportedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_token_reports_reported_by_user_id"); + + b.Navigation("ReportedByUser"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") + .WithMany("Devices") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_devices_owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "Device") + .WithMany("OtaUpdates") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_device_ota_updates_device_id"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") + .WithMany("OwnedPublicShares") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_public_shares_owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShareShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.PublicShare", "PublicShare") + .WithMany("ShockerMappings") + .HasForeignKey("PublicShareId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_public_share_shockers_public_share_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("PublicShareMappings") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_public_share_shockers_shocker_id"); + + b.Navigation("PublicShare"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "Device") + .WithMany("Shockers") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shockers_device_id"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "ControlledByUser") + .WithMany("ShockerControlLogs") + .HasForeignKey("ControlledByUserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_shocker_control_logs_controlled_by_user_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerControlLogs") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shocker_control_logs_shocker_id"); + + b.Navigation("ControlledByUser"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerShareCodes") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shocker_share_codes_shocker_id"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserActivationRequest", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithOne("UserActivationRequest") + .HasForeignKey("OpenShock.Common.OpenShockDb.UserActivationRequest", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_activation_requests_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserDeactivation", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "DeactivatedByUser") + .WithMany() + .HasForeignKey("DeactivatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_deactivations_deactivated_by_user_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.User", "DeactivatedUser") + .WithOne("UserDeactivation") + .HasForeignKey("OpenShock.Common.OpenShockDb.UserDeactivation", "DeactivatedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_deactivations_deactivated_user_id"); + + b.Navigation("DeactivatedByUser"); + + b.Navigation("DeactivatedUser"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserEmailChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("EmailChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_email_changes_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("NameChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_name_changes_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserPasswordReset", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("PasswordResets") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_password_resets_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShare", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "SharedWithUser") + .WithMany("IncomingUserShares") + .HasForeignKey("SharedWithUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_shares_shared_with_user_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("UserShares") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_shares_shocker_id"); + + b.Navigation("SharedWithUser"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") + .WithMany("OutgoingUserShareInvites") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_share_invites_owner_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.User", "RecipientUser") + .WithMany("IncomingUserShareInvites") + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_user_share_invites_recipient_user_id"); + + b.Navigation("Owner"); + + b.Navigation("RecipientUser"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInviteShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.UserShareInvite", "Invite") + .WithMany("ShockerMappings") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_share_invite_shockers_invite_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("UserShareInviteShockerMappings") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_share_invite_shockers_shocker_id"); + + b.Navigation("Invite"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Navigation("OtaUpdates"); + + b.Navigation("Shockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b => + { + b.Navigation("ShockerMappings"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Navigation("PublicShareMappings"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShareCodes"); + + b.Navigation("UserShareInviteShockerMappings"); + + b.Navigation("UserShares"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Navigation("ApiTokens"); + + b.Navigation("Devices"); + + b.Navigation("EmailChanges"); + + b.Navigation("IncomingUserShareInvites"); + + b.Navigation("IncomingUserShares"); + + b.Navigation("NameChanges"); + + b.Navigation("OutgoingUserShareInvites"); + + b.Navigation("OwnedPublicShares"); + + b.Navigation("PasswordResets"); + + b.Navigation("ReportedApiTokens"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("UserActivationRequest"); + + b.Navigation("UserDeactivation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b => + { + b.Navigation("ShockerMappings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Common/Migrations/20250525183650_AddApiTokenReports.cs b/Common/Migrations/20250525183650_AddApiTokenReports.cs new file mode 100644 index 00000000..3bf86a5b --- /dev/null +++ b/Common/Migrations/20250525183650_AddApiTokenReports.cs @@ -0,0 +1,49 @@ +using System; +using System.Net; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class AddApiTokenReports : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api_token_reports", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + reported_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + reported_by_user_id = table.Column(type: "uuid", nullable: false), + reported_by_ip = table.Column(type: "inet", nullable: false), + reported_by_ip_country = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("api_token_reports_pkey", x => x.id); + table.ForeignKey( + name: "fk_api_token_reports_reported_by_user_id", + column: x => x.reported_by_user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_api_token_reports_reported_by_user_id", + table: "api_token_reports", + column: "reported_by_user_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api_token_reports"); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 12d9ebb5..4008538c 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -176,6 +176,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("api_tokens", (string)null); }); + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ReportedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("reported_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ReportedByIp") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("reported_by_ip"); + + b.Property("ReportedByIpCountry") + .HasColumnType("text") + .HasColumnName("reported_by_ip_country"); + + b.Property("ReportedByUserId") + .HasColumnType("uuid") + .HasColumnName("reported_by_user_id"); + + b.HasKey("Id") + .HasName("api_token_reports_pkey"); + + b.HasIndex("ReportedByUserId"); + + b.ToTable("api_token_reports", (string)null); + }); + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => { b.Property("Id") @@ -929,6 +962,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "ReportedByUser") + .WithMany("ReportedApiTokens") + .HasForeignKey("ReportedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_token_reports_reported_by_user_id"); + + b.Navigation("ReportedByUser"); + }); + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => { b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") @@ -1206,6 +1251,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("PasswordResets"); + b.Navigation("ReportedApiTokens"); + b.Navigation("ShockerControlLogs"); b.Navigation("UserActivationRequest"); From b35972b387ac7761c51d9359265d08b19c1c2cdf Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sun, 25 May 2025 20:44:15 +0200 Subject: [PATCH 8/8] Update ReportTokens.cs --- API/Controller/Tokens/ReportTokens.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/API/Controller/Tokens/ReportTokens.cs b/API/Controller/Tokens/ReportTokens.cs index 50e0bb77..f97cefc6 100644 --- a/API/Controller/Tokens/ReportTokens.cs +++ b/API/Controller/Tokens/ReportTokens.cs @@ -20,6 +20,7 @@ public sealed partial class TokensController /// /// /// + /// /// /// The tokens were deleted if found [HttpPost("report")]