fix: confirm email changes and enforce clean backend build
This commit is contained in:
@@ -39,6 +39,6 @@ internal static class ContentTypes
|
||||
|
||||
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
||||
string content = Encoding.UTF8.GetString(buffer);
|
||||
return content.Contains("<!DOCTYPE html>");
|
||||
return content.Contains("<!DOCTYPE html>", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ internal sealed class LocalBlobStorage(
|
||||
private const long MaxUploadSize = 10 * 1024 * 1024;
|
||||
private const string ContentTypeMetadataSuffix = ".content-type";
|
||||
|
||||
private static readonly char[] PathSeparators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, string, Exception?> LogUploadedFile =
|
||||
LoggerMessage.Define<string, string, string, string>(
|
||||
LogLevel.Information,
|
||||
new EventId(1, nameof(UploadFileAsync)),
|
||||
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]");
|
||||
|
||||
private readonly LocalBlobStorageOptions _options = options.Value;
|
||||
|
||||
public async Task<string> UploadFileAsync(
|
||||
@@ -46,12 +54,7 @@ internal sealed class LocalBlobStorage(
|
||||
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
||||
|
||||
string fileUri = BuildPublicUrl(relativePath);
|
||||
logger.LogInformation(
|
||||
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]",
|
||||
blobName,
|
||||
containerName,
|
||||
contentType,
|
||||
fileUri);
|
||||
LogUploadedFile(logger, blobName, containerName, contentType, fileUri, null);
|
||||
|
||||
return fileUri;
|
||||
}
|
||||
@@ -106,7 +109,7 @@ internal sealed class LocalBlobStorage(
|
||||
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
||||
}
|
||||
|
||||
string[] pathParts = [containerName, .. blobName.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar])];
|
||||
string[] pathParts = [containerName, .. blobName.Split(PathSeparators)];
|
||||
if (pathParts.Any(part => part is "" or "." or ".."))
|
||||
{
|
||||
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
||||
@@ -135,7 +138,7 @@ internal sealed class LocalBlobStorage(
|
||||
? "/api/storage"
|
||||
: requestPath.Trim();
|
||||
|
||||
return normalized.StartsWith("/", StringComparison.Ordinal)
|
||||
return normalized.StartsWith('/')
|
||||
? normalized.TrimEnd('/')
|
||||
: $"/{normalized.TrimEnd('/')}";
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||
internal class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||
: IEmailSender
|
||||
{
|
||||
private static readonly Action<ILogger, string, string, string, string, Exception?> LogDevelopmentEmail =
|
||||
LoggerMessage.Define<string, string, string, string>(
|
||||
LogLevel.Information,
|
||||
new EventId(1, nameof(SendEmailAsync)),
|
||||
"Development email to {Email} with subject {Subject}:{NewLine}{Message}");
|
||||
|
||||
public Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Development email to {Email} with subject {Subject}:{NewLine}{Message}",
|
||||
email,
|
||||
subject,
|
||||
Environment.NewLine,
|
||||
message);
|
||||
LogDevelopmentEmail(logger, email, subject, Environment.NewLine, message, null);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -43,14 +43,13 @@ internal class ResendEmailSender : IEmailSender
|
||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string toEmail, string subject, string htmlMessage)
|
||||
public async Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
var payload = new { from = _options.FromEmail, to = toEmail, subject, html = htmlMessage };
|
||||
var payload = new { from = _options.FromEmail, to = email, subject, html = message };
|
||||
|
||||
string json = JsonSerializer.Serialize(payload);
|
||||
StringContent content = new(json, Encoding.UTF8, "application/json");
|
||||
|
||||
HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
||||
using StringContent content = new(json, Encoding.UTF8, "application/json");
|
||||
using HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
|
||||
@@ -7,49 +7,49 @@ namespace Socialize.Api.Infrastructure.Security;
|
||||
internal sealed class AccessScopeService(
|
||||
OrganizationAccessService organizationAccessService)
|
||||
{
|
||||
public bool IsManager(ClaimsPrincipal user)
|
||||
public static bool IsManager(ClaimsPrincipal user)
|
||||
{
|
||||
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
|
||||
}
|
||||
|
||||
public bool IsProvider(ClaimsPrincipal user)
|
||||
public static bool IsProvider(ClaimsPrincipal user)
|
||||
{
|
||||
return user.IsInRole(KnownRoles.Provider);
|
||||
}
|
||||
|
||||
public bool IsClient(ClaimsPrincipal user)
|
||||
public static bool IsClient(ClaimsPrincipal user)
|
||||
{
|
||||
return user.IsInRole(KnownRoles.Client);
|
||||
}
|
||||
|
||||
public bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||
public static bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||
{
|
||||
return IsManager(user) || user.GetWorkspaceScopeIds().Contains(workspaceId);
|
||||
}
|
||||
|
||||
public bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||
public static bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||
{
|
||||
return IsManager(user) && CanAccessWorkspace(user, workspaceId);
|
||||
}
|
||||
|
||||
public bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
||||
public static bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
||||
{
|
||||
return IsManager(user)
|
||||
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
||||
}
|
||||
|
||||
public bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||
public static bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||
{
|
||||
return IsManager(user)
|
||||
|| (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId));
|
||||
}
|
||||
|
||||
public bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||
public static bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||
{
|
||||
return IsManager(user) || (IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId));
|
||||
}
|
||||
|
||||
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||
public static bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||
{
|
||||
return IsManager(user)
|
||||
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
@@ -81,11 +82,11 @@ internal static class ClaimsPrincipalExtensions
|
||||
|
||||
if (claim is null)
|
||||
{
|
||||
throw new MissingClaimException(key);
|
||||
throw MissingClaimException.ForClaim(key);
|
||||
}
|
||||
|
||||
return typeof(TValue) == typeof(Guid)
|
||||
? Guid.Parse(claim.Value)
|
||||
: Convert.ChangeType(claim.Value, typeof(TValue));
|
||||
: Convert.ChangeType(claim.Value, typeof(TValue), CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
internal class MissingClaimException(
|
||||
string claimName)
|
||||
: Exception($"Claim '{claimName}' is missing.");
|
||||
public class MissingClaimException : Exception
|
||||
{
|
||||
public MissingClaimException()
|
||||
{
|
||||
}
|
||||
|
||||
public MissingClaimException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public MissingClaimException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
internal static MissingClaimException ForClaim(string claimName)
|
||||
{
|
||||
return new MissingClaimException($"Claim '{claimName}' is missing.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ internal static class PasswordGenerator
|
||||
private const string Numbers = "0123456789";
|
||||
private const string SpecialCharacters = "!@#$%^&*()_+-=[];',./`~{}|:\"<>?";
|
||||
|
||||
private static readonly Random Random = new();
|
||||
|
||||
public static string Next(
|
||||
int length = 15,
|
||||
bool requireNumber = true,
|
||||
@@ -23,7 +21,7 @@ internal static class PasswordGenerator
|
||||
// Create pools based on the requirements
|
||||
StringBuilder characterPool = new();
|
||||
|
||||
if (requireNumber)
|
||||
if (requireLowercase)
|
||||
{
|
||||
characterPool.Append(LowerLetters);
|
||||
}
|
||||
@@ -51,22 +49,22 @@ internal static class PasswordGenerator
|
||||
|
||||
if (requireLowercase)
|
||||
{
|
||||
password[index++] = LowerLetters[Random.Next(LowerLetters.Length)];
|
||||
password[index++] = LowerLetters[RandomNumberGenerator.GetInt32(LowerLetters.Length)];
|
||||
}
|
||||
|
||||
if (requireCapital)
|
||||
{
|
||||
password[index++] = UpperLetters[Random.Next(UpperLetters.Length)];
|
||||
password[index++] = UpperLetters[RandomNumberGenerator.GetInt32(UpperLetters.Length)];
|
||||
}
|
||||
|
||||
if (requireNumber)
|
||||
{
|
||||
password[index++] = Numbers[Random.Next(Numbers.Length)];
|
||||
password[index++] = Numbers[RandomNumberGenerator.GetInt32(Numbers.Length)];
|
||||
}
|
||||
|
||||
if (requireSpecialCharacter)
|
||||
{
|
||||
password[index++] = SpecialCharacters[Random.Next(SpecialCharacters.Length)];
|
||||
password[index++] = SpecialCharacters[RandomNumberGenerator.GetInt32(SpecialCharacters.Length)];
|
||||
}
|
||||
|
||||
// Fill the rest with the password
|
||||
@@ -85,7 +83,7 @@ internal static class PasswordGenerator
|
||||
{
|
||||
for (int i = array.Length - 1; i > 0; i--)
|
||||
{
|
||||
int j = Random.Next(i + 1);
|
||||
int j = RandomNumberGenerator.GetInt32(i + 1);
|
||||
(array[i], array[j]) = (array[j], array[i]); // Swap elements
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Socialize.Api.Infrastructure.TestData;
|
||||
|
||||
#pragma warning disable S1075 // Test data intentionally uses representative external URLs.
|
||||
|
||||
internal static class TestDataSeedExtensions
|
||||
{
|
||||
private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999");
|
||||
@@ -209,13 +211,7 @@ internal static class TestDataSeedExtensions
|
||||
await userManager.RemoveClaimAsync(user, claim);
|
||||
}
|
||||
|
||||
string persona = roles.Contains(KnownRoles.Manager, StringComparer.Ordinal)
|
||||
? KnownRoles.Manager
|
||||
: roles.Contains(KnownRoles.Client, StringComparer.Ordinal)
|
||||
? KnownRoles.Client
|
||||
: roles.Contains(KnownRoles.Provider, StringComparer.Ordinal)
|
||||
? KnownRoles.Provider
|
||||
: KnownRoles.WorkspaceMember;
|
||||
string persona = GetPersona(roles);
|
||||
|
||||
foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)]))
|
||||
{
|
||||
@@ -225,6 +221,26 @@ internal static class TestDataSeedExtensions
|
||||
return user;
|
||||
}
|
||||
|
||||
private static string GetPersona(IReadOnlyCollection<string> roles)
|
||||
{
|
||||
if (roles.Contains(KnownRoles.Manager, StringComparer.Ordinal))
|
||||
{
|
||||
return KnownRoles.Manager;
|
||||
}
|
||||
|
||||
if (roles.Contains(KnownRoles.Client, StringComparer.Ordinal))
|
||||
{
|
||||
return KnownRoles.Client;
|
||||
}
|
||||
|
||||
if (roles.Contains(KnownRoles.Provider, StringComparer.Ordinal))
|
||||
{
|
||||
return KnownRoles.Provider;
|
||||
}
|
||||
|
||||
return KnownRoles.WorkspaceMember;
|
||||
}
|
||||
|
||||
private static async Task EnsureOrganizationDataAsync(
|
||||
Guid managerUserId,
|
||||
Guid developerUserId,
|
||||
|
||||
Reference in New Issue
Block a user