feat: make some limits configurable
This commit is contained in:
		
							parent
							
								
									74800b46ef
								
							
						
					
					
						commit
						373d97e70a
					
				
					 11 changed files with 312 additions and 218 deletions
				
			
		|  | @ -31,6 +31,7 @@ public class Config | |||
|     public LoggingConfig Logging { get; init; } = new(); | ||||
|     public DatabaseConfig Database { get; init; } = new(); | ||||
|     public StorageConfig Storage { get; init; } = new(); | ||||
|     public LimitsConfig Limits { get; init; } = new(); | ||||
|     public EmailAuthConfig EmailAuth { get; init; } = new(); | ||||
|     public DiscordAuthConfig DiscordAuth { get; init; } = new(); | ||||
|     public GoogleAuthConfig GoogleAuth { get; init; } = new(); | ||||
|  | @ -93,4 +94,17 @@ public class Config | |||
|         public string? ClientId { get; init; } | ||||
|         public string? ClientSecret { get; init; } | ||||
|     } | ||||
| 
 | ||||
|     public class LimitsConfig | ||||
|     { | ||||
|         public int MaxMemberCount { get; init; } = 1000; | ||||
| 
 | ||||
|         public int MaxUsernameLength { get; init; } = 40; | ||||
|         public int MaxMemberNameLength { get; init; } = 100; | ||||
|         public int MaxDisplayNameLength { get; init; } = 100; | ||||
|         public int MaxLinks { get; init; } = 25; | ||||
|         public int MaxLinkLength { get; init; } = 256; | ||||
|         public int MaxBioLength { get; init; } = 1024; | ||||
|         public int MaxAvatarLength { get; init; } = 1_500_000; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -38,7 +38,9 @@ public class MembersController( | |||
|     ISnowflakeGenerator snowflakeGenerator, | ||||
|     ObjectStorageService objectStorageService, | ||||
|     IQueue queue, | ||||
|     IClock clock | ||||
|     IClock clock, | ||||
|     ValidationService validationService, | ||||
|     Config config | ||||
| ) : ApiControllerBase | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<MembersController>(); | ||||
|  | @ -65,8 +67,6 @@ public class MembersController( | |||
|         return Ok(memberRenderer.RenderMember(member, CurrentToken)); | ||||
|     } | ||||
| 
 | ||||
|     public const int MaxMemberCount = 1000; | ||||
| 
 | ||||
|     [HttpPost("/api/v2/users/@me/members")] | ||||
|     [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)] | ||||
|     [Authorize("member.create")] | ||||
|  | @ -77,10 +77,10 @@ public class MembersController( | |||
|     { | ||||
|         ValidationUtils.Validate( | ||||
|             [ | ||||
|                 ("name", ValidationUtils.ValidateMemberName(req.Name)), | ||||
|                 ("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)), | ||||
|                 ("bio", ValidationUtils.ValidateBio(req.Bio)), | ||||
|                 ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)), | ||||
|                 ("name", validationService.ValidateMemberName(req.Name)), | ||||
|                 ("display_name", validationService.ValidateDisplayName(req.DisplayName)), | ||||
|                 ("bio", validationService.ValidateBio(req.Bio)), | ||||
|                 ("avatar", validationService.ValidateAvatar(req.Avatar)), | ||||
|                 .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), | ||||
|                 .. ValidationUtils.ValidateFieldEntries( | ||||
|                     req.Names?.ToArray(), | ||||
|  | @ -91,12 +91,12 @@ public class MembersController( | |||
|                     req.Pronouns?.ToArray(), | ||||
|                     CurrentUser!.CustomPreferences | ||||
|                 ), | ||||
|                 .. ValidationUtils.ValidateLinks(req.Links), | ||||
|                 .. validationService.ValidateLinks(req.Links), | ||||
|             ] | ||||
|         ); | ||||
| 
 | ||||
|         int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); | ||||
|         if (memberCount >= MaxMemberCount) | ||||
|         if (memberCount >= config.Limits.MaxMemberCount) | ||||
|             throw new ApiError.BadRequest("Maximum number of members reached"); | ||||
| 
 | ||||
|         var member = new Member | ||||
|  | @ -163,25 +163,25 @@ public class MembersController( | |||
|         // These should only take effect when a member's name is changed, not on other changes. | ||||
|         if (req.Name != null && req.Name != member.Name) | ||||
|         { | ||||
|             errors.Add(("name", ValidationUtils.ValidateMemberName(req.Name))); | ||||
|             errors.Add(("name", validationService.ValidateMemberName(req.Name))); | ||||
|             member.Name = req.Name; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.DisplayName))) | ||||
|         { | ||||
|             errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName))); | ||||
|             errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName))); | ||||
|             member.DisplayName = req.DisplayName; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Bio))) | ||||
|         { | ||||
|             errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio))); | ||||
|             errors.Add(("bio", validationService.ValidateBio(req.Bio))); | ||||
|             member.Bio = req.Bio; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Links))) | ||||
|         { | ||||
|             errors.AddRange(ValidationUtils.ValidateLinks(req.Links)); | ||||
|             errors.AddRange(validationService.ValidateLinks(req.Links)); | ||||
|             member.Links = req.Links ?? []; | ||||
|         } | ||||
| 
 | ||||
|  | @ -228,7 +228,7 @@ public class MembersController( | |||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Avatar))) | ||||
|             errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); | ||||
|             errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar))); | ||||
| 
 | ||||
|         ValidationUtils.Validate(errors); | ||||
|         // This is fired off regardless of whether the transaction is committed | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ using Microsoft.AspNetCore.Mvc; | |||
| namespace Foxnouns.Backend.Controllers; | ||||
| 
 | ||||
| [Route("/api/v2/meta")] | ||||
| public partial class MetaController : ApiControllerBase | ||||
| public partial class MetaController(Config config) : ApiControllerBase | ||||
| { | ||||
|     private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; | ||||
| 
 | ||||
|  | @ -40,8 +40,8 @@ public partial class MetaController : ApiControllerBase | |||
|                     (int)FoxnounsMetrics.UsersActiveDayCount.Value | ||||
|                 ), | ||||
|                 new LimitsResponse( | ||||
|                     MembersController.MaxMemberCount, | ||||
|                     ValidationUtils.MaxBioLength, | ||||
|                     config.Limits.MaxMemberCount, | ||||
|                     config.Limits.MaxBioLength, | ||||
|                     ValidationUtils.MaxCustomPreferences, | ||||
|                     AuthUtils.MaxAuthMethodsPerType, | ||||
|                     FlagsController.MaxFlagCount | ||||
|  |  | |||
|  | @ -35,7 +35,8 @@ public class UsersController( | |||
|     UserRendererService userRenderer, | ||||
|     ISnowflakeGenerator snowflakeGenerator, | ||||
|     IQueue queue, | ||||
|     IClock clock | ||||
|     IClock clock, | ||||
|     ValidationService validationService | ||||
| ) : ApiControllerBase | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<UsersController>(); | ||||
|  | @ -65,25 +66,25 @@ public class UsersController( | |||
| 
 | ||||
|         if (req.Username != null && req.Username != user.Username) | ||||
|         { | ||||
|             errors.Add(("username", ValidationUtils.ValidateUsername(req.Username))); | ||||
|             errors.Add(("username", validationService.ValidateUsername(req.Username))); | ||||
|             user.Username = req.Username; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.DisplayName))) | ||||
|         { | ||||
|             errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName))); | ||||
|             errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName))); | ||||
|             user.DisplayName = req.DisplayName; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Bio))) | ||||
|         { | ||||
|             errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio))); | ||||
|             errors.Add(("bio", validationService.ValidateBio(req.Bio))); | ||||
|             user.Bio = req.Bio; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Links))) | ||||
|         { | ||||
|             errors.AddRange(ValidationUtils.ValidateLinks(req.Links)); | ||||
|             errors.AddRange(validationService.ValidateLinks(req.Links)); | ||||
|             user.Links = req.Links ?? []; | ||||
|         } | ||||
| 
 | ||||
|  | @ -123,7 +124,7 @@ public class UsersController( | |||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Avatar))) | ||||
|             errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); | ||||
|             errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar))); | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.MemberTitle))) | ||||
|         { | ||||
|  | @ -133,7 +134,9 @@ public class UsersController( | |||
|             } | ||||
|             else | ||||
|             { | ||||
|                 errors.Add(("member_title", ValidationUtils.ValidateDisplayName(req.MemberTitle))); | ||||
|                 errors.Add( | ||||
|                     ("member_title", validationService.ValidateDisplayName(req.MemberTitle)) | ||||
|                 ); | ||||
|                 user.MemberTitle = req.MemberTitle; | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -122,6 +122,7 @@ public static class WebApplicationExtensions | |||
|                     .AddScoped<FediverseAuthService>() | ||||
|                     .AddScoped<ObjectStorageService>() | ||||
|                     .AddTransient<DataCleanupService>() | ||||
|                     .AddTransient<ValidationService>() | ||||
|                     // Background services | ||||
|                     .AddHostedService<PeriodicTasksService>() | ||||
|                     // Transient jobs | ||||
|  |  | |||
|  | @ -29,7 +29,8 @@ public class AuthService( | |||
|     ILogger logger, | ||||
|     DatabaseContext db, | ||||
|     ISnowflakeGenerator snowflakeGenerator, | ||||
|     UserRendererService userRenderer | ||||
|     UserRendererService userRenderer, | ||||
|     ValidationService validationService | ||||
| ) | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<AuthService>(); | ||||
|  | @ -49,7 +50,7 @@ public class AuthService( | |||
|         // Validate username and whether it's not taken | ||||
|         ValidationUtils.Validate( | ||||
|             [ | ||||
|                 ("username", ValidationUtils.ValidateUsername(username)), | ||||
|                 ("username", validationService.ValidateUsername(username)), | ||||
|                 ("password", ValidationUtils.ValidatePassword(password)), | ||||
|             ] | ||||
|         ); | ||||
|  | @ -97,7 +98,7 @@ public class AuthService( | |||
|         AssertValidAuthType(authType, instance); | ||||
| 
 | ||||
|         // Validate username and whether it's not taken | ||||
|         ValidationUtils.Validate([("username", ValidationUtils.ValidateUsername(username))]); | ||||
|         ValidationUtils.Validate([("username", validationService.ValidateUsername(username))]); | ||||
|         if (await db.Users.AnyAsync(u => u.Username == username, ct)) | ||||
|             throw new ApiError.BadRequest("Username is already taken", "username", username); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										256
									
								
								Foxnouns.Backend/Services/ValidationService.Strings.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								Foxnouns.Backend/Services/ValidationService.Strings.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,256 @@ | |||
| // Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published | ||||
| // by the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| using System.Text.RegularExpressions; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Services; | ||||
| 
 | ||||
| public partial class ValidationService | ||||
| { | ||||
|     private static readonly string[] InvalidUsernames = | ||||
|     [ | ||||
|         "..", | ||||
|         "admin", | ||||
|         "administrator", | ||||
|         "mod", | ||||
|         "moderator", | ||||
|         "api", | ||||
|         "page", | ||||
|         "pronouns", | ||||
|         "settings", | ||||
|         "pronouns.cc", | ||||
|         "pronounscc", | ||||
|     ]; | ||||
| 
 | ||||
|     private static readonly string[] InvalidMemberNames = | ||||
|     [ | ||||
|         // these break routing outright | ||||
|         ".", | ||||
|         "..", | ||||
|         // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible | ||||
|         "edit", | ||||
|     ]; | ||||
| 
 | ||||
|     public ValidationError? ValidateUsername(string username) | ||||
|     { | ||||
|         if (!UsernameRegex().IsMatch(username)) | ||||
|         { | ||||
|             if (username.Length < 2) | ||||
|             { | ||||
|                 return ValidationError.LengthError( | ||||
|                     "Username is too short", | ||||
|                     2, | ||||
|                     _limits.MaxUsernameLength, | ||||
|                     username.Length | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             if (username.Length > _limits.MaxUsernameLength) | ||||
|             { | ||||
|                 return ValidationError.LengthError( | ||||
|                     "Username is too long", | ||||
|                     2, | ||||
|                     _limits.MaxUsernameLength, | ||||
|                     username.Length | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             return ValidationError.GenericValidationError( | ||||
|                 "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", | ||||
|                 username | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if ( | ||||
|             InvalidUsernames.Any(u => | ||||
|                 string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase) | ||||
|             ) | ||||
|         ) | ||||
|         { | ||||
|             return ValidationError.GenericValidationError("Username is not allowed", username); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public ValidationError? ValidateMemberName(string memberName) | ||||
|     { | ||||
|         if (!MemberRegex().IsMatch(memberName)) | ||||
|         { | ||||
|             if (memberName.Length < 1) | ||||
|             { | ||||
|                 return ValidationError.LengthError( | ||||
|                     "Name is too short", | ||||
|                     1, | ||||
|                     _limits.MaxMemberNameLength, | ||||
|                     memberName.Length | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             if (memberName.Length > _limits.MaxMemberNameLength) | ||||
|             { | ||||
|                 return ValidationError.LengthError( | ||||
|                     "Name is too long", | ||||
|                     1, | ||||
|                     _limits.MaxMemberNameLength, | ||||
|                     memberName.Length | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             return ValidationError.GenericValidationError( | ||||
|                 "Member name cannot contain any of the following: " | ||||
|                     + " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , " | ||||
|                     + "and cannot be one or two periods", | ||||
|                 memberName | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if ( | ||||
|             InvalidMemberNames.Any(u => | ||||
|                 string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase) | ||||
|             ) | ||||
|         ) | ||||
|         { | ||||
|             return ValidationError.GenericValidationError("Name is not allowed", memberName); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public ValidationError? ValidateDisplayName(string? displayName) | ||||
|     { | ||||
|         if (displayName?.Length == 0) | ||||
|         { | ||||
|             return ValidationError.LengthError( | ||||
|                 "Display name is too short", | ||||
|                 1, | ||||
|                 _limits.MaxDisplayNameLength, | ||||
|                 displayName.Length | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (displayName?.Length > _limits.MaxDisplayNameLength) | ||||
|         { | ||||
|             return ValidationError.LengthError( | ||||
|                 "Display name is too long", | ||||
|                 1, | ||||
|                 _limits.MaxDisplayNameLength, | ||||
|                 displayName.Length | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) | ||||
|     { | ||||
|         if (links == null) | ||||
|             return []; | ||||
|         if (links.Length > _limits.MaxLinks) | ||||
|         { | ||||
|             return | ||||
|             [ | ||||
|                 ( | ||||
|                     "links", | ||||
|                     ValidationError.LengthError("Too many links", 0, _limits.MaxLinks, links.Length) | ||||
|                 ), | ||||
|             ]; | ||||
|         } | ||||
| 
 | ||||
|         var errors = new List<(string, ValidationError?)>(); | ||||
|         foreach ((string link, int idx) in links.Select((l, i) => (l, i))) | ||||
|         { | ||||
|             if (link.Length == 0) | ||||
|             { | ||||
|                 errors.Add( | ||||
|                     ( | ||||
|                         $"links.{idx}", | ||||
|                         ValidationError.LengthError( | ||||
|                             "Link cannot be empty", | ||||
|                             1, | ||||
|                             _limits.MaxLinkLength, | ||||
|                             0 | ||||
|                         ) | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|             else if (link.Length > _limits.MaxLinkLength) | ||||
|             { | ||||
|                 errors.Add( | ||||
|                     ( | ||||
|                         $"links.{idx}", | ||||
|                         ValidationError.LengthError( | ||||
|                             "Link is too long", | ||||
|                             1, | ||||
|                             _limits.MaxLinkLength, | ||||
|                             link.Length | ||||
|                         ) | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return errors; | ||||
|     } | ||||
| 
 | ||||
|     public ValidationError? ValidateBio(string? bio) | ||||
|     { | ||||
|         if (bio?.Length == 0) | ||||
|         { | ||||
|             return ValidationError.LengthError( | ||||
|                 "Bio is too short", | ||||
|                 1, | ||||
|                 _limits.MaxBioLength, | ||||
|                 bio.Length | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (bio?.Length > _limits.MaxBioLength) | ||||
|         { | ||||
|             return ValidationError.LengthError( | ||||
|                 "Bio is too long", | ||||
|                 1, | ||||
|                 _limits.MaxBioLength, | ||||
|                 bio.Length | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public ValidationError? ValidateAvatar(string? avatar) | ||||
|     { | ||||
|         if (avatar?.Length == 0) | ||||
|         { | ||||
|             return ValidationError.GenericValidationError("Avatar cannot be empty", null); | ||||
|         } | ||||
| 
 | ||||
|         if (avatar?.Length > _limits.MaxAvatarLength) | ||||
|         { | ||||
|             return ValidationError.GenericValidationError("Avatar is too large", null); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-US")]
 | ||||
|     private static partial Regex UsernameRegex(); | ||||
| 
 | ||||
|     [GeneratedRegex( | ||||
|         """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", | ||||
|         RegexOptions.IgnoreCase, | ||||
|         "en-US" | ||||
|     )] | ||||
|     private static partial Regex MemberRegex(); | ||||
| } | ||||
							
								
								
									
										6
									
								
								Foxnouns.Backend/Services/ValidationService.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Foxnouns.Backend/Services/ValidationService.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| namespace Foxnouns.Backend.Services; | ||||
| 
 | ||||
| public partial class ValidationService(Config config) | ||||
| { | ||||
|     private readonly Config.LimitsConfig _limits = config.Limits; | ||||
| } | ||||
|  | @ -135,7 +135,7 @@ public static class AuthUtils | |||
|         Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); | ||||
| 
 | ||||
|     public static string RandomToken(int bytes = 48) => | ||||
|         RandomUrlUnsafeToken() | ||||
|         RandomUrlUnsafeToken(bytes) | ||||
|             // Make the token URL-safe | ||||
|             .Replace('+', '-') | ||||
|             .Replace('/', '_'); | ||||
|  |  | |||
|  | @ -12,190 +12,10 @@ | |||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| using System.Text.RegularExpressions; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Utils; | ||||
| 
 | ||||
| public static partial class ValidationUtils | ||||
| { | ||||
|     private static readonly string[] InvalidUsernames = | ||||
|     [ | ||||
|         "..", | ||||
|         "admin", | ||||
|         "administrator", | ||||
|         "mod", | ||||
|         "moderator", | ||||
|         "api", | ||||
|         "page", | ||||
|         "pronouns", | ||||
|         "settings", | ||||
|         "pronouns.cc", | ||||
|         "pronounscc", | ||||
|     ]; | ||||
| 
 | ||||
|     private static readonly string[] InvalidMemberNames = | ||||
|     [ | ||||
|         // these break routing outright | ||||
|         ".", | ||||
|         "..", | ||||
|         // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible | ||||
|         "edit", | ||||
|     ]; | ||||
| 
 | ||||
|     public static ValidationError? ValidateUsername(string username) | ||||
|     { | ||||
|         if (!UsernameRegex().IsMatch(username)) | ||||
|         { | ||||
|             return username.Length switch | ||||
|             { | ||||
|                 < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), | ||||
|                 > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), | ||||
|                 _ => ValidationError.GenericValidationError( | ||||
|                     "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", | ||||
|                     username | ||||
|                 ), | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         if ( | ||||
|             InvalidUsernames.Any(u => | ||||
|                 string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase) | ||||
|             ) | ||||
|         ) | ||||
|         { | ||||
|             return ValidationError.GenericValidationError("Username is not allowed", username); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public static ValidationError? ValidateMemberName(string memberName) | ||||
|     { | ||||
|         if (!MemberRegex().IsMatch(memberName)) | ||||
|         { | ||||
|             return memberName.Length switch | ||||
|             { | ||||
|                 < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), | ||||
|                 > 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), | ||||
|                 _ => ValidationError.GenericValidationError( | ||||
|                     "Member name cannot contain any of the following: " | ||||
|                         + " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , " | ||||
|                         + "and cannot be one or two periods", | ||||
|                     memberName | ||||
|                 ), | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         if ( | ||||
|             InvalidMemberNames.Any(u => | ||||
|                 string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase) | ||||
|             ) | ||||
|         ) | ||||
|         { | ||||
|             return ValidationError.GenericValidationError("Name is not allowed", memberName); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public static ValidationError? ValidateDisplayName(string? displayName) | ||||
|     { | ||||
|         return displayName?.Length switch | ||||
|         { | ||||
|             0 => ValidationError.LengthError( | ||||
|                 "Display name is too short", | ||||
|                 1, | ||||
|                 100, | ||||
|                 displayName.Length | ||||
|             ), | ||||
|             > 100 => ValidationError.LengthError( | ||||
|                 "Display name is too long", | ||||
|                 1, | ||||
|                 100, | ||||
|                 displayName.Length | ||||
|             ), | ||||
|             _ => null, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private const int MaxLinks = 25; | ||||
|     private const int MaxLinkLength = 256; | ||||
| 
 | ||||
|     public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) | ||||
|     { | ||||
|         if (links == null) | ||||
|             return []; | ||||
|         if (links.Length > MaxLinks) | ||||
|         { | ||||
|             return | ||||
|             [ | ||||
|                 ("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)), | ||||
|             ]; | ||||
|         } | ||||
| 
 | ||||
|         var errors = new List<(string, ValidationError?)>(); | ||||
|         foreach ((string link, int idx) in links.Select((l, i) => (l, i))) | ||||
|         { | ||||
|             switch (link.Length) | ||||
|             { | ||||
|                 case 0: | ||||
|                     errors.Add( | ||||
|                         ( | ||||
|                             $"links.{idx}", | ||||
|                             ValidationError.LengthError("Link cannot be empty", 1, 256, 0) | ||||
|                         ) | ||||
|                     ); | ||||
|                     break; | ||||
|                 case > MaxLinkLength: | ||||
|                     errors.Add( | ||||
|                         ( | ||||
|                             $"links.{idx}", | ||||
|                             ValidationError.LengthError( | ||||
|                                 "Link is too long", | ||||
|                                 1, | ||||
|                                 MaxLinkLength, | ||||
|                                 link.Length | ||||
|                             ) | ||||
|                         ) | ||||
|                     ); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return errors; | ||||
|     } | ||||
| 
 | ||||
|     public const int MaxBioLength = 1024; | ||||
|     public const int MaxAvatarLength = 1_500_000; | ||||
| 
 | ||||
|     public static ValidationError? ValidateBio(string? bio) | ||||
|     { | ||||
|         return bio?.Length switch | ||||
|         { | ||||
|             0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length), | ||||
|             > MaxBioLength => ValidationError.LengthError( | ||||
|                 "Bio is too long", | ||||
|                 1, | ||||
|                 MaxBioLength, | ||||
|                 bio.Length | ||||
|             ), | ||||
|             _ => null, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public static ValidationError? ValidateAvatar(string? avatar) | ||||
|     { | ||||
|         return avatar?.Length switch | ||||
|         { | ||||
|             0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), | ||||
|             > MaxAvatarLength => ValidationError.GenericValidationError( | ||||
|                 "Avatar is too large", | ||||
|                 null | ||||
|             ), | ||||
|             _ => null, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public const int MaximumReportContextLength = 512; | ||||
| 
 | ||||
|     public static ValidationError? ValidateReportContext(string? context) => | ||||
|  | @ -223,14 +43,4 @@ public static partial class ValidationUtils | |||
|             ), | ||||
|             _ => null, | ||||
|         }; | ||||
| 
 | ||||
|     [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")]
 | ||||
|     private static partial Regex UsernameRegex(); | ||||
| 
 | ||||
|     [GeneratedRegex( | ||||
|         """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", | ||||
|         RegexOptions.IgnoreCase, | ||||
|         "en-NL" | ||||
|     )] | ||||
|     private static partial Regex MemberRegex(); | ||||
| } | ||||
|  |  | |||
|  | @ -43,6 +43,9 @@ AccessKey = <s3AccessKey> | |||
| SecretKey = <s3SecretKey> | ||||
| Bucket = pronounscc | ||||
| 
 | ||||
| [Limits] | ||||
| MaxMemberCount = 5000 | ||||
| 
 | ||||
| [EmailAuth] | ||||
| ; The address that emails will be sent from. If not set, email auth is disabled. | ||||
| From = noreply@accounts.pronouns.cc | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue