Add validation to RealmCreate

This commit is contained in:
eelke 2026-02-08 18:00:24 +01:00
parent 09480eb1e4
commit ddbb1f42d7
16 changed files with 326 additions and 23 deletions

View file

@ -1,3 +1,3 @@
namespace IdentityShroud.Core.Messages.Realm;
public record RealmCreateRequest(Guid? Id, string Slug, string Description);
public record RealmCreateRequest(Guid? Id, string? Slug, string Name);

View file

@ -0,0 +1,85 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.WebUtilities;
namespace IdentityShroud.Core.Helpers;
public static class SlugHelper
{
public static string GenerateSlug(string text, int maxLength = 40)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
// Normalize to decomposed form (separates accents from letters)
string normalized = text.Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder(normalized.Length);
bool lastWasHyphen = false;
foreach (char c in normalized)
{
// Skip diacritics (accents)
if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark)
continue;
char lower = char.ToLowerInvariant(c);
// Convert valid characters
if ((lower >= 'a' && lower <= 'z') || (lower >= '0' && lower <= '9'))
{
sb.Append(lower);
lastWasHyphen = false;
}
// Convert spaces, underscores, and other separators to hyphen
else if (char.IsWhiteSpace(c) || c == '_' || c == '-')
{
if (!lastWasHyphen && sb.Length > 0)
{
sb.Append('-');
lastWasHyphen = true;
}
}
// Skip all other characters
}
// Trim trailing hyphen if any
if (sb.Length > 0 && sb[sb.Length - 1] == '-')
sb.Length--;
string slug = sb.ToString();
// Handle truncation with hash suffix for long strings
if (slug.Length > maxLength)
{
// Generate hash of original text
string hashSuffix = GenerateHashSuffix(text);
int contentLength = maxLength - hashSuffix.Length;
// Truncate at word boundary if possible
int cutPoint = contentLength;
int lastHyphen = slug.LastIndexOf('-', contentLength - 1);
if (lastHyphen > contentLength / 2)
cutPoint = lastHyphen;
slug = slug.Substring(0, cutPoint).TrimEnd('-') + hashSuffix;
}
return slug;
}
private static string GenerateHashSuffix(string text)
{
using (var sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
// Take first 4 bytes (will become ~5-6 base64url chars)
string base64Url = WebEncoders.Base64UrlEncode(hash, 0, 4);
return "-" + base64Url;
}
}
}

View file

@ -0,0 +1,8 @@
using IdentityShroud.Core.Messages.Realm;
namespace IdentityShroud.Core.Services;
public interface IRealmService
{
Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default);
}

View file

@ -1,23 +1,24 @@
using System.Security.Cryptography;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Helpers;
using IdentityShroud.Core.Messages.Realm;
using IdentityShroud.Core.Model;
namespace IdentityShroud.Core.Services;
public record RealmCreateResponse(Realm Realm);
public record RealmCreateResponse(Guid Id, string Slug, string Name);
public class RealmService(
Db db,
IEncryptionService encryptionService)
IEncryptionService encryptionService) : IRealmService
{
public async Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default)
{
Realm realm = new()
{
Id = request.Id ?? Guid.CreateVersion7(),
Slug = request.Slug,
Name = request.Description,
Slug = request.Slug ?? SlugHelper.GenerateSlug(request.Name),
Name = request.Name,
};
using RSA rsa = RSA.Create(2048);
@ -26,6 +27,7 @@ public class RealmService(
db.Add(realm);
await db.SaveChangesAsync(ct);
return new RealmCreateResponse(realm);
return new RealmCreateResponse(
realm.Id, realm.Slug, realm.Name);
}
}