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 LoggingConfig Logging { get; init; } = new();
 | 
				
			||||||
    public DatabaseConfig Database { get; init; } = new();
 | 
					    public DatabaseConfig Database { get; init; } = new();
 | 
				
			||||||
    public StorageConfig Storage { get; init; } = new();
 | 
					    public StorageConfig Storage { get; init; } = new();
 | 
				
			||||||
 | 
					    public LimitsConfig Limits { get; init; } = new();
 | 
				
			||||||
    public EmailAuthConfig EmailAuth { get; init; } = new();
 | 
					    public EmailAuthConfig EmailAuth { get; init; } = new();
 | 
				
			||||||
    public DiscordAuthConfig DiscordAuth { get; init; } = new();
 | 
					    public DiscordAuthConfig DiscordAuth { get; init; } = new();
 | 
				
			||||||
    public GoogleAuthConfig GoogleAuth { get; init; } = new();
 | 
					    public GoogleAuthConfig GoogleAuth { get; init; } = new();
 | 
				
			||||||
| 
						 | 
					@ -93,4 +94,17 @@ public class Config
 | 
				
			||||||
        public string? ClientId { get; init; }
 | 
					        public string? ClientId { get; init; }
 | 
				
			||||||
        public string? ClientSecret { 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,
 | 
					    ISnowflakeGenerator snowflakeGenerator,
 | 
				
			||||||
    ObjectStorageService objectStorageService,
 | 
					    ObjectStorageService objectStorageService,
 | 
				
			||||||
    IQueue queue,
 | 
					    IQueue queue,
 | 
				
			||||||
    IClock clock
 | 
					    IClock clock,
 | 
				
			||||||
 | 
					    ValidationService validationService,
 | 
				
			||||||
 | 
					    Config config
 | 
				
			||||||
) : ApiControllerBase
 | 
					) : ApiControllerBase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly ILogger _logger = logger.ForContext<MembersController>();
 | 
					    private readonly ILogger _logger = logger.ForContext<MembersController>();
 | 
				
			||||||
| 
						 | 
					@ -65,8 +67,6 @@ public class MembersController(
 | 
				
			||||||
        return Ok(memberRenderer.RenderMember(member, CurrentToken));
 | 
					        return Ok(memberRenderer.RenderMember(member, CurrentToken));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public const int MaxMemberCount = 1000;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [HttpPost("/api/v2/users/@me/members")]
 | 
					    [HttpPost("/api/v2/users/@me/members")]
 | 
				
			||||||
    [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
 | 
					    [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
 | 
				
			||||||
    [Authorize("member.create")]
 | 
					    [Authorize("member.create")]
 | 
				
			||||||
| 
						 | 
					@ -77,10 +77,10 @@ public class MembersController(
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        ValidationUtils.Validate(
 | 
					        ValidationUtils.Validate(
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
                ("name", ValidationUtils.ValidateMemberName(req.Name)),
 | 
					                ("name", validationService.ValidateMemberName(req.Name)),
 | 
				
			||||||
                ("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)),
 | 
					                ("display_name", validationService.ValidateDisplayName(req.DisplayName)),
 | 
				
			||||||
                ("bio", ValidationUtils.ValidateBio(req.Bio)),
 | 
					                ("bio", validationService.ValidateBio(req.Bio)),
 | 
				
			||||||
                ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)),
 | 
					                ("avatar", validationService.ValidateAvatar(req.Avatar)),
 | 
				
			||||||
                .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
 | 
					                .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
 | 
				
			||||||
                .. ValidationUtils.ValidateFieldEntries(
 | 
					                .. ValidationUtils.ValidateFieldEntries(
 | 
				
			||||||
                    req.Names?.ToArray(),
 | 
					                    req.Names?.ToArray(),
 | 
				
			||||||
| 
						 | 
					@ -91,12 +91,12 @@ public class MembersController(
 | 
				
			||||||
                    req.Pronouns?.ToArray(),
 | 
					                    req.Pronouns?.ToArray(),
 | 
				
			||||||
                    CurrentUser!.CustomPreferences
 | 
					                    CurrentUser!.CustomPreferences
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                .. ValidationUtils.ValidateLinks(req.Links),
 | 
					                .. validationService.ValidateLinks(req.Links),
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct);
 | 
					        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");
 | 
					            throw new ApiError.BadRequest("Maximum number of members reached");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var member = new Member
 | 
					        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.
 | 
					        // These should only take effect when a member's name is changed, not on other changes.
 | 
				
			||||||
        if (req.Name != null && req.Name != member.Name)
 | 
					        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;
 | 
					            member.Name = req.Name;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.DisplayName)))
 | 
					        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;
 | 
					            member.DisplayName = req.DisplayName;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.Bio)))
 | 
					        if (req.HasProperty(nameof(req.Bio)))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio)));
 | 
					            errors.Add(("bio", validationService.ValidateBio(req.Bio)));
 | 
				
			||||||
            member.Bio = req.Bio;
 | 
					            member.Bio = req.Bio;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.Links)))
 | 
					        if (req.HasProperty(nameof(req.Links)))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            errors.AddRange(ValidationUtils.ValidateLinks(req.Links));
 | 
					            errors.AddRange(validationService.ValidateLinks(req.Links));
 | 
				
			||||||
            member.Links = req.Links ?? [];
 | 
					            member.Links = req.Links ?? [];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -228,7 +228,7 @@ public class MembersController(
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.Avatar)))
 | 
					        if (req.HasProperty(nameof(req.Avatar)))
 | 
				
			||||||
            errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar)));
 | 
					            errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ValidationUtils.Validate(errors);
 | 
					        ValidationUtils.Validate(errors);
 | 
				
			||||||
        // This is fired off regardless of whether the transaction is committed
 | 
					        // This is fired off regardless of whether the transaction is committed
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
namespace Foxnouns.Backend.Controllers;
 | 
					namespace Foxnouns.Backend.Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Route("/api/v2/meta")]
 | 
					[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";
 | 
					    private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,8 +40,8 @@ public partial class MetaController : ApiControllerBase
 | 
				
			||||||
                    (int)FoxnounsMetrics.UsersActiveDayCount.Value
 | 
					                    (int)FoxnounsMetrics.UsersActiveDayCount.Value
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                new LimitsResponse(
 | 
					                new LimitsResponse(
 | 
				
			||||||
                    MembersController.MaxMemberCount,
 | 
					                    config.Limits.MaxMemberCount,
 | 
				
			||||||
                    ValidationUtils.MaxBioLength,
 | 
					                    config.Limits.MaxBioLength,
 | 
				
			||||||
                    ValidationUtils.MaxCustomPreferences,
 | 
					                    ValidationUtils.MaxCustomPreferences,
 | 
				
			||||||
                    AuthUtils.MaxAuthMethodsPerType,
 | 
					                    AuthUtils.MaxAuthMethodsPerType,
 | 
				
			||||||
                    FlagsController.MaxFlagCount
 | 
					                    FlagsController.MaxFlagCount
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,8 @@ public class UsersController(
 | 
				
			||||||
    UserRendererService userRenderer,
 | 
					    UserRendererService userRenderer,
 | 
				
			||||||
    ISnowflakeGenerator snowflakeGenerator,
 | 
					    ISnowflakeGenerator snowflakeGenerator,
 | 
				
			||||||
    IQueue queue,
 | 
					    IQueue queue,
 | 
				
			||||||
    IClock clock
 | 
					    IClock clock,
 | 
				
			||||||
 | 
					    ValidationService validationService
 | 
				
			||||||
) : ApiControllerBase
 | 
					) : ApiControllerBase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly ILogger _logger = logger.ForContext<UsersController>();
 | 
					    private readonly ILogger _logger = logger.ForContext<UsersController>();
 | 
				
			||||||
| 
						 | 
					@ -65,25 +66,25 @@ public class UsersController(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.Username != null && req.Username != user.Username)
 | 
					        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;
 | 
					            user.Username = req.Username;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.DisplayName)))
 | 
					        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;
 | 
					            user.DisplayName = req.DisplayName;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.Bio)))
 | 
					        if (req.HasProperty(nameof(req.Bio)))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio)));
 | 
					            errors.Add(("bio", validationService.ValidateBio(req.Bio)));
 | 
				
			||||||
            user.Bio = req.Bio;
 | 
					            user.Bio = req.Bio;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.Links)))
 | 
					        if (req.HasProperty(nameof(req.Links)))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            errors.AddRange(ValidationUtils.ValidateLinks(req.Links));
 | 
					            errors.AddRange(validationService.ValidateLinks(req.Links));
 | 
				
			||||||
            user.Links = req.Links ?? [];
 | 
					            user.Links = req.Links ?? [];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,7 +124,7 @@ public class UsersController(
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (req.HasProperty(nameof(req.Avatar)))
 | 
					        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)))
 | 
					        if (req.HasProperty(nameof(req.MemberTitle)))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					@ -133,7 +134,9 @@ public class UsersController(
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else
 | 
					            else
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                errors.Add(("member_title", ValidationUtils.ValidateDisplayName(req.MemberTitle)));
 | 
					                errors.Add(
 | 
				
			||||||
 | 
					                    ("member_title", validationService.ValidateDisplayName(req.MemberTitle))
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
                user.MemberTitle = req.MemberTitle;
 | 
					                user.MemberTitle = req.MemberTitle;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -122,6 +122,7 @@ public static class WebApplicationExtensions
 | 
				
			||||||
                    .AddScoped<FediverseAuthService>()
 | 
					                    .AddScoped<FediverseAuthService>()
 | 
				
			||||||
                    .AddScoped<ObjectStorageService>()
 | 
					                    .AddScoped<ObjectStorageService>()
 | 
				
			||||||
                    .AddTransient<DataCleanupService>()
 | 
					                    .AddTransient<DataCleanupService>()
 | 
				
			||||||
 | 
					                    .AddTransient<ValidationService>()
 | 
				
			||||||
                    // Background services
 | 
					                    // Background services
 | 
				
			||||||
                    .AddHostedService<PeriodicTasksService>()
 | 
					                    .AddHostedService<PeriodicTasksService>()
 | 
				
			||||||
                    // Transient jobs
 | 
					                    // Transient jobs
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,8 @@ public class AuthService(
 | 
				
			||||||
    ILogger logger,
 | 
					    ILogger logger,
 | 
				
			||||||
    DatabaseContext db,
 | 
					    DatabaseContext db,
 | 
				
			||||||
    ISnowflakeGenerator snowflakeGenerator,
 | 
					    ISnowflakeGenerator snowflakeGenerator,
 | 
				
			||||||
    UserRendererService userRenderer
 | 
					    UserRendererService userRenderer,
 | 
				
			||||||
 | 
					    ValidationService validationService
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly ILogger _logger = logger.ForContext<AuthService>();
 | 
					    private readonly ILogger _logger = logger.ForContext<AuthService>();
 | 
				
			||||||
| 
						 | 
					@ -49,7 +50,7 @@ public class AuthService(
 | 
				
			||||||
        // Validate username and whether it's not taken
 | 
					        // Validate username and whether it's not taken
 | 
				
			||||||
        ValidationUtils.Validate(
 | 
					        ValidationUtils.Validate(
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
                ("username", ValidationUtils.ValidateUsername(username)),
 | 
					                ("username", validationService.ValidateUsername(username)),
 | 
				
			||||||
                ("password", ValidationUtils.ValidatePassword(password)),
 | 
					                ("password", ValidationUtils.ValidatePassword(password)),
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
| 
						 | 
					@ -97,7 +98,7 @@ public class AuthService(
 | 
				
			||||||
        AssertValidAuthType(authType, instance);
 | 
					        AssertValidAuthType(authType, instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Validate username and whether it's not taken
 | 
					        // 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))
 | 
					        if (await db.Users.AnyAsync(u => u.Username == username, ct))
 | 
				
			||||||
            throw new ApiError.BadRequest("Username is already taken", "username", username);
 | 
					            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('=');
 | 
					        Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('=');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static string RandomToken(int bytes = 48) =>
 | 
					    public static string RandomToken(int bytes = 48) =>
 | 
				
			||||||
        RandomUrlUnsafeToken()
 | 
					        RandomUrlUnsafeToken(bytes)
 | 
				
			||||||
            // Make the token URL-safe
 | 
					            // Make the token URL-safe
 | 
				
			||||||
            .Replace('+', '-')
 | 
					            .Replace('+', '-')
 | 
				
			||||||
            .Replace('/', '_');
 | 
					            .Replace('/', '_');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,190 +12,10 @@
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// You should have received a copy of the GNU Affero General Public License
 | 
					// 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/>.
 | 
					// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
using System.Text.RegularExpressions;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Foxnouns.Backend.Utils;
 | 
					namespace Foxnouns.Backend.Utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public static partial class ValidationUtils
 | 
					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 const int MaximumReportContextLength = 512;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static ValidationError? ValidateReportContext(string? context) =>
 | 
					    public static ValidationError? ValidateReportContext(string? context) =>
 | 
				
			||||||
| 
						 | 
					@ -223,14 +43,4 @@ public static partial class ValidationUtils
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            _ => null,
 | 
					            _ => 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>
 | 
					SecretKey = <s3SecretKey>
 | 
				
			||||||
Bucket = pronounscc
 | 
					Bucket = pronounscc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Limits]
 | 
				
			||||||
 | 
					MaxMemberCount = 5000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[EmailAuth]
 | 
					[EmailAuth]
 | 
				
			||||||
; The address that emails will be sent from. If not set, email auth is disabled.
 | 
					; The address that emails will be sent from. If not set, email auth is disabled.
 | 
				
			||||||
From = noreply@accounts.pronouns.cc
 | 
					From = noreply@accounts.pronouns.cc
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue