From 083e08fd5c79947eff5b01ca862fefb98996338d Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 6 Nov 2024 00:14:18 +0100 Subject: [PATCH 1/6] Initial commit --- API/Controller/Account/Login.cs | 3 +- API/Controller/Account/LoginV2.cs | 3 +- API/Models/Requests/ChangeEmailRequest.cs | 6 +- API/Models/Requests/ChangePasswordRequest.cs | 8 ++- API/Models/Requests/ChangeUsernameRequest.cs | 7 +- API/Models/Requests/CreateShareRequest.cs | 3 +- API/Models/Requests/HubCreateRequest.cs | 3 +- API/Models/Requests/HubEditRequest.cs | 3 +- API/Models/Requests/Login.cs | 6 +- API/Models/Requests/LoginV2.cs | 12 +++- API/Models/Requests/NewShocker.cs | 5 +- API/Models/Requests/ShareLinkCreate.cs | 7 +- API/Models/Requests/ShareLinkEditShocker.cs | 3 +- API/Models/Requests/Signup.cs | 8 ++- API/Models/Requests/SignupV2.cs | 13 ++-- API/Models/Response/ShockerLimits.cs | 5 +- API/Services/Account/AccountService.cs | 13 ++-- Common.Tests/Geo/DistanceLookupTests.cs | 5 +- .../Handlers/LoginSessionAuthentication.cs | 7 +- Common/{ => Constants}/Constants.cs | 12 +--- Common/Constants/Distance.cs | 6 ++ Common/Constants/HardLimits.cs | 48 +++++++++++++ .../DataAnnotations/EmailAddressAttribute.cs | 69 +++++++++++++++++++ Common/DataAnnotations/PasswordAttribute.cs | 69 +++++++++++++++++++ Common/DataAnnotations/UsernameAttribute.cs | 20 +++--- Common/DeviceControl/ControlLogic.cs | 9 +-- Common/Geo/DistanceLookup.cs | 5 +- Common/Models/WebSocket/User/Control.cs | 5 +- Common/Models/WebSocket/User/ControlLog.cs | 5 +- Common/OpenShockDb/OpenShockContext.cs | 23 +++++-- .../LCGNodeProvisioner/LCGNodeProvisioner.cs | 3 +- Common/Validation/UsernameValidator.cs | 5 +- Common/Validation/ValidationConstants.cs | 7 -- Cron/Jobs/ClearOldPasswordResetsJob.cs | 3 +- .../Controllers/DeviceControllerBase.cs | 9 +-- .../Controllers/DeviceV2Controller.cs | 3 +- .../Controllers/LiveControlController.cs | 3 +- .../LifetimeManager/ShockerState.cs | 3 +- 38 files changed, 336 insertions(+), 91 deletions(-) rename Common/{ => Constants}/Constants.cs (65%) create mode 100644 Common/Constants/Distance.cs create mode 100644 Common/Constants/HardLimits.cs create mode 100644 Common/DataAnnotations/EmailAddressAttribute.cs create mode 100644 Common/DataAnnotations/PasswordAttribute.cs delete mode 100644 Common/Validation/ValidationConstants.cs diff --git a/API/Controller/Account/Login.cs b/API/Controller/Account/Login.cs index 21312eea..97833227 100644 --- a/API/Controller/Account/Login.cs +++ b/API/Controller/Account/Login.cs @@ -4,6 +4,7 @@ using Asp.Versioning; using OpenShock.API.Services.Account; using OpenShock.Common; +using OpenShock.Common.Constants; using OpenShock.Common.Errors; using OpenShock.Common.Problems; using OpenShock.Common.Utils; @@ -41,7 +42,7 @@ public async Task Login( HttpContext.Response.Cookies.Append("openShockSession", loginAction.AsT0.Value, new CookieOptions { - Expires = new DateTimeOffset(DateTime.UtcNow.Add(Constants.LoginSessionLifetime)), + Expires = new DateTimeOffset(DateTime.UtcNow.Add(Duration.LoginSessionLifetime)), Secure = true, HttpOnly = true, SameSite = SameSiteMode.Strict, diff --git a/API/Controller/Account/LoginV2.cs b/API/Controller/Account/LoginV2.cs index f2f7d11e..c30dd209 100644 --- a/API/Controller/Account/LoginV2.cs +++ b/API/Controller/Account/LoginV2.cs @@ -4,6 +4,7 @@ using Asp.Versioning; using OpenShock.API.Services.Account; using OpenShock.Common; +using OpenShock.Common.Constants; using OpenShock.Common.Errors; using OpenShock.Common.Problems; using OpenShock.Common.Services.Turnstile; @@ -48,7 +49,7 @@ public async Task LoginV2( HttpContext.Response.Cookies.Append("openShockSession", loginAction.AsT0.Value, new CookieOptions { - Expires = new DateTimeOffset(DateTime.UtcNow.Add(Constants.LoginSessionLifetime)), + Expires = new DateTimeOffset(DateTime.UtcNow.Add(Duration.LoginSessionLifetime)), Secure = true, HttpOnly = true, SameSite = SameSiteMode.Strict, diff --git a/API/Models/Requests/ChangeEmailRequest.cs b/API/Models/Requests/ChangeEmailRequest.cs index ec57d2cc..70177ce6 100644 --- a/API/Models/Requests/ChangeEmailRequest.cs +++ b/API/Models/Requests/ChangeEmailRequest.cs @@ -1,6 +1,10 @@ -namespace OpenShock.API.Models.Requests; + +using OpenShock.Common.DataAnnotations; + +namespace OpenShock.API.Models.Requests; public sealed class ChangeEmailRequest { + [EmailAddress(true)] public required string Email { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/ChangePasswordRequest.cs b/API/Models/Requests/ChangePasswordRequest.cs index 3924c09e..6ca1ed7d 100644 --- a/API/Models/Requests/ChangePasswordRequest.cs +++ b/API/Models/Requests/ChangePasswordRequest.cs @@ -1,7 +1,13 @@ -namespace OpenShock.API.Models.Requests; +using System.ComponentModel.DataAnnotations; +using OpenShock.Common.DataAnnotations; + +namespace OpenShock.API.Models.Requests; public sealed class ChangePasswordRequest { + [Required(AllowEmptyStrings = false)] public required string OldPassword { get; set; } + + [Password(true)] public required string NewPassword { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/ChangeUsernameRequest.cs b/API/Models/Requests/ChangeUsernameRequest.cs index 8080b067..c8597612 100644 --- a/API/Models/Requests/ChangeUsernameRequest.cs +++ b/API/Models/Requests/ChangeUsernameRequest.cs @@ -1,6 +1,11 @@ -namespace OpenShock.API.Models.Requests; +using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; +using OpenShock.Common.DataAnnotations; + +namespace OpenShock.API.Models.Requests; public sealed class ChangeUsernameRequest { + [Username(true)] public required string Username { get; init; } } \ No newline at end of file diff --git a/API/Models/Requests/CreateShareRequest.cs b/API/Models/Requests/CreateShareRequest.cs index b39e4231..41136551 100644 --- a/API/Models/Requests/CreateShareRequest.cs +++ b/API/Models/Requests/CreateShareRequest.cs @@ -1,10 +1,11 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Requests; public sealed class CreateShareRequest { - [MaxLength(128)] // Hard limit + [MaxLength(HardLimits.CreateShareRequestMaxShockers)] public required IEnumerable Shockers { get; set; } public Guid? User { get; set; } = null; } diff --git a/API/Models/Requests/HubCreateRequest.cs b/API/Models/Requests/HubCreateRequest.cs index 5b404d76..eb30da29 100644 --- a/API/Models/Requests/HubCreateRequest.cs +++ b/API/Models/Requests/HubCreateRequest.cs @@ -1,10 +1,11 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Requests; public sealed class HubCreateRequest { [Required(AllowEmptyStrings = false)] - [StringLength(32, MinimumLength = 1)] + [StringLength(HardLimits.HubNameMaxLength, MinimumLength = HardLimits.HubNameMinLength)] public required string Name { get; init; } } \ No newline at end of file diff --git a/API/Models/Requests/HubEditRequest.cs b/API/Models/Requests/HubEditRequest.cs index 44848bc3..10440b39 100644 --- a/API/Models/Requests/HubEditRequest.cs +++ b/API/Models/Requests/HubEditRequest.cs @@ -1,10 +1,11 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Requests; public sealed class HubEditRequest { [Required(AllowEmptyStrings = false)] - [StringLength(32, MinimumLength = 1)] + [StringLength(HardLimits.HubNameMaxLength, MinimumLength = HardLimits.HubNameMinLength)] public required string Name { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/Login.cs b/API/Models/Requests/Login.cs index 62116f9b..16096aaa 100644 --- a/API/Models/Requests/Login.cs +++ b/API/Models/Requests/Login.cs @@ -1,11 +1,13 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Requests; public sealed class Login { - [MinLength(1)] + [Required(AllowEmptyStrings = false)] public required string Password { get; set; } - [MinLength(1)] + + [Required(AllowEmptyStrings = false)] public required string Email { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/LoginV2.cs b/API/Models/Requests/LoginV2.cs index d2d6075a..e65059d8 100644 --- a/API/Models/Requests/LoginV2.cs +++ b/API/Models/Requests/LoginV2.cs @@ -1,10 +1,16 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Requests; public sealed class LoginV2 { - [Required(AllowEmptyStrings = false)] public required string Password { get; set; } - [Required(AllowEmptyStrings = false)] public required string Email { get; set; } - [Required(AllowEmptyStrings = false)] public required string TurnstileResponse { get; set; } + [Required(AllowEmptyStrings = false)] + public required string Password { get; set; } + + [Required(AllowEmptyStrings = false)] + public required string Email { get; set; } + + [Required(AllowEmptyStrings = false)] + public required string TurnstileResponse { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/NewShocker.cs b/API/Models/Requests/NewShocker.cs index d9c11545..3b1f0be2 100644 --- a/API/Models/Requests/NewShocker.cs +++ b/API/Models/Requests/NewShocker.cs @@ -1,11 +1,14 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; using OpenShock.Common.Models; namespace OpenShock.API.Models.Requests; public sealed class NewShocker { - [StringLength(48, MinimumLength = 1)] public required string Name { get; set; } + [Required(AllowEmptyStrings = false)] + [StringLength(HardLimits.ShockerNameMaxLength, MinimumLength = HardLimits.ShockerNameMinLength)] + public required string Name { get; set; } public required ushort RfId { get; set; } public required Guid Device { get; set; } public required ShockerModelType Model { get; set; } diff --git a/API/Models/Requests/ShareLinkCreate.cs b/API/Models/Requests/ShareLinkCreate.cs index 4f86d8b4..c46db808 100644 --- a/API/Models/Requests/ShareLinkCreate.cs +++ b/API/Models/Requests/ShareLinkCreate.cs @@ -1,7 +1,12 @@ -namespace OpenShock.API.Models.Requests; +using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; + +namespace OpenShock.API.Models.Requests; public sealed class ShareLinkCreate { + [Required(AllowEmptyStrings = false)] + [StringLength(HardLimits.ShockerShareLinkNameMaxLength, MinimumLength = HardLimits.ShockerShareLinkNameMinLength)] public required string Name { get; set; } public DateTime? ExpiresOn { get; set; } = null; } \ No newline at end of file diff --git a/API/Models/Requests/ShareLinkEditShocker.cs b/API/Models/Requests/ShareLinkEditShocker.cs index 6f59b11f..3c752fb3 100644 --- a/API/Models/Requests/ShareLinkEditShocker.cs +++ b/API/Models/Requests/ShareLinkEditShocker.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using OpenShock.API.Models.Response; using OpenShock.Common; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Requests; @@ -9,6 +10,6 @@ public sealed class ShareLinkEditShocker public required ShockerPermissions Permissions { get; set; } public required ShockerLimits Limits { get; set; } - [Range(Constants.MinControlDuration, Constants.MaxControlDuration)] + [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] public ushort? Cooldown { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/Signup.cs b/API/Models/Requests/Signup.cs index 9e62c0c2..7d5613e6 100644 --- a/API/Models/Requests/Signup.cs +++ b/API/Models/Requests/Signup.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; using OpenShock.Common.DataAnnotations; namespace OpenShock.API.Models.Requests; @@ -7,8 +7,10 @@ public sealed class SignUp { [Username(true)] public required string Username { get; set; } - [StringLength(256, MinimumLength = 12)] + + [Password(true)] public required string Password { get; set; } - [EmailAddress] + + [EmailAddress(true)] public required string Email { get; set; } } \ No newline at end of file diff --git a/API/Models/Requests/SignupV2.cs b/API/Models/Requests/SignupV2.cs index a3725f24..6a8ff571 100644 --- a/API/Models/Requests/SignupV2.cs +++ b/API/Models/Requests/SignupV2.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using OpenShock.Common.DataAnnotations; +using OpenShock.Common.DataAnnotations; namespace OpenShock.API.Models.Requests; @@ -7,9 +6,13 @@ public sealed class SignUpV2 { [Username(true)] public required string Username { get; set; } - [StringLength(256, MinimumLength = 12)] + + [Password(true)] public required string Password { get; set; } - [EmailAddress] + + [EmailAddress(true)] public required string Email { get; set; } - [Required(AllowEmptyStrings = false)] public required string TurnstileResponse { get; set; } + + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = false)] + public required string TurnstileResponse { get; set; } } \ No newline at end of file diff --git a/API/Models/Response/ShockerLimits.cs b/API/Models/Response/ShockerLimits.cs index dd04197e..4d9d721b 100644 --- a/API/Models/Response/ShockerLimits.cs +++ b/API/Models/Response/ShockerLimits.cs @@ -1,13 +1,14 @@ using OpenShock.Common; using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.API.Models.Response; public sealed class ShockerLimits { - [Range(Constants.MinControlIntensity, Constants.MaxControlIntensity)] + [Range(HardLimits.MinControlIntensity, HardLimits.MaxControlIntensity)] public required byte? Intensity { get; set; } - [Range(Constants.MinControlDuration, Constants.MaxControlDuration)] + [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] public required ushort? Duration { get; set; } } \ No newline at end of file diff --git a/API/Services/Account/AccountService.cs b/API/Services/Account/AccountService.cs index a660c530..78988b0e 100644 --- a/API/Services/Account/AccountService.cs +++ b/API/Services/Account/AccountService.cs @@ -7,6 +7,7 @@ using OpenShock.API.Services.Email.Mailjet.Mail; using OpenShock.API.Utils; using OpenShock.Common; +using OpenShock.Common.Constants; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis; using OpenShock.Common.Utils; @@ -131,8 +132,8 @@ await _loginSessions.InsertAsync(new LoginSession Ip = loginContext.Ip, PublicId = Guid.NewGuid(), Created = DateTime.UtcNow, - Expires = DateTime.UtcNow.Add(Constants.LoginSessionLifetime), - }, Constants.LoginSessionLifetime); + Expires = DateTime.UtcNow.Add(Duration.LoginSessionLifetime), + }, Duration.LoginSessionLifetime); return new Success(randomSessionId); } @@ -141,7 +142,7 @@ await _loginSessions.InsertAsync(new LoginSession public async Task> PasswordResetExists(Guid passwordResetId, string secret, CancellationToken cancellationToken = default) { - var validUntil = DateTime.UtcNow.Add(Constants.PasswordResetRequestLifetime); + var validUntil = DateTime.UtcNow.Add(Duration.PasswordResetRequestLifetime); var reset = await _db.PasswordResets.FirstOrDefaultAsync(x => x.Id == passwordResetId && x.UsedOn == null && x.CreatedOn < validUntil, cancellationToken: cancellationToken); @@ -154,7 +155,7 @@ public async Task> PasswordResetExists(G /// public async Task> CreatePasswordReset(string email) { - var validUntil = DateTime.UtcNow.Add(Constants.PasswordResetRequestLifetime); + var validUntil = DateTime.UtcNow.Add(Duration.PasswordResetRequestLifetime); var lowerCaseEmail = email.ToLowerInvariant(); var user = await _db.Users.Where(x => x.Email == lowerCaseEmail).Select(x => new { @@ -185,7 +186,7 @@ await _emailService.PasswordReset(new Contact(user.User.Email, user.User.Name), public async Task> PasswordResetComplete(Guid passwordResetId, string secret, string newPassword) { - var validUntil = DateTime.UtcNow.Add(Constants.PasswordResetRequestLifetime); + var validUntil = DateTime.UtcNow.Add(Duration.PasswordResetRequestLifetime); var reset = await _db.PasswordResets.Include(x => x.User).FirstOrDefaultAsync(x => x.Id == passwordResetId && x.UsedOn == null && x.CreatedOn < validUntil); @@ -218,7 +219,7 @@ public async Task x.UserId == userId && x.CreatedOn >= cooldownSubtracted).AnyAsync()) { return new Error>(new RecentlyChanged()); diff --git a/Common.Tests/Geo/DistanceLookupTests.cs b/Common.Tests/Geo/DistanceLookupTests.cs index b13dc7c5..649b921d 100644 --- a/Common.Tests/Geo/DistanceLookupTests.cs +++ b/Common.Tests/Geo/DistanceLookupTests.cs @@ -1,4 +1,5 @@ -using OpenShock.Common.Geo; +using OpenShock.Common.Constants; +using OpenShock.Common.Geo; namespace OpenShock.Common.Tests.Geo; @@ -29,6 +30,6 @@ public async Task TryGetDistanceBetween_UnknownCountry(string str1, string str2) // Assert await Assert.That(result).IsFalse(); - await Assert.That(distance).IsEqualTo(Constants.DistanceToAndromedaGalaxyInKm); + await Assert.That(distance).IsEqualTo(Distance.DistanceToAndromedaGalaxyInKm); } } diff --git a/Common/Authentication/Handlers/LoginSessionAuthentication.cs b/Common/Authentication/Handlers/LoginSessionAuthentication.cs index ac9f9c46..4a179b53 100644 --- a/Common/Authentication/Handlers/LoginSessionAuthentication.cs +++ b/Common/Authentication/Handlers/LoginSessionAuthentication.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using OpenShock.Common.Authentication.Services; +using OpenShock.Common.Constants; using OpenShock.Common.Errors; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; @@ -100,14 +101,14 @@ private async Task SessionAuth(string sessionKey) // This can be removed at a later point, this is just for upgrade purposes if(UpdateOlderLoginSessions(session)) await _userSessions.SaveAsync(); - if (session.Expires!.Value < DateTime.UtcNow.Subtract(Constants.LoginSessionExpansionAfter)) + if (session.Expires!.Value < DateTime.UtcNow.Subtract(Duration.LoginSessionExpansionAfter)) { #pragma warning disable CS4014 LucTask.Run(async () => #pragma warning restore CS4014 { - session.Expires = DateTime.UtcNow.Add(Constants.LoginSessionLifetime); - await _userSessions.UpdateAsync(session, Constants.LoginSessionLifetime); + session.Expires = DateTime.UtcNow.Add(Duration.LoginSessionLifetime); + await _userSessions.UpdateAsync(session, Duration.LoginSessionLifetime); }); } diff --git a/Common/Constants.cs b/Common/Constants/Constants.cs similarity index 65% rename from Common/Constants.cs rename to Common/Constants/Constants.cs index cd24d3fa..b13523bc 100644 --- a/Common/Constants.cs +++ b/Common/Constants/Constants.cs @@ -1,18 +1,10 @@ -namespace OpenShock.Common; +namespace OpenShock.Common.Constants; -public static class Constants +public static class Duration { - public const byte MinControlIntensity = 0; - public const byte MaxControlIntensity = 100; - - public const ushort MinControlDuration = 300; - public const ushort MaxControlDuration = 30000; // TODO: No reason to hard limit this to 30 seconds, can we extend it to ushort.MaxValue (65535)? - public static readonly TimeSpan PasswordResetRequestLifetime = TimeSpan.FromHours(1); public static readonly TimeSpan NameChangeCooldown = TimeSpan.FromDays(7); - - public const float DistanceToAndromedaGalaxyInKm = 2.401E19f; public static readonly TimeSpan LoginSessionLifetime = TimeSpan.FromDays(30); public static readonly TimeSpan LoginSessionExpansionAfter = TimeSpan.FromDays(1); diff --git a/Common/Constants/Distance.cs b/Common/Constants/Distance.cs new file mode 100644 index 00000000..91044905 --- /dev/null +++ b/Common/Constants/Distance.cs @@ -0,0 +1,6 @@ +namespace OpenShock.Common.Constants; + +public static class Distance +{ + public const float DistanceToAndromedaGalaxyInKm = 2.401E19f; +} \ No newline at end of file diff --git a/Common/Constants/HardLimits.cs b/Common/Constants/HardLimits.cs new file mode 100644 index 00000000..8713bef9 --- /dev/null +++ b/Common/Constants/HardLimits.cs @@ -0,0 +1,48 @@ +namespace OpenShock.Common.Constants; + +public static class HardLimits +{ + public const byte MinControlIntensity = 0; + public const byte MaxControlIntensity = 100; + + public const ushort MinControlDuration = 300; + public const ushort MaxControlDuration = 30000; // TODO: No reason to hard limit this to 30 seconds, can we extend it to ushort.MaxValue (65535)? + + public const int UsernameMinLength = 3; + public const int UsernameMaxLength = 32; + + public const int EmailAddressMinLength = 5; // "a@b.c" (5 chars) + public const int EmailAddressMaxLength = 320; // 64 + 1 + 255 (RFC 2821) + + public const int PasswordMinLength = 12; + public const int PasswordMaxLength = 256; + + public const int UserAgentMaxLength = 1024; + + public const int ApiKeyNameMaxLength = 64; + public const int ApiKeyTokenMaxLength = 256; + + public const int HubNameMinLength = 1; + public const int HubNameMaxLength = 64; + public const int HubTokenMaxLength = 256; + + public const int ShockerNameMinLength = 1; + public const int ShockerNameMaxLength = 64; + + public const int ShockerShareLinkNameMinLength = 1; + public const int ShockerShareLinkNameMaxLength = 64; + + public const int IpAddressMaxLength = 40; + + public const int SemVerMaxLength = 64; + public const int OtaUpdateMessageMaxLength = 128; + + public const int PasswordHashMaxLength = 100; + + public const int UserEmailChangeSecretMaxLength = 128; + public const int UserActivationSecretMaxLength = 128; + public const int PasswordResetSecretMaxLength = 100; + public const int ShockerControlLogCustomNameMaxLength = 64; + + public const int CreateShareRequestMaxShockers = 128; +} diff --git a/Common/DataAnnotations/EmailAddressAttribute.cs b/Common/DataAnnotations/EmailAddressAttribute.cs new file mode 100644 index 00000000..cf7a3ec0 --- /dev/null +++ b/Common/DataAnnotations/EmailAddressAttribute.cs @@ -0,0 +1,69 @@ +using System.ComponentModel.DataAnnotations; +using System.Net.Mail; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using OpenShock.Common.Constants; +using OpenShock.Common.DataAnnotations.Interfaces; + +namespace OpenShock.Common.DataAnnotations; + +/// +/// An attribute used to validate whether an email is valid. +/// +/// +/// Inherits from . +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public sealed class EmailAddressAttribute : ValidationAttribute, IParameterAttribute +{ + /// + /// Example value used to generate OpenApi documentation. + /// + private const string ExampleValue = "user@example.com"; + + private const string ErrMsgCannotBeNull = "Email cannot be null"; + private const string ErrMsgMustBeString = "Email must be a string"; + private const string ErrMsgTooShort = "Email is too short"; + private const string ErrMsgTooLong = "Email is too long"; + private const string ErrMsgMustBeEmail = "Email must be an email address"; + + /// + /// Indicates whether validation should be performed. + /// + public bool ShouldValidate { get; } + + /// + /// Initializes a new instance of the class with the specified validation behavior. + /// + /// True if validation should be performed; otherwise, false. + public EmailAddressAttribute(bool shouldValidate) => ShouldValidate = shouldValidate; + + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (!ShouldValidate) return ValidationResult.Success; + + if (value is null) return new ValidationResult(ErrMsgCannotBeNull); + + if (value is not string email) return new ValidationResult(ErrMsgMustBeString); + + if (email.Length < HardLimits.EmailAddressMinLength) return new ValidationResult(ErrMsgTooShort); + + if (email.Length > HardLimits.EmailAddressMaxLength) return new ValidationResult(ErrMsgTooLong); + + if (!MailAddress.TryCreate(email, out _)) return new ValidationResult(ErrMsgMustBeEmail); + + return ValidationResult.Success; + } + + /// + public void Apply(OpenApiSchema schema) + { + //if (ShouldValidate) schema.Pattern = ???; + + schema.Example = new OpenApiString(ExampleValue); + } + + /// + public void Apply(OpenApiParameter parameter) => Apply(parameter.Schema); +} \ No newline at end of file diff --git a/Common/DataAnnotations/PasswordAttribute.cs b/Common/DataAnnotations/PasswordAttribute.cs new file mode 100644 index 00000000..2aa2a8f1 --- /dev/null +++ b/Common/DataAnnotations/PasswordAttribute.cs @@ -0,0 +1,69 @@ +using System.ComponentModel.DataAnnotations; +using System.Net.Mail; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using OpenShock.Common.Constants; +using OpenShock.Common.DataAnnotations.Interfaces; + +namespace OpenShock.Common.DataAnnotations; + +/// +/// An attribute used to validate whether a password is valid. +/// +/// +/// Inherits from . +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public sealed class PasswordAttribute : ValidationAttribute, IParameterAttribute +{ + /// + /// Example value used to generate OpenApi documentation. + /// + private const string ExampleValue = "user@example.com"; + + private const string ErrMsgCannotBeNull = "Password cannot be null"; + private const string ErrMsgMustBeString = "Password must be a string"; + private const string ErrMsgTooShort = "Password is too short"; + private const string ErrMsgTooLong = "Password is too long"; + private const string ErrMsgCannotStartOrEndWithWhiteSpace = "Password cannot start or end with whitespace"; + + /// + /// Indicates whether validation should be performed. + /// + public bool ShouldValidate { get; } + + /// + /// Initializes a new instance of the class with the specified validation behavior. + /// + /// True if validation should be performed; otherwise, false. + public PasswordAttribute(bool shouldValidate) => ShouldValidate = shouldValidate; + + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (!ShouldValidate) return ValidationResult.Success; + + if (value is null) return new ValidationResult(ErrMsgCannotBeNull); + + if (value is not string password) return new ValidationResult(ErrMsgMustBeString); + + if (password.Length < HardLimits.EmailAddressMinLength) return new ValidationResult(ErrMsgTooShort); + + if (password.Length > HardLimits.EmailAddressMaxLength) return new ValidationResult(ErrMsgTooLong); + + if (password.Trim().Length != password.Length) return new ValidationResult(ErrMsgCannotStartOrEndWithWhiteSpace); + + return ValidationResult.Success; + } + + /// + public void Apply(OpenApiSchema schema) + { + //if (ShouldValidate) schema.Pattern = ???; + + schema.Example = new OpenApiString(ExampleValue); + } + + /// + public void Apply(OpenApiParameter parameter) => Apply(parameter.Schema); +} \ No newline at end of file diff --git a/Common/DataAnnotations/UsernameAttribute.cs b/Common/DataAnnotations/UsernameAttribute.cs index 9dc399b2..eb66b39d 100644 --- a/Common/DataAnnotations/UsernameAttribute.cs +++ b/Common/DataAnnotations/UsernameAttribute.cs @@ -12,20 +12,16 @@ namespace OpenShock.Common.DataAnnotations; /// /// Inherits from . /// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] public sealed class UsernameAttribute : ValidationAttribute, IParameterAttribute { /// - /// Regular expression for username validation. + /// Example value used to generate OpenApi documentation. /// - private const string DisplaynameRegex = /* lang=regex */ @"^[^\s].*[^\s]$"; + private const string ExampleValue = "String"; - /// - /// Example username used to generate OpenApi documentation. - /// - private const string ExampleDisplayname = "String"; - - private const string ErrMsgMustBeString = "Must be a string"; + private const string ErrMsgCannotBeNull = "Username cannot be null"; + private const string ErrMsgMustBeString = "Username must be a string"; /// /// Indicates whether validation should be performed. @@ -43,7 +39,7 @@ public sealed class UsernameAttribute : ValidationAttribute, IParameterAttribute { if (!ShouldValidate) return ValidationResult.Success; - if (value is null) return new ValidationResult("Username cannot be null"); + if (value is null) return new ValidationResult(ErrMsgCannotBeNull); if (value is not string displayName) return new ValidationResult(ErrMsgMustBeString); @@ -57,9 +53,9 @@ public sealed class UsernameAttribute : ValidationAttribute, IParameterAttribute /// public void Apply(OpenApiSchema schema) { - if (ShouldValidate) schema.Pattern = DisplaynameRegex; + //if (ShouldValidate) schema.Pattern = ???; - schema.Example = new OpenApiString(ExampleDisplayname); + schema.Example = new OpenApiString(ExampleValue); } /// diff --git a/Common/DeviceControl/ControlLogic.cs b/Common/DeviceControl/ControlLogic.cs index 62b1b8d8..89490037 100644 --- a/Common/DeviceControl/ControlLogic.cs +++ b/Common/DeviceControl/ControlLogic.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using OneOf; using OneOf.Types; +using OpenShock.Common.Constants; using OpenShock.Common.Hubs; using OpenShock.Common.Models; using OpenShock.Common.Models.WebSocket.User; @@ -97,8 +98,8 @@ private static async Task diff --git a/Common/Models/WebSocket/User/ControlLog.cs b/Common/Models/WebSocket/User/ControlLog.cs index f6e04519..befef719 100644 --- a/Common/Models/WebSocket/User/ControlLog.cs +++ b/Common/Models/WebSocket/User/ControlLog.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using OpenShock.Common.Constants; namespace OpenShock.Common.Models.WebSocket.User; @@ -9,10 +10,10 @@ public sealed class ControlLog public required ControlType Type { get; set; } - [Range(Constants.MinControlIntensity, Constants.MaxControlIntensity)] + [Range(HardLimits.MinControlIntensity, HardLimits.MaxControlIntensity)] public required byte Intensity { get; set; } - [Range(Constants.MinControlDuration, Constants.MaxControlDuration)] + [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] public required uint Duration { get; set; } public required DateTime ExecutedAt { get; set; } diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 10ab3eff..e4832bf8 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; +using OpenShock.Common.Constants; namespace OpenShock.Common.OpenShockDb; @@ -82,6 +83,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("id"); entity.Property(e => e.CreatedByIp) .HasColumnType("character varying") + .HasMaxLength(HardLimits.IpAddressMaxLength) .HasColumnName("created_by_ip"); entity.Property(e => e.CreatedOn) .HasDefaultValueSql("CURRENT_TIMESTAMP") @@ -90,10 +92,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("'-infinity'::timestamp without time zone") .HasColumnName("last_used"); entity.Property(e => e.Name) - .HasMaxLength(64) + .HasMaxLength(HardLimits.ApiKeyNameMaxLength) .HasColumnName("name"); entity.Property(e => e.Token) - .HasMaxLength(256) + .HasMaxLength(HardLimits.ApiKeyTokenMaxLength) .HasColumnName("token"); entity.Property(e => e.UserId).HasColumnName("user_id"); entity.Property(e => e.ValidUntil).HasColumnName("valid_until"); @@ -122,10 +124,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Name) .HasColumnType("character varying") + .HasMaxLength(HardLimits.DeviceNameMaxLength) .HasColumnName("name"); entity.Property(e => e.Owner).HasColumnName("owner"); entity.Property(e => e.Token) - .HasMaxLength(256) + .HasMaxLength(HardLimits.DeviceTokenMaxLength) .HasColumnName("token"); entity.HasOne(d => d.OwnerNavigation).WithMany(p => p.Devices) @@ -148,9 +151,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Message) .HasColumnType("character varying") + .HasMaxLength(HardLimits.OtaUpdateMessageMaxLength) .HasColumnName("message"); entity.Property(e => e.Version) .HasColumnType("character varying") + .HasMaxLength(HardLimits.SemVerMaxLength) .HasColumnName("version"); entity.Property(e => e.Status).HasColumnType("ota_update_status").HasColumnName("status"); @@ -177,6 +182,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Secret) .HasColumnType("character varying") + .HasMaxLength(HardLimits.PasswordResetSecretMaxLength) .HasColumnName("secret"); entity.Property(e => e.UsedOn) .HasColumnName("used_on"); @@ -262,7 +268,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Device).HasColumnName("device"); entity.Property(e => e.Name) - .HasMaxLength(64) + .HasMaxLength(HardLimits.ShockerNameMaxLength) .HasColumnName("name"); entity.Property(e => e.Paused) .HasDefaultValue(false) @@ -292,6 +298,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.CustomName) .HasColumnType("character varying") + .HasMaxLength(HardLimits.ShockerControlLogCustomNameMaxLength) .HasColumnName("custom_name"); entity.Property(e => e.Duration).HasColumnName("duration"); entity.Property(e => e.Intensity).HasColumnName("intensity"); @@ -398,6 +405,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.ExpiresOn).HasColumnName("expires_on"); entity.Property(e => e.Name) .HasColumnType("character varying") + .HasMaxLength(HardLimits.ShockerShareLinkNameMaxLength) .HasColumnName("name"); entity.Property(e => e.OwnerId).HasColumnName("owner_id"); @@ -453,6 +461,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_at"); entity.Property(e => e.Email) .HasColumnType("character varying") + .HasMaxLength(HardLimits.EmailAddressMaxLength) .HasColumnName("email"); entity.Property(e => e.EmailActived) .HasDefaultValue(false) @@ -460,9 +469,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Name) .UseCollation("ndcoll") .HasColumnType("character varying") + .HasMaxLength(HardLimits.UsernameMaxLength) .HasColumnName("name"); entity.Property(e => e.PasswordHash) .HasColumnType("character varying") + .HasMaxLength(HardLimits.PasswordHashMaxLength) .HasColumnName("password_hash"); entity.Property(e => e.Rank) .HasColumnType("rank_type") @@ -485,6 +496,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Secret) .HasColumnType("character varying") + .HasMaxLength(HardLimits.UserActivationSecretMaxLength) .HasColumnName("secret"); entity.Property(e => e.UsedOn).HasColumnName("used_on"); entity.Property(e => e.UserId).HasColumnName("user_id"); @@ -514,9 +526,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Email) .HasColumnType("character varying") + .HasMaxLength(HardLimits.EmailAddressMaxLength) .HasColumnName("email"); entity.Property(e => e.Secret) .HasColumnType("character varying") + .HasMaxLength(HardLimits.UserEmailChangeSecretMaxLength) .HasColumnName("secret"); entity.Property(e => e.UsedOn).HasColumnName("used_on"); entity.Property(e => e.UserId).HasColumnName("user_id"); @@ -548,6 +562,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.OldName) .HasColumnType("character varying") + .HasMaxLength(HardLimits.UsernameMaxLength) .HasColumnName("old_name"); entity.HasOne(d => d.User).WithMany(p => p.UsersNameChanges) diff --git a/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs b/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs index e6b7a4dc..e49a672b 100644 --- a/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs +++ b/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using OpenShock.Common.Constants; using OpenShock.Common.Geo; using OpenShock.Common.Redis; using Redis.OM; @@ -44,7 +45,7 @@ public LCGNodeProvisioner(IRedisConnectionProvider redisConnectionProvider, ILog .ToArrayAsync(); var node = nodes - .OrderBy(x => DistanceLookup.TryGetDistanceBetween(x.Country, countryCode, out float distance) ? distance : Constants.DistanceToAndromedaGalaxyInKm) // Just a large number :3 + .OrderBy(x => DistanceLookup.TryGetDistanceBetween(x.Country, countryCode, out float distance) ? distance : Distance.DistanceToAndromedaGalaxyInKm) // Just a large number :3 .ThenBy(x => x.Load) .FirstOrDefault(); diff --git a/Common/Validation/UsernameValidator.cs b/Common/Validation/UsernameValidator.cs index 6504948e..778b9aea 100644 --- a/Common/Validation/UsernameValidator.cs +++ b/Common/Validation/UsernameValidator.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using OneOf; using OneOf.Types; +using OpenShock.Common.Constants; namespace OpenShock.Common.Validation; @@ -8,12 +9,12 @@ public static class UsernameValidator { public static OneOf Validate(string username) { - if (username.Length < ValidationConstants.UsernameMinLength) + if (username.Length < HardLimits.UsernameMinLength) { return new UsernameError(UsernameErrorType.TooShort, "Username is too short."); } - if (username.Length > ValidationConstants.UsernameMaxLength) + if (username.Length > HardLimits.UsernameMaxLength) { return new UsernameError(UsernameErrorType.TooLong, "Username is too long."); } diff --git a/Common/Validation/ValidationConstants.cs b/Common/Validation/ValidationConstants.cs deleted file mode 100644 index 5a3f1541..00000000 --- a/Common/Validation/ValidationConstants.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace OpenShock.Common.Validation; - -public static class ValidationConstants -{ - public const uint UsernameMinLength = 3; - public const uint UsernameMaxLength = 32; -} \ No newline at end of file diff --git a/Cron/Jobs/ClearOldPasswordResetsJob.cs b/Cron/Jobs/ClearOldPasswordResetsJob.cs index 052b04fa..83a4e3ac 100644 --- a/Cron/Jobs/ClearOldPasswordResetsJob.cs +++ b/Cron/Jobs/ClearOldPasswordResetsJob.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using OpenShock.Common; +using OpenShock.Common.Constants; using OpenShock.Common.OpenShockDb; using OpenShock.Cron.Attributes; @@ -26,7 +27,7 @@ public async Task Execute() { // Delete all password reset requests that have not been used and are older than the lifetime. // Leave expired requests that have been used for 14 days for moderation purposes. - var earliestCreatedOn = DateTime.Now - (Constants.PasswordResetRequestLifetime + TimeSpan.FromDays(14)); + var earliestCreatedOn = DateTime.Now - (Duration.PasswordResetRequestLifetime + TimeSpan.FromDays(14)); var earliestCreatedOnUtc = DateTime.SpecifyKind(earliestCreatedOn, DateTimeKind.Utc); // Run the delete query diff --git a/LiveControlGateway/Controllers/DeviceControllerBase.cs b/LiveControlGateway/Controllers/DeviceControllerBase.cs index cad65fd3..3a7776ce 100644 --- a/LiveControlGateway/Controllers/DeviceControllerBase.cs +++ b/LiveControlGateway/Controllers/DeviceControllerBase.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using OpenShock.Common; using OpenShock.Common.Authentication.Services; +using OpenShock.Common.Constants; using OpenShock.Common.Hubs; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis; @@ -39,7 +40,7 @@ public abstract class DeviceControllerBase : FlatbuffersWebsocketBase private readonly IDbContextFactory _dbContextFactory; private readonly LCGConfig _lcgConfig; - private readonly Timer _keepAliveTimeoutTimer = new(Constants.DeviceKeepAliveInitialTimeout); + private readonly Timer _keepAliveTimeoutTimer = new(Duration.DeviceKeepAliveInitialTimeout); private DateTimeOffset _connected = DateTimeOffset.UtcNow; public override Guid Id => CurrentDevice.Id; @@ -126,7 +127,7 @@ protected async Task SelfOnline() { Logger.LogDebug("Received keep alive from device [{DeviceId}]", CurrentDevice.Id); - _keepAliveTimeoutTimer.Interval = Constants.DeviceKeepAliveTimeout.TotalMilliseconds; + _keepAliveTimeoutTimer.Interval = Duration.DeviceKeepAliveTimeout.TotalMilliseconds; var deviceOnline = _redisConnectionProvider.RedisCollection(); var deviceId = CurrentDevice.Id.ToString(); @@ -140,7 +141,7 @@ await deviceOnline.InsertAsync(new DeviceOnline FirmwareVersion = FirmwareVersion, Gateway = _lcgConfig.Lcg.Fqdn, ConnectedAt = _connected - }, Constants.DeviceKeepAliveTimeout); + }, Duration.DeviceKeepAliveTimeout); return; } @@ -155,7 +156,7 @@ await deviceOnline.InsertAsync(new DeviceOnline } await _redisConnectionProvider.Connection.ExecuteAsync("EXPIRE", - $"{typeof(DeviceOnline).FullName}:{CurrentDevice.Id}", Constants.DeviceKeepAliveTimeoutIntBoxed); + $"{typeof(DeviceOnline).FullName}:{CurrentDevice.Id}", Duration.DeviceKeepAliveTimeoutIntBoxed); } /// diff --git a/LiveControlGateway/Controllers/DeviceV2Controller.cs b/LiveControlGateway/Controllers/DeviceV2Controller.cs index 0f76437b..a8a17ba4 100644 --- a/LiveControlGateway/Controllers/DeviceV2Controller.cs +++ b/LiveControlGateway/Controllers/DeviceV2Controller.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using OpenShock.Common; using OpenShock.Common.Authentication; +using OpenShock.Common.Constants; using OpenShock.Common.Hubs; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; @@ -52,7 +53,7 @@ public DeviceV2Controller( dbContextFactory, serviceProvider, lcgConfig) { _userHubContext = userHubContext; - _pingTimer = new Timer(PingTimerElapsed, null, Constants.DevicePingInitialDelay, Constants.DevicePingPeriod); + _pingTimer = new Timer(PingTimerElapsed, null, Duration.DevicePingInitialDelay, Duration.DevicePingPeriod); } private async void PingTimerElapsed(object? state) diff --git a/LiveControlGateway/Controllers/LiveControlController.cs b/LiveControlGateway/Controllers/LiveControlController.cs index e54d7344..d858c3ba 100644 --- a/LiveControlGateway/Controllers/LiveControlController.cs +++ b/LiveControlGateway/Controllers/LiveControlController.cs @@ -11,6 +11,7 @@ using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Authentication.Services; +using OpenShock.Common.Constants; using OpenShock.Common.JsonSerialization; using OpenShock.Common.Models; using OpenShock.Common.Models.WebSocket; @@ -500,7 +501,7 @@ await QueueMessage(new Common.Models.WebSocket.BaseResponse() var perms = permCheck.AsT0.Value; // Clamp to limits - var intensity = Math.Clamp(frame.Intensity, Constants.MinControlIntensity, perms.Intensity ?? Constants.MaxControlIntensity); + var intensity = Math.Clamp(frame.Intensity, HardLimits.MinControlIntensity, perms.Intensity ?? HardLimits.MaxControlIntensity); var result = DeviceLifetimeManager.ReceiveFrame(Id, frame.Shocker, frame.Type, intensity, _tps); if (result.IsT0) diff --git a/LiveControlGateway/LifetimeManager/ShockerState.cs b/LiveControlGateway/LifetimeManager/ShockerState.cs index 1d4e7bf3..852fcad9 100644 --- a/LiveControlGateway/LifetimeManager/ShockerState.cs +++ b/LiveControlGateway/LifetimeManager/ShockerState.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common; +using OpenShock.Common.Constants; using OpenShock.Common.Models; namespace OpenShock.LiveControlGateway.LifetimeManager; @@ -31,7 +32,7 @@ public sealed class ShockerState /// /// Last intensity sent to the shocker via live control /// - [Range(Constants.MinControlIntensity, Constants.MaxControlIntensity)] + [Range(HardLimits.MinControlIntensity, HardLimits.MaxControlIntensity)] public byte LastIntensity { get; set; } = 0; /// From 961a13fc629a85a177c771b4e4141e20667090ae Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 6 Nov 2024 00:35:27 +0100 Subject: [PATCH 2/6] Change ApiToken Max length --- API/Controller/Tokens/TokenController.cs | 8 +++++--- Common/Constants/HardLimits.cs | 4 +++- Common/OpenShockDb/OpenShockContext.cs | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 7c32d77f..9404f190 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -6,6 +6,7 @@ using OpenShock.API.Utils; using OpenShock.Common; using OpenShock.Common.Authentication.Attributes; +using OpenShock.Common.Constants; using OpenShock.Common.Errors; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; @@ -108,7 +109,7 @@ public async Task CreateToken([FromBody] CreateTokenReques var token = new ApiToken { UserId = CurrentUser.DbUser.Id, - Token = CryptoUtils.RandomString(64), + Token = CryptoUtils.RandomString(HardLimits.ApiKeyTokenMaxLength), CreatedByIp = HttpContext.GetRemoteIP().ToString(), Permissions = body.Permissions.Distinct().ToList(), Id = Guid.NewGuid(), @@ -150,9 +151,10 @@ public async Task EditToken([FromRoute] Guid tokenId, [FromBody] public class EditTokenRequest { - [StringLength(64, ErrorMessage = "Name must be less than 64 characters")] + [StringLength(HardLimits.ApiKeyTokenMaxLength, MinimumLength = HardLimits.ApiKeyTokenMinLength, ErrorMessage = "API token length must be between {1} and {2}")] public required string Name { get; set; } - [MaxLength(256, ErrorMessage = "You can only have 256 permissions, this is a hard limit")] + + [MaxLength(HardLimits.ApiKeyMaxPermissions, ErrorMessage = "API token permissions must be between {1} and {2}")] public List Permissions { get; set; } = [PermissionType.Shockers_Use]; } diff --git a/Common/Constants/HardLimits.cs b/Common/Constants/HardLimits.cs index 8713bef9..2685c1d1 100644 --- a/Common/Constants/HardLimits.cs +++ b/Common/Constants/HardLimits.cs @@ -20,7 +20,9 @@ public static class HardLimits public const int UserAgentMaxLength = 1024; public const int ApiKeyNameMaxLength = 64; - public const int ApiKeyTokenMaxLength = 256; + public const int ApiKeyTokenMinLength = 1; + public const int ApiKeyTokenMaxLength = 64; + public const int ApiKeyMaxPermissions = 256; public const int HubNameMinLength = 1; public const int HubNameMaxLength = 64; diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index e4832bf8..f8aee3fb 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -124,11 +124,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.Name) .HasColumnType("character varying") - .HasMaxLength(HardLimits.DeviceNameMaxLength) + .HasMaxLength(HardLimits.HubNameMaxLength) .HasColumnName("name"); entity.Property(e => e.Owner).HasColumnName("owner"); entity.Property(e => e.Token) - .HasMaxLength(HardLimits.DeviceTokenMaxLength) + .HasMaxLength(HardLimits.HubTokenMaxLength) .HasColumnName("token"); entity.HasOne(d => d.OwnerNavigation).WithMany(p => p.Devices) From 8c7e92d6466ea665eef930739d65dedf25e7cea0 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 6 Nov 2024 00:52:29 +0100 Subject: [PATCH 3/6] EFCore being stupid again --- ...105235041_RestrictFieldLengths.Designer.cs | 1129 +++++++++++++++++ .../20241105235041_RestrictFieldLengths.cs | 298 +++++ .../OpenShockContextModelSnapshot.cs | 126 +- Common/OpenShockDb/OpenShockContext.cs | 43 +- Common/Utils/PropertyBuilderExtension.cs | 12 + 5 files changed, 1563 insertions(+), 45 deletions(-) create mode 100644 Common/Migrations/20241105235041_RestrictFieldLengths.Designer.cs create mode 100644 Common/Migrations/20241105235041_RestrictFieldLengths.cs create mode 100644 Common/Utils/PropertyBuilderExtension.cs diff --git a/Common/Migrations/20241105235041_RestrictFieldLengths.Designer.cs b/Common/Migrations/20241105235041_RestrictFieldLengths.Designer.cs new file mode 100644 index 00000000..e2a7e372 --- /dev/null +++ b/Common/Migrations/20241105235041_RestrictFieldLengths.Designer.cs @@ -0,0 +1,1129 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OpenShock.Common.OpenShockDb; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + [DbContext(typeof(OpenShockContext))] + [Migration("20241105235041_RestrictFieldLengths")] + partial class RestrictFieldLengths + { + /// + 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", "8.0.10") + .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, "rank_type", new[] { "user", "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("ApiTokenCount") + .HasColumnType("integer") + .HasColumnName("api_token_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeviceCount") + .HasColumnType("integer") + .HasColumnName("device_count"); + + b.Property("Email") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("email"); + + b.Property("EmailActived") + .HasColumnType("boolean") + .HasColumnName("email_actived"); + + 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("Rank") + .HasColumnType("rank_type") + .HasColumnName("rank"); + + b.Property("ShockerControlLogCount") + .HasColumnType("integer") + .HasColumnName("shocker_control_log_count"); + + b.Property("ShockerCount") + .HasColumnType("integer") + .HasColumnName("shocker_count"); + + b.Property("ShockerShareCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_count"); + + b.Property("ShockerShareLinkCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_link_count"); + + b.Property("UserActivationCount") + .HasColumnType("integer") + .HasColumnName("user_activation_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("CreatedByIp") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("created_by_ip"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + 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.Property("Permissions") + .IsRequired() + .HasColumnType("permission_type[]") + .HasColumnName("permissions"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token"); + + 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("Token") + .IsUnique(); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("ValidUntil") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("api_tokens", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Owner") + .HasColumnType("uuid") + .HasColumnName("owner"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("token"); + + b.HasKey("Id") + .HasName("devices_pkey"); + + b.HasIndex("Owner") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("devices", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.Property("Device") + .HasColumnType("uuid") + .HasColumnName("device"); + + b.Property("UpdateId") + .HasColumnType("integer") + .HasColumnName("update_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .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("Device", "UpdateId") + .HasName("device_ota_updates_pkey"); + + b.HasIndex(new[] { "CreatedOn" }, "device_ota_updates_created_on_idx") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("device_ota_updates", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PasswordReset", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("password_resets_pkey"); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("password_resets", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Owner") + .HasColumnType("uuid") + .HasColumnName("owner"); + + b.Property("User") + .HasColumnType("uuid") + .HasColumnName("user"); + + b.HasKey("Id") + .HasName("shares_codes_pkey"); + + b.HasIndex("Owner") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("User"); + + b.ToTable("share_requests", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequestsShocker", b => + { + b.Property("ShareRequest") + .HasColumnType("uuid") + .HasColumnName("share_request"); + + b.Property("Shocker") + .HasColumnType("uuid") + .HasColumnName("shocker"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.HasKey("ShareRequest", "Shocker") + .HasName("share_requests_shockers_pkey"); + + b.HasIndex("Shocker"); + + b.ToTable("share_requests_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Device") + .HasColumnType("uuid") + .HasColumnName("device"); + + b.Property("Model") + .HasColumnType("shocker_model_type") + .HasColumnName("model"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("RfId") + .HasColumnType("integer") + .HasColumnName("rf_id"); + + b.HasKey("Id") + .HasName("shockers_pkey"); + + b.HasIndex("Device") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ControlledBy") + .HasColumnType("uuid") + .HasColumnName("controlled_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .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("ControlledBy"); + + b.HasIndex("ShockerId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_control_logs", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShare", b => + { + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("SharedWith") + .HasColumnType("uuid") + .HasColumnName("shared_with"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.HasKey("ShockerId", "SharedWith") + .HasName("shocker_shares_pkey"); + + b.HasIndex("SharedWith") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_shares", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + 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.ShockerSharesLink", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.HasKey("Id") + .HasName("shocker_shares_links_pkey"); + + b.HasIndex("OwnerId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_shares_links", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLinksShocker", b => + { + b.Property("ShareLinkId") + .HasColumnType("uuid") + .HasColumnName("share_link_id"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("Cooldown") + .HasColumnType("integer") + .HasColumnName("cooldown"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .HasColumnType("boolean") + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .HasColumnType("boolean") + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .HasColumnType("boolean") + .HasColumnName("perm_vibrate"); + + b.HasKey("ShareLinkId", "ShockerId") + .HasName("shocker_shares_links_shockers_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("shocker_shares_links_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", 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("EmailActived") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("email_actived"); + + 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.Property("Rank") + .HasColumnType("rank_type") + .HasColumnName("rank"); + + 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.UsersActivation", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("users_activation_pkey"); + + b.HasIndex("UserId"); + + b.ToTable("users_activation", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersEmailChange", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("users_email_change_pkey"); + + b.HasIndex("CreatedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UsedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UserId"); + + b.ToTable("users_email_changes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersNameChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OldName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("old_name"); + + b.HasKey("Id", "UserId") + .HasName("users_name_changes_pkey"); + + b.HasIndex("CreatedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("OldName") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("users_name_changes", (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_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "OwnerNavigation") + .WithMany("Devices") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("owner_user_id"); + + b.Navigation("OwnerNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "DeviceNavigation") + .WithMany("DeviceOtaUpdates") + .HasForeignKey("Device") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("device_ota_updates_device"); + + b.Navigation("DeviceNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PasswordReset", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("PasswordResets") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "OwnerNavigation") + .WithMany("ShareRequestOwnerNavigations") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_owner"); + + b.HasOne("OpenShock.Common.OpenShockDb.User", "UserNavigation") + .WithMany("ShareRequestUserNavigations") + .HasForeignKey("User") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_share_requests_user"); + + b.Navigation("OwnerNavigation"); + + b.Navigation("UserNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequestsShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.ShareRequest", "ShareRequestNavigation") + .WithMany("ShareRequestsShockers") + .HasForeignKey("ShareRequest") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_shockers_share_request"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "ShockerNavigation") + .WithMany("ShareRequestsShockers") + .HasForeignKey("Shocker") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_shockers_shocker"); + + b.Navigation("ShareRequestNavigation"); + + b.Navigation("ShockerNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "DeviceNavigation") + .WithMany("Shockers") + .HasForeignKey("Device") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("device_id"); + + b.Navigation("DeviceNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "ControlledByNavigation") + .WithMany("ShockerControlLogs") + .HasForeignKey("ControlledBy") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_controlled_by"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerControlLogs") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shocker_id"); + + b.Navigation("ControlledByNavigation"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShare", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "SharedWithNavigation") + .WithMany("ShockerShares") + .HasForeignKey("SharedWith") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("shared_with_user_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerShares") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("ref_shocker_id"); + + b.Navigation("SharedWithNavigation"); + + 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_id"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") + .WithMany("ShockerSharesLinks") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLinksShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.ShockerSharesLink", "ShareLink") + .WithMany("ShockerSharesLinksShockers") + .HasForeignKey("ShareLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("share_link_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerSharesLinksShockers") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("shocker_id"); + + b.Navigation("ShareLink"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersActivation", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersActivations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersEmailChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersEmailChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersNameChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersNameChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Navigation("DeviceOtaUpdates"); + + b.Navigation("Shockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.Navigation("ShareRequestsShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Navigation("ShareRequestsShockers"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShareCodes"); + + b.Navigation("ShockerShares"); + + b.Navigation("ShockerSharesLinksShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.Navigation("ShockerSharesLinksShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Navigation("ApiTokens"); + + b.Navigation("Devices"); + + b.Navigation("PasswordResets"); + + b.Navigation("ShareRequestOwnerNavigations"); + + b.Navigation("ShareRequestUserNavigations"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShares"); + + b.Navigation("ShockerSharesLinks"); + + b.Navigation("UsersActivations"); + + b.Navigation("UsersEmailChanges"); + + b.Navigation("UsersNameChanges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Common/Migrations/20241105235041_RestrictFieldLengths.cs b/Common/Migrations/20241105235041_RestrictFieldLengths.cs new file mode 100644 index 00000000..6c96cfab --- /dev/null +++ b/Common/Migrations/20241105235041_RestrictFieldLengths.cs @@ -0,0 +1,298 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class RestrictFieldLengths : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "old_name", + table: "users_name_changes", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "secret", + table: "users_email_changes", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "email", + table: "users_email_changes", + type: "character varying(320)", + maxLength: 320, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "secret", + table: "users_activation", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "password_hash", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "name", + table: "users", + type: "character varying(32)", + maxLength: 32, + nullable: false, + collation: "ndcoll", + oldClrType: typeof(string), + oldType: "character varying", + oldCollation: "ndcoll"); + + migrationBuilder.AlterColumn( + name: "email", + table: "users", + type: "character varying(320)", + maxLength: 320, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "name", + table: "shocker_shares_links", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "custom_name", + table: "shocker_control_logs", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "secret", + table: "password_resets", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "name", + table: "devices", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "version", + table: "device_ota_updates", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + + migrationBuilder.AlterColumn( + name: "message", + table: "device_ota_updates", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "token", + table: "api_tokens", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "created_by_ip", + table: "api_tokens", + type: "character varying(40)", + maxLength: 40, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "old_name", + table: "users_name_changes", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "secret", + table: "users_email_changes", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "email", + table: "users_email_changes", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(320)", + oldMaxLength: 320); + + migrationBuilder.AlterColumn( + name: "secret", + table: "users_activation", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "password_hash", + table: "users", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "name", + table: "users", + type: "character varying", + nullable: false, + collation: "ndcoll", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldCollation: "ndcoll"); + + migrationBuilder.AlterColumn( + name: "email", + table: "users", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(320)", + oldMaxLength: 320); + + migrationBuilder.AlterColumn( + name: "name", + table: "shocker_shares_links", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "custom_name", + table: "shocker_control_logs", + type: "character varying", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "secret", + table: "password_resets", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "name", + table: "devices", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "version", + table: "device_ota_updates", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "message", + table: "device_ota_updates", + type: "character varying", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "token", + table: "api_tokens", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "created_by_ip", + table: "api_tokens", + type: "character varying", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(40)", + oldMaxLength: 40); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index ac413237..4d84fc17 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -29,6 +29,84 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("OpenShock.Common.OpenShockDb.AdminUsersView", b => + { + b.Property("ApiTokenCount") + .HasColumnType("integer") + .HasColumnName("api_token_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeviceCount") + .HasColumnType("integer") + .HasColumnName("device_count"); + + b.Property("Email") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("email"); + + b.Property("EmailActived") + .HasColumnType("boolean") + .HasColumnName("email_actived"); + + 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("Rank") + .HasColumnType("rank_type") + .HasColumnName("rank"); + + b.Property("ShockerControlLogCount") + .HasColumnType("integer") + .HasColumnName("shocker_control_log_count"); + + b.Property("ShockerCount") + .HasColumnType("integer") + .HasColumnName("shocker_count"); + + b.Property("ShockerShareCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_count"); + + b.Property("ShockerShareLinkCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_link_count"); + + b.Property("UserActivationCount") + .HasColumnType("integer") + .HasColumnName("user_activation_count"); + + b.ToTable((string)null); + + b.ToView("admin_users_view", (string)null); + }); + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b => { b.Property("Id") @@ -37,7 +115,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedByIp") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(40) + .HasColumnType("character varying(40)") .HasColumnName("created_by_ip"); b.Property("CreatedOn") @@ -65,8 +144,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Token") .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("token"); b.Property("UserId") @@ -106,7 +185,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("name"); b.Property("Owner") @@ -148,7 +228,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP"); b.Property("Message") - .HasColumnType("character varying") + .HasMaxLength(128) + .HasColumnType("character varying(128)") .HasColumnName("message"); b.Property("Status") @@ -157,7 +238,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Version") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("version"); b.HasKey("Device", "UpdateId") @@ -183,7 +265,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Secret") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(100) + .HasColumnType("character varying(100)") .HasColumnName("secret"); b.Property("UsedOn") @@ -248,7 +331,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("limit_duration"); - b.Property("LimitIntensity") + b.Property("LimitIntensity") .HasColumnType("smallint") .HasColumnName("limit_intensity"); @@ -346,7 +429,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP"); b.Property("CustomName") - .HasColumnType("character varying") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("custom_name"); b.Property("Duration") @@ -513,7 +597,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("name"); b.Property("OwnerId") @@ -597,7 +682,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Email") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(320) + .HasColumnType("character varying(320)") .HasColumnName("email"); b.Property("EmailActived") @@ -608,13 +694,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(32) + .HasColumnType("character varying(32)") .HasColumnName("name") .UseCollation("ndcoll"); b.Property("PasswordHash") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(100) + .HasColumnType("character varying(100)") .HasColumnName("password_hash"); b.Property("Rank") @@ -649,7 +737,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Secret") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(128) + .HasColumnType("character varying(128)") .HasColumnName("secret"); b.Property("UsedOn") @@ -682,12 +771,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Email") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(320) + .HasColumnType("character varying(320)") .HasColumnName("email"); b.Property("Secret") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(128) + .HasColumnType("character varying(128)") .HasColumnName("secret"); b.Property("UsedOn") @@ -733,7 +824,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("OldName") .IsRequired() - .HasColumnType("character varying") + .HasMaxLength(32) + .HasColumnType("character varying(32)") .HasColumnName("old_name"); b.HasKey("Id", "UserId") diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index f8aee3fb..ccbafbef 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using OpenShock.Common.Constants; +using OpenShock.Common.Utils; namespace OpenShock.Common.OpenShockDb; @@ -82,8 +83,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .ValueGeneratedNever() .HasColumnName("id"); entity.Property(e => e.CreatedByIp) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.IpAddressMaxLength) + .VarCharWithLength(HardLimits.IpAddressMaxLength) .HasColumnName("created_by_ip"); entity.Property(e => e.CreatedOn) .HasDefaultValueSql("CURRENT_TIMESTAMP") @@ -123,8 +123,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.Name) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.HubNameMaxLength) + .VarCharWithLength(HardLimits.HubNameMaxLength) .HasColumnName("name"); entity.Property(e => e.Owner).HasColumnName("owner"); entity.Property(e => e.Token) @@ -150,12 +149,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.Message) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.OtaUpdateMessageMaxLength) + .VarCharWithLength(HardLimits.OtaUpdateMessageMaxLength) .HasColumnName("message"); entity.Property(e => e.Version) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.SemVerMaxLength) + .VarCharWithLength(HardLimits.SemVerMaxLength) .HasColumnName("version"); entity.Property(e => e.Status).HasColumnType("ota_update_status").HasColumnName("status"); @@ -181,8 +178,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.Secret) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.PasswordResetSecretMaxLength) + .VarCharWithLength(HardLimits.PasswordResetSecretMaxLength) .HasColumnName("secret"); entity.Property(e => e.UsedOn) .HasColumnName("used_on"); @@ -297,8 +293,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.CustomName) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.ShockerControlLogCustomNameMaxLength) + .VarCharWithLength(HardLimits.ShockerControlLogCustomNameMaxLength) .HasColumnName("custom_name"); entity.Property(e => e.Duration).HasColumnName("duration"); entity.Property(e => e.Intensity).HasColumnName("intensity"); @@ -404,8 +399,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_on"); entity.Property(e => e.ExpiresOn).HasColumnName("expires_on"); entity.Property(e => e.Name) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.ShockerShareLinkNameMaxLength) + .VarCharWithLength(HardLimits.ShockerShareLinkNameMaxLength) .HasColumnName("name"); entity.Property(e => e.OwnerId).HasColumnName("owner_id"); @@ -460,20 +454,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_at"); entity.Property(e => e.Email) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.EmailAddressMaxLength) + .VarCharWithLength(HardLimits.EmailAddressMaxLength) .HasColumnName("email"); entity.Property(e => e.EmailActived) .HasDefaultValue(false) .HasColumnName("email_actived"); entity.Property(e => e.Name) .UseCollation("ndcoll") - .HasColumnType("character varying") - .HasMaxLength(HardLimits.UsernameMaxLength) + .VarCharWithLength(HardLimits.UsernameMaxLength) .HasColumnName("name"); entity.Property(e => e.PasswordHash) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.PasswordHashMaxLength) + .VarCharWithLength(HardLimits.PasswordHashMaxLength) .HasColumnName("password_hash"); entity.Property(e => e.Rank) .HasColumnType("rank_type") @@ -495,8 +486,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.Secret) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.UserActivationSecretMaxLength) + .VarCharWithLength(HardLimits.UserActivationSecretMaxLength) .HasColumnName("secret"); entity.Property(e => e.UsedOn).HasColumnName("used_on"); entity.Property(e => e.UserId).HasColumnName("user_id"); @@ -525,12 +515,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.Email) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.EmailAddressMaxLength) + .VarCharWithLength(HardLimits.EmailAddressMaxLength) .HasColumnName("email"); entity.Property(e => e.Secret) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.UserEmailChangeSecretMaxLength) + .VarCharWithLength(HardLimits.UserEmailChangeSecretMaxLength) .HasColumnName("secret"); entity.Property(e => e.UsedOn).HasColumnName("used_on"); entity.Property(e => e.UserId).HasColumnName("user_id"); @@ -561,8 +549,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP") .HasColumnName("created_on"); entity.Property(e => e.OldName) - .HasColumnType("character varying") - .HasMaxLength(HardLimits.UsernameMaxLength) + .VarCharWithLength(HardLimits.UsernameMaxLength) .HasColumnName("old_name"); entity.HasOne(d => d.User).WithMany(p => p.UsersNameChanges) diff --git a/Common/Utils/PropertyBuilderExtension.cs b/Common/Utils/PropertyBuilderExtension.cs new file mode 100644 index 00000000..f833b807 --- /dev/null +++ b/Common/Utils/PropertyBuilderExtension.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace OpenShock.Common.Utils; + +public static class PropertyBuilderExtension +{ + public static PropertyBuilder VarCharWithLength(this PropertyBuilder propertyBuilder, int length) + { + return propertyBuilder.HasColumnType($"character varying({length})").HasMaxLength(length); + } +} \ No newline at end of file From 90738e4bb6e59b5a2c8525da7a919b97b9e67850 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 6 Nov 2024 01:11:22 +0100 Subject: [PATCH 4/6] MonkaS type migration --- .../20241031153812_AddAdminUsersView.cs | 57 +++++++++++-------- .../20241105235041_RestrictFieldLengths.cs | 8 +++ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Common/Migrations/20241031153812_AddAdminUsersView.cs b/Common/Migrations/20241031153812_AddAdminUsersView.cs index 4520d7cd..e4985445 100644 --- a/Common/Migrations/20241031153812_AddAdminUsersView.cs +++ b/Common/Migrations/20241031153812_AddAdminUsersView.cs @@ -8,39 +8,46 @@ namespace OpenShock.Common.Migrations /// public partial class AddAdminUsersView : Migration { + public const string AdminUsersViewCreateQuery = + """ + CREATE VIEW admin_users_view AS + SELECT + u.id, + u.name, + u.email, + SPLIT_PART(u.password_hash, ':', 1) AS password_hash_type, + u.created_at, + u.email_actived, + u.rank, + (SELECT COUNT(*) FROM api_tokens ato WHERE ato.user_id = u.id) AS api_token_count, + (SELECT COUNT(*) FROM password_resets pre WHERE pre.user_id = u.id) AS password_reset_count, + (SELECT COUNT(*) FROM shocker_shares ssh WHERE ssh.shared_with = u.id) AS shocker_share_count, + (SELECT COUNT(*) FROM shocker_shares_links ssl WHERE ssl.owner_id = u.id) AS shocker_share_link_count, + (SELECT COUNT(*) FROM users_email_changes uec WHERE uec.user_id = u.id) AS email_change_request_count, + (SELECT COUNT(*) FROM users_name_changes unc WHERE unc.user_id = u.id) AS name_change_request_count, + (SELECT COUNT(*) FROM users_activation uac WHERE uac.user_id = u.id) AS user_activation_count, + (SELECT COUNT(*) FROM devices dev WHERE dev.owner = u.id) AS device_count, + (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device WHERE dev.owner = u.id) AS shocker_count, + (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device JOIN shocker_control_logs scl ON scl.shocker_id = sck.id WHERE dev.owner = u.id) AS shocker_control_log_count + FROM + users u; + """; + + public const string AdminUsersViewDropQuery = + """ + DROP VIEW admin_users_view + """; + /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql( - """ - CREATE VIEW admin_users_view AS - SELECT - u.id, - u.name, - u.email, - SPLIT_PART(u.password_hash, ':', 1) AS password_hash_type, - u.created_at, - u.email_actived, - u.rank, - (SELECT COUNT(*) FROM api_tokens ato WHERE ato.user_id = u.id) AS api_token_count, - (SELECT COUNT(*) FROM password_resets pre WHERE pre.user_id = u.id) AS password_reset_count, - (SELECT COUNT(*) FROM shocker_shares ssh WHERE ssh.shared_with = u.id) AS shocker_share_count, - (SELECT COUNT(*) FROM shocker_shares_links ssl WHERE ssl.owner_id = u.id) AS shocker_share_link_count, - (SELECT COUNT(*) FROM users_email_changes uec WHERE uec.user_id = u.id) AS email_change_request_count, - (SELECT COUNT(*) FROM users_name_changes unc WHERE unc.user_id = u.id) AS name_change_request_count, - (SELECT COUNT(*) FROM users_activation uac WHERE uac.user_id = u.id) AS user_activation_count, - (SELECT COUNT(*) FROM devices dev WHERE dev.owner = u.id) AS device_count, - (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device WHERE dev.owner = u.id) AS shocker_count, - (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device JOIN shocker_control_logs scl ON scl.shocker_id = sck.id WHERE dev.owner = u.id) AS shocker_control_log_count - FROM - users u; - """); + migrationBuilder.Sql(AdminUsersViewCreateQuery); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql("DROP VIEW admin_users_view"); + migrationBuilder.Sql(AdminUsersViewDropQuery); } } } diff --git a/Common/Migrations/20241105235041_RestrictFieldLengths.cs b/Common/Migrations/20241105235041_RestrictFieldLengths.cs index 6c96cfab..47fc3d7f 100644 --- a/Common/Migrations/20241105235041_RestrictFieldLengths.cs +++ b/Common/Migrations/20241105235041_RestrictFieldLengths.cs @@ -10,6 +10,8 @@ public partial class RestrictFieldLengths : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewDropQuery); + migrationBuilder.AlterColumn( name: "old_name", table: "users_name_changes", @@ -149,11 +151,15 @@ protected override void Up(MigrationBuilder migrationBuilder) nullable: false, oldClrType: typeof(string), oldType: "character varying"); + + migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewCreateQuery); } /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewDropQuery); + migrationBuilder.AlterColumn( name: "old_name", table: "users_name_changes", @@ -293,6 +299,8 @@ protected override void Down(MigrationBuilder migrationBuilder) oldClrType: typeof(string), oldType: "character varying(40)", oldMaxLength: 40); + + migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewCreateQuery); } } } From bca7970cb3748dfdadd3b141dde8644db64cc1e4 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 6 Nov 2024 01:27:16 +0100 Subject: [PATCH 5/6] Truncate existing shocker sharelink names --- Common/Migrations/20241105235041_RestrictFieldLengths.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Common/Migrations/20241105235041_RestrictFieldLengths.cs b/Common/Migrations/20241105235041_RestrictFieldLengths.cs index 47fc3d7f..20334a36 100644 --- a/Common/Migrations/20241105235041_RestrictFieldLengths.cs +++ b/Common/Migrations/20241105235041_RestrictFieldLengths.cs @@ -10,6 +10,10 @@ public partial class RestrictFieldLengths : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { + // Truncate shocker sharelinks names to 64 characters + migrationBuilder.Sql("UPDATE public.shocker_shares_links SET name = LEFT(name, 64) WHERE LENGTH(name) > 64"); + + // We need to drop the view to modify the target table migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewDropQuery); migrationBuilder.AlterColumn( @@ -152,12 +156,14 @@ protected override void Up(MigrationBuilder migrationBuilder) oldClrType: typeof(string), oldType: "character varying"); + // Re-Create the view migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewCreateQuery); } /// protected override void Down(MigrationBuilder migrationBuilder) { + // We need to drop the view to modify the target table migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewDropQuery); migrationBuilder.AlterColumn( @@ -300,6 +306,7 @@ protected override void Down(MigrationBuilder migrationBuilder) oldType: "character varying(40)", oldMaxLength: 40); + // Re-Create the view migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewCreateQuery); } } From afbd3ab173bfd286fcfadf6355b54d068a0f8984 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 6 Nov 2024 01:33:41 +0100 Subject: [PATCH 6/6] Truncate Dubya & Myrkur's logs --- Common/Migrations/20241105235041_RestrictFieldLengths.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Common/Migrations/20241105235041_RestrictFieldLengths.cs b/Common/Migrations/20241105235041_RestrictFieldLengths.cs index 20334a36..bc54746c 100644 --- a/Common/Migrations/20241105235041_RestrictFieldLengths.cs +++ b/Common/Migrations/20241105235041_RestrictFieldLengths.cs @@ -12,6 +12,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { // Truncate shocker sharelinks names to 64 characters migrationBuilder.Sql("UPDATE public.shocker_shares_links SET name = LEFT(name, 64) WHERE LENGTH(name) > 64"); + migrationBuilder.Sql("UPDATE public.shocker_control_logs SET custom_name = LEFT(custom_name, 64) WHERE LENGTH(custom_name) > 64"); // We need to drop the view to modify the target table migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewDropQuery);