Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/fluid and email forms #48

Merged
merged 2 commits into from
Nov 16, 2024
Merged

Feature/fluid and email forms #48

merged 2 commits into from
Nov 16, 2024

Conversation

0xF6
Copy link
Member

@0xF6 0xF6 commented Nov 16, 2024

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a template engine for managing email forms, enhancing the application's ability to handle dynamic content.
    • Added new HTML templates for session details and password reset functionality, improving user experience during login and password recovery.
  • Bug Fixes

    • Updated package dependencies, ensuring improved stability and security.
  • Style

    • Enhanced the visual design of existing HTML templates with a dark theme for better readability.
  • Chores

    • Refactored service configuration to replace deprecated email form handling with the new template engine.

Copy link

coderabbitai bot commented Nov 16, 2024

Walkthrough

The pull request introduces multiple changes primarily focused on package management and feature updates within the Argon API. Key modifications include the addition of a new package reference for Fluid.Core, updates to existing package versions, and the removal and introduction of classes and methods related to email form handling. A new HTML file for session details is added, while existing HTML files are modified for a dark theme. Additionally, the Orleans client configuration is updated to retrieve cluster settings dynamically.

Changes

File Path Change Summary
src/Argon.Api/Argon.Api.csproj - Added package reference: Fluid.Core version 2.12.0
- Updated ActualLab.Fusion to 9.5.64
- Updated System.IdentityModel.Tokens.Jwt to 8.2.1
src/Argon.Api/Features/EmailForms/EMailFormStorage.cs - Removed class EMailFormStorage and methods related to email forms handling.
src/Argon.Api/Features/Template/TemplateFeature.cs - Added TemplateFeature class with methods for template engine and email form management.
src/Argon.Api/Grains/EmailManager.cs - Updated method from CompileAndGetForm to Render in SendOtpCodeAsync.
src/Argon.Api/Program.cs - Removed AddEMailForms() and added AddTemplateEngine().
src/Argon.Api/Resources/new_session.html - Added new HTML file for session details after login.
src/Argon.Api/Resources/otp.html - Modified styles for dark theme.
src/Argon.Api/Resources/reset_pass.html - Added new HTML file for password reset email template.
src/Argon.Entry/Program.cs - Updated Orleans client configuration to load cluster options from app settings.

Possibly related PRs

  • Users #7: Updates to package references in Argon.Api.csproj, relevant to the overall project structure.
  • Feature/fusion auth #13: Modifications to Argon.Api.csproj, adding new package references and properties.
  • Fixes #16: Changes in the UserManager class that may relate to user management functionality impacted by dependency updates.
  • Redis storage provider #32: Addition of new package references in Argon.Api.csproj, suggesting a connection to project configuration.
  • Cut client\server\grains\fusion #39: Introduction of a new project in the solution file, relating to overall project restructuring.

Suggested reviewers

  • urumo

🐰 In the land of code where changes bloom,
New templates and features dispel the gloom.
With packages updated and forms now anew,
The Argon API hops forward, it's true!
So raise your paws high, let’s celebrate bright,
For every new line brings a future so light! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@0xF6 0xF6 requested a review from urumo November 16, 2024 03:59
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (5)
src/Argon.Api/Grains/EmailManager.cs (2)

Line range hint 29-36: Add error handling for template rendering

The template rendering lacks proper error handling and validation. Consider these improvements:

  1. Add try-catch block for template rendering
  2. Validate template existence
  3. Consider moving the template name to a constant or configuration

Here's a suggested improvement:

+    private const string OTP_TEMPLATE = "otp";
+
     public async Task SendOtpCodeAsync(string email, string otpCode, TimeSpan validity)
     {
-        var form = formStorage.Render("otp", new Dictionary<string, string>
-        {
-            {
-                "otp", otpCode
-            },
-            {
-                "validity", $"{(int)Math.Floor(validity.TotalMinutes):D}"
-            }
-        });
+        string form;
+        try
+        {
+            form = await formStorage.Render(OTP_TEMPLATE, new Dictionary<string, string>
+            {
+                { "otp", otpCode },
+                { "validity", $"{(int)Math.Floor(validity.TotalMinutes):D}" }
+            });
+        }
+        catch (Exception ex)
+        {
+            logger.LogError(ex, "Failed to render OTP template");
+            throw new InvalidOperationException("Failed to generate email content", ex);
+        }

Line range hint 11-42: Consider migrating from SmtpClient to MailKit

SmtpClient is deprecated by Microsoft. Consider migrating to MailKit which offers:

  • Better async support
  • More secure defaults
  • Active maintenance
  • Built-in retry policies

Would you like me to provide a code example for the migration to MailKit?

src/Argon.Api/Features/Template/TemplateFeature.cs (1)

8-14: Consider adding configuration options for better flexibility

The current implementation uses hardcoded service registrations. Consider adding configuration options to allow customization of:

  • Template file location
  • File patterns
  • Parser options

Here's a suggested implementation:

+public class TemplateOptions
+{
+    public string TemplateDirectory { get; set; } = "./Resources";
+    public string FilePattern { get; set; } = "*.html";
+    public Action<FluidParser>? ConfigureParser { get; set; }
+}

 public static class TemplateFeature
 {
-    public static IServiceCollection AddTemplateEngine(this WebApplicationBuilder builder)
+    public static IServiceCollection AddTemplateEngine(
+        this WebApplicationBuilder builder,
+        Action<TemplateOptions>? configure = null)
     {
-        builder.Services.AddSingleton<FluidParser>();
+        var options = new TemplateOptions();
+        configure?.Invoke(options);
+        
+        builder.Services.AddSingleton(options);
+        builder.Services.AddSingleton(sp => {
+            var parser = new FluidParser();
+            options.ConfigureParser?.Invoke(parser);
+            return parser;
+        });
         builder.Services.AddHostedService<EMailFormLoader>();
         builder.Services.AddSingleton<EMailFormStorage>();
         return builder.Services;
     }
 }
src/Argon.Api/Resources/otp.html (2)

19-19: Consider enhancing visual separation in dark mode.

While the current styling works, the container could benefit from better visual separation from the background.

Consider this enhancement:

 .container {
     max-width: 600px;
     margin: 0 auto;
     background-color: #2a2a2a;
     padding: 20px;
     border-radius: 8px;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
+    box-shadow: 0 4px 12px rgba(187, 134, 252, 0.15);
+    border: 1px solid rgba(187, 134, 252, 0.1);
 }

Also applies to: 22-22


40-50: Consider adding interactive states for better UX.

While the styling is consistent, adding hover and active states would improve user experience when the OTP code is interactive.

Consider adding these states:

 .otp-code {
     display: inline-block;
     font-size: 28px;
     font-weight: bold;
     padding: 10px 20px;
     color: #ffffff;
     background-color: #6200ea;
     border-radius: 4px;
     text-decoration: none;
     margin-bottom: 20px;
+    transition: background-color 0.2s ease;
+}
+.otp-code:hover {
+    background-color: #7722ff;
+}
+.otp-code:active {
+    background-color: #5000d0;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 8916011 and 33fd695.

📒 Files selected for processing (9)
  • src/Argon.Api/Argon.Api.csproj (3 hunks)
  • src/Argon.Api/Features/EmailForms/EMailFormStorage.cs (0 hunks)
  • src/Argon.Api/Features/Template/TemplateFeature.cs (1 hunks)
  • src/Argon.Api/Grains/EmailManager.cs (2 hunks)
  • src/Argon.Api/Program.cs (2 hunks)
  • src/Argon.Api/Resources/new_session.html (1 hunks)
  • src/Argon.Api/Resources/otp.html (1 hunks)
  • src/Argon.Api/Resources/reset_pass.html (1 hunks)
  • src/Argon.Entry/Program.cs (1 hunks)
💤 Files with no reviewable changes (1)
  • src/Argon.Api/Features/EmailForms/EMailFormStorage.cs
✅ Files skipped from review due to trivial changes (2)
  • src/Argon.Api/Resources/new_session.html
  • src/Argon.Api/Resources/reset_pass.html
🔇 Additional comments (11)
src/Argon.Api/Grains/EmailManager.cs (1)

5-5: Verify complete migration from EmailForms to Template feature

The import change reflects the architectural shift from EmailForms to Template feature.

Let's verify if there are any remaining references to the old EmailForms feature:

✅ Verification successful

Migration from EmailForms to Template feature appears correct

The references to "emailform" found in the codebase are part of the new Template feature implementation (EMailFormStorage and EMailFormLoader in TemplateFeature.cs), indicating a proper migration where the old functionality has been encapsulated within the new Template feature. The import change in EmailManager.cs is consistent with this architectural change.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining references to EmailForms feature
# that might have been missed during migration

# Search for any remaining references to EmailForms
rg -l "EmailForms" --type-add 'csharp:*.{cs,csproj}' --type csharp

# Search for any remaining AddEMailForms method calls
ast-grep --pattern 'AddEMailForms($$$)'

Length of output: 438


Script:

#!/bin/bash
# Let's do a more thorough search for any remaining references to EmailForms
# including partial matches and case variations to ensure complete migration

# Search for any variations of EmailForms in C# files
rg -i "emailform" --type-add 'csharp:*.{cs,csproj}' --type csharp -l

# Search for Template feature usage to verify proper migration
rg -i "Features\.Template" --type-add 'csharp:*.{cs,csproj}' --type csharp -l

# Look for any form-related classes or interfaces that might need updates
ast-grep --pattern 'class $_ : $$$Form$$$'
ast-grep --pattern 'interface I$_Form'

Length of output: 2963


Script:

#!/bin/bash
# Let's examine the specific changes in the files that still contain emailform references
# to ensure they are just part of the migration and not missed updates

# Check the content of EmailManager.cs for any old EmailForms usage
rg "emailform" -i src/Argon.Api/Grains/EmailManager.cs -C 2

# Check TemplateFeature.cs to understand the migration context
rg "emailform" -i src/Argon.Api/Features/Template/TemplateFeature.cs -C 2

# Look for any template-related method signatures that replaced email forms
ast-grep --pattern 'Render($$$)'
ast-grep --pattern 'CompileAndGetForm($$$)'

Length of output: 1651

src/Argon.Entry/Program.cs (1)

42-42: LGTM! Verify Orleans configuration section exists.

The change from hardcoded cluster configuration to dynamic configuration is a good practice. However, ensure that the "Orleans" configuration section is properly defined in your configuration files.

Let's verify the Orleans configuration section exists:

✅ Verification successful

Orleans configuration section exists and is properly configured

The "Orleans" configuration section is present in both src/Argon.Entry/appsettings.json and src/Argon.Api/appsettings.json with the following configuration:

  • ClusterId: "argonapi"
  • ServiceId: "argonapi"
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for Orleans configuration in appsettings files
# Expected: Find configuration files containing Orleans section

# Search for appsettings files
fd appsettings.json$ --exec sh -c 'echo "=== {} ===" && cat {} | jq -r "select(.Orleans != null) | .Orleans"'

Length of output: 350

src/Argon.Api/Features/Template/TemplateFeature.cs (1)

1-5: LGTM! Clean and minimal imports

src/Argon.Api/Resources/otp.html (3)

10-10: LGTM! Color contrast meets accessibility standards.

The background-to-text color contrast ratio of 14.23:1 exceeds WCAG AAA requirements (minimum 7:1), ensuring excellent readability.

Also applies to: 13-13


30-30: LGTM! Consistent with Material Design dark theme.

The purple accent color (#bb86fc) aligns with Material Design dark theme guidelines.


35-39: LGTM! Text remains highly readable.

The message text color provides excellent contrast (9.57:1 ratio) against the container background.

src/Argon.Api/Program.cs (2)

14-14: LGTM! Namespace follows project conventions.

The addition of the Template feature namespace aligns with the architectural changes and follows the project's naming conventions.


52-52: Verify template engine integration with existing email functionality.

The shift from AddEMailForms() to AddTemplateEngine() represents a significant architectural change. While the implementation looks correct, we should verify that all email form consumers have been updated to use the new template engine.

✅ Verification successful

Template engine integration is properly implemented and functional

The verification shows that:

  1. The new template engine (TemplateFeature) is a complete replacement that includes the email form functionality
  2. EmailManager is properly integrated with the template system through EMailFormStorage
  3. The system loads HTML templates from the Resources directory and manages them through a concurrent dictionary
  4. Email sending functionality is properly wired up with template rendering
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that all email form consumers are updated to use the new template engine

# Search for any remaining references to old email forms
echo "Checking for old email form references..."
rg -l "EMailForm" --type cs

# Search for template engine usage to ensure proper adoption
echo "Checking template engine usage..."
rg -l "Template(Engine|Feature)" --type cs

# Look for any email-related HTML templates
echo "Checking email templates..."
fd -e html . | grep -i "email\|mail\|template"

Length of output: 520


Script:

#!/bin/bash
# Let's examine the actual implementation and usage patterns

# Check the content of EmailManager.cs to see how email forms are used
echo "Checking EmailManager.cs implementation..."
rg -A 5 -B 5 "EMailForm" src/Argon.Api/Grains/EmailManager.cs

# Check TemplateFeature.cs to understand the relationship between old and new systems
echo "Checking TemplateFeature.cs implementation..."
cat src/Argon.Api/Features/Template/TemplateFeature.cs

# Look for actual email sending patterns
echo "Checking email sending patterns..."
rg -A 3 "SendEmail|SendMail" --type cs

Length of output: 3735

src/Argon.Api/Argon.Api.csproj (3)

14-14: Verify template engine implementation

The addition of Fluid.Core aligns with the PR objectives for fluid forms implementation.

#!/bin/bash
# Description: Verify the usage of Fluid.Core template engine
# Expected: Find template-related implementations using Fluid

# Search for Fluid template usage
ast-grep --pattern 'using Fluid;'

# Look for template-related files
fd -e cs -e liquid -e html template

27-27: Verify ActualLab.Fusion version compatibility

The update from 9.5.59 to 9.5.64 is a minor version bump. Please ensure there are no breaking changes.

#!/bin/bash
# Description: Check for potential breaking changes in ActualLab.Fusion
# Expected: Find usage patterns that might be affected by the update

# Search for ActualLab.Fusion usage patterns
rg -l "using ActualLab\.Fusion" 

# Look for common breaking patterns in Fusion apps
ast-grep --pattern 'IComputed<$_>'
ast-grep --pattern '[ComputeMethod]'
ast-grep --pattern 'IComputeService'

48-48: Verify security implications of JWT package update

The JWT package update (8.2.0 → 8.2.1) is a patch version bump. Please verify security implications.

✅ Verification successful

JWT package update is safe to proceed

The version update from 8.2.0 to 8.2.1 is a patch version that does not fall within any vulnerable version ranges identified in the security advisories. The latest security advisories for this package are from January 2024 and affect versions below 7.1.2, which is well before our current version.

The git history shows a stable progression of JWT package versions, with proper dependency management. The update follows semantic versioning principles and represents a safe, backward-compatible patch.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for security advisories and JWT usage patterns
# Expected: Find JWT configurations and usage that might need review

# Check for security advisories
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NUGET, package: "System.IdentityModel.Tokens.Jwt") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

# Look for JWT usage patterns
ast-grep --pattern 'JwtSecurityToken'
ast-grep --pattern 'JwtRegisteredClaimNames'

Length of output: 1698


Script:

#!/bin/bash
# Let's check the specific version changes in the git history and current JWT usage
git log -p --all -S "System.IdentityModel.Tokens.Jwt" -- "**/*.csproj"

# Search for JWT token generation and validation patterns
rg -l "JwtSecurityTokenHandler|TokenValidationParameters|SigningCredentials" --type cs

# Look for JWT configuration in settings
rg -l "JWT|Bearer" --type json

Length of output: 9232

{
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
var formFiles = Directory.EnumerateFiles("./Resources", "*.html").ToList();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Add security measures for template directory access

The code has potential security issues that need to be addressed:

  • The template directory path "./Resources" is hardcoded and accessed without validation
  • No path sanitization or access controls are in place
  • Directory traversal protection is missing

Required changes:

  • Move template directory path to configuration
  • Add path validation to ensure templates are accessed within allowed boundaries
  • Implement sanitization for template file names
  • Consider using Path.GetFullPath() to resolve relative paths securely
🔗 Analysis chain

Verify template directory access security

The file system operations need additional security checks:

  1. Verify template directory is within allowed paths
  2. Check for path traversal in template names

Also applies to: 53-54


Let me gather more information about the template directory security by checking for configuration and validation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential security issues in template handling

# Check if template paths contain parent directory traversal
rg -g '*.cs' '(?i)path\.(?:combine|join).*\.\.' 

# Check for template directory configuration in settings files
fd -e json -e yaml -e xml | xargs rg -i 'template.*(?:directory|path|location)'

# Look for other file system operations that might need security review
ast-grep --pattern 'File.$$$' 

Length of output: 5096


Script:

#!/bin/bash
# Check for template directory configuration and validation

# Look for template directory configuration
rg -g '*.cs' -B 2 -A 2 '"./Resources"'

# Check for any validation or security checks around template access
ast-grep --pattern 'Directory.$$$'

# Check if Resources directory is configured in settings
fd appsettings | xargs rg -i 'resources'

# Look for any path validation or sanitization
rg -g '*.cs' -B 2 -A 2 'Path\.(?:GetFullPath|Combine)'

Length of output: 5174

Comment on lines +17 to +41
public class EMailFormStorage
{
private readonly ConcurrentDictionary<string, IFluidTemplate> htmlForms = new();

public void Load(string name, IFluidTemplate template) => htmlForms.TryAdd(name, template);

public IFluidTemplate GetContentFor(string formKey)
{
if (htmlForms.TryGetValue(formKey, out var form))
return form;
throw new InvalidOperationException($"No '{formKey}' form found");
}

public string Render(string formKey, Dictionary<string, string> values)
{
var template = GetContentFor(formKey);

var context = new TemplateContext();

foreach (var (key, value) in values)
context.SetValue(key, value);

return template.Render(context);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider enhancing error handling and API design

Several improvements could make this class more robust:

  1. The storage has no upper bound which could lead to memory issues
  2. No way to check template existence without exception
  3. Values dictionary is restricted to strings

Suggested improvements:

 public class EMailFormStorage
 {
-    private readonly ConcurrentDictionary<string, IFluidTemplate> htmlForms = new();
+    private readonly ConcurrentDictionary<string, IFluidTemplate> htmlForms;
+    private readonly int maxTemplates;
+
+    public EMailFormStorage(IConfiguration config)
+    {
+        maxTemplates = config.GetValue<int>("Templates:MaxCount", 1000);
+        htmlForms = new(Environment.ProcessorCount, maxTemplates);
+    }

-    public void Load(string name, IFluidTemplate template) => htmlForms.TryAdd(name, template);
+    public bool Load(string name, IFluidTemplate template)
+    {
+        if (htmlForms.Count >= maxTemplates)
+            return false;
+        return htmlForms.TryAdd(name, template);
+    }

+    public bool Exists(string formKey) => htmlForms.ContainsKey(formKey);

     public IFluidTemplate GetContentFor(string formKey)
     {
         if (htmlForms.TryGetValue(formKey, out var form))
             return form;
-        throw new InvalidOperationException($"No '{formKey}' form found");
+        throw new KeyNotFoundException($"Template '{formKey}' not found");
     }

-    public string Render(string formKey, Dictionary<string, string> values)
+    public string Render<T>(string formKey, T values) where T : class
     {
         var template = GetContentFor(formKey);
         var context = new TemplateContext();
-        foreach (var (key, value) in values)
-            context.SetValue(key, value);
+        context.SetValue("Model", values);
         return template.Render(context);
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class EMailFormStorage
{
private readonly ConcurrentDictionary<string, IFluidTemplate> htmlForms = new();
public void Load(string name, IFluidTemplate template) => htmlForms.TryAdd(name, template);
public IFluidTemplate GetContentFor(string formKey)
{
if (htmlForms.TryGetValue(formKey, out var form))
return form;
throw new InvalidOperationException($"No '{formKey}' form found");
}
public string Render(string formKey, Dictionary<string, string> values)
{
var template = GetContentFor(formKey);
var context = new TemplateContext();
foreach (var (key, value) in values)
context.SetValue(key, value);
return template.Render(context);
}
}
public class EMailFormStorage
{
private readonly ConcurrentDictionary<string, IFluidTemplate> htmlForms;
private readonly int maxTemplates;
public EMailFormStorage(IConfiguration config)
{
maxTemplates = config.GetValue<int>("Templates:MaxCount", 1000);
htmlForms = new(Environment.ProcessorCount, maxTemplates);
}
public bool Load(string name, IFluidTemplate template)
{
if (htmlForms.Count >= maxTemplates)
return false;
return htmlForms.TryAdd(name, template);
}
public bool Exists(string formKey) => htmlForms.ContainsKey(formKey);
public IFluidTemplate GetContentFor(string formKey)
{
if (htmlForms.TryGetValue(formKey, out var form))
return form;
throw new KeyNotFoundException($"Template '{formKey}' not found");
}
public string Render<T>(string formKey, T values) where T : class
{
var template = GetContentFor(formKey);
var context = new TemplateContext();
context.SetValue("Model", values);
return template.Render(context);
}
}

Comment on lines +43 to +65
public class EMailFormLoader(EMailFormStorage storage, ILogger<EMailFormLoader> logger, FluidParser engine) : BackgroundService
{
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
var formFiles = Directory.EnumerateFiles("./Resources", "*.html").ToList();

logger.LogInformation("Found '{count}' email forms", formFiles.Count);

foreach (var file in formFiles)
{
var content = await File.ReadAllTextAsync(file, stoppingToken);
var name = Path.GetFileNameWithoutExtension(file);

if (engine.TryParse(content, out var result, out var error))
{
storage.Load(name, result);
logger.LogInformation("Loaded '{name}' email form", name);
}
else
logger.LogError("Failed load '{name}' email form, error: {error}", name, error);
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical improvements needed for robust template loading

Several critical issues need to be addressed:

  1. Hardcoded path could fail in production
  2. No way to reload templates without service restart
  3. Missing error handling for file system operations
  4. No retry mechanism for failed templates

Here's a suggested implementation with improvements:

-public class EMailFormLoader(EMailFormStorage storage, ILogger<EMailFormLoader> logger, FluidParser engine) 
+public class EMailFormLoader(
+    EMailFormStorage storage,
+    ILogger<EMailFormLoader> logger,
+    FluidParser engine,
+    TemplateOptions options)
     : BackgroundService
 {
+    private readonly FileSystemWatcher? watcher;
+    private readonly HashSet<string> failedTemplates = new();
+    private readonly SemaphoreSlim semaphore = new(1, 1);
+
+    public EMailFormLoader()
+    {
+        if (Directory.Exists(options.TemplateDirectory))
+        {
+            watcher = new(options.TemplateDirectory, options.FilePattern);
+            watcher.Changed += OnTemplateChanged;
+            watcher.Created += OnTemplateChanged;
+        }
+    }

     protected async override Task ExecuteAsync(CancellationToken stoppingToken)
     {
-        var formFiles = Directory.EnumerateFiles("./Resources", "*.html").ToList();
+        await LoadTemplatesAsync(stoppingToken);
+        
+        if (watcher != null)
+        {
+            watcher.EnableRaisingEvents = true;
+            
+            // Periodically retry failed templates
+            while (!stoppingToken.IsCancellationRequested)
+            {
+                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
+                await RetryFailedTemplatesAsync(stoppingToken);
+            }
+        }
+    }

-        logger.LogInformation("Found '{count}' email forms", formFiles.Count);
+    private async Task LoadTemplatesAsync(CancellationToken stoppingToken)
+    {
+        try
+        {
+            var formFiles = Directory.EnumerateFiles(
+                options.TemplateDirectory, 
+                options.FilePattern
+            ).ToList();

+            logger.LogInformation(
+                "Found {Count} email forms in {Directory}", 
+                formFiles.Count, 
+                options.TemplateDirectory
+            );

+            await LoadTemplatesFromFilesAsync(formFiles, stoppingToken);
+        }
+        catch (Exception ex) when (ex is DirectoryNotFoundException or UnauthorizedAccessException)
+        {
+            logger.LogError(
+                ex,
+                "Failed to access template directory {Directory}",
+                options.TemplateDirectory
+            );
+        }
+    }

-        foreach (var file in formFiles)
-        {
-            var content = await File.ReadAllTextAsync(file, stoppingToken);
-            var name    = Path.GetFileNameWithoutExtension(file);
+    private async Task LoadTemplatesFromFilesAsync(
+        IEnumerable<string> files,
+        CancellationToken stoppingToken)
+    {
+        foreach (var file in files)
+        {
+            await LoadTemplateAsync(file, stoppingToken);
+        }
+    }

+    private async Task LoadTemplateAsync(
+        string file,
+        CancellationToken stoppingToken)
+    {
+        try
+        {
+            var content = await File.ReadAllTextAsync(file, stoppingToken);
+            var name = Path.GetFileNameWithoutExtension(file);

             if (engine.TryParse(content, out var result, out var error))
             {
-                storage.Load(name, result);
+                if (storage.Load(name, result))
+                {
+                    failedTemplates.Remove(name);
                     logger.LogInformation("Loaded '{name}' email form", name);
+                }
+                else
+                {
+                    logger.LogWarning(
+                        "Failed to load '{Name}' - storage capacity reached",
+                        name
+                    );
+                }
             }
             else
+            {
+                failedTemplates.Add(name);
                 logger.LogError("Failed load '{name}' email form, error: {error}", name, error);
+            }
+        }
+        catch (Exception ex) when (
+            ex is IOException or
+            UnauthorizedAccessException)
+        {
+            logger.LogError(
+                ex,
+                "Failed to read template file {File}",
+                file
+            );
         }
     }

+    private async void OnTemplateChanged(object sender, FileSystemEventArgs e)
+    {
+        try
+        {
+            await semaphore.WaitAsync();
+            await LoadTemplateAsync(e.FullPath, CancellationToken.None);
+        }
+        finally
+        {
+            semaphore.Release();
+        }
+    }

+    private async Task RetryFailedTemplatesAsync(CancellationToken stoppingToken)
+    {
+        if (failedTemplates.Count == 0) return;

+        logger.LogInformation(
+            "Retrying {Count} failed templates",
+            failedTemplates.Count
+        );

+        var templates = failedTemplates.ToList();
+        foreach (var template in templates)
+        {
+            var file = Path.Combine(
+                options.TemplateDirectory,
+                template + ".html"
+            );
+            await LoadTemplateAsync(file, stoppingToken);
+        }
+    }

+    public override void Dispose()
+    {
+        watcher?.Dispose();
+        semaphore.Dispose();
+        base.Dispose();
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class EMailFormLoader(EMailFormStorage storage, ILogger<EMailFormLoader> logger, FluidParser engine) : BackgroundService
{
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
var formFiles = Directory.EnumerateFiles("./Resources", "*.html").ToList();
logger.LogInformation("Found '{count}' email forms", formFiles.Count);
foreach (var file in formFiles)
{
var content = await File.ReadAllTextAsync(file, stoppingToken);
var name = Path.GetFileNameWithoutExtension(file);
if (engine.TryParse(content, out var result, out var error))
{
storage.Load(name, result);
logger.LogInformation("Loaded '{name}' email form", name);
}
else
logger.LogError("Failed load '{name}' email form, error: {error}", name, error);
}
}
}
public class EMailFormLoader(
EMailFormStorage storage,
ILogger<EMailFormLoader> logger,
FluidParser engine,
TemplateOptions options)
: BackgroundService
{
private readonly FileSystemWatcher? watcher;
private readonly HashSet<string> failedTemplates = new();
private readonly SemaphoreSlim semaphore = new(1, 1);
public EMailFormLoader()
{
if (Directory.Exists(options.TemplateDirectory))
{
watcher = new(options.TemplateDirectory, options.FilePattern);
watcher.Changed += OnTemplateChanged;
watcher.Created += OnTemplateChanged;
}
}
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
await LoadTemplatesAsync(stoppingToken);
if (watcher != null)
{
watcher.EnableRaisingEvents = true;
// Periodically retry failed templates
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
await RetryFailedTemplatesAsync(stoppingToken);
}
}
}
private async Task LoadTemplatesAsync(CancellationToken stoppingToken)
{
try
{
var formFiles = Directory.EnumerateFiles(
options.TemplateDirectory,
options.FilePattern
).ToList();
logger.LogInformation(
"Found {Count} email forms in {Directory}",
formFiles.Count,
options.TemplateDirectory
);
await LoadTemplatesFromFilesAsync(formFiles, stoppingToken);
}
catch (Exception ex) when (ex is DirectoryNotFoundException or UnauthorizedAccessException)
{
logger.LogError(
ex,
"Failed to access template directory {Directory}",
options.TemplateDirectory
);
}
}
private async Task LoadTemplatesFromFilesAsync(
IEnumerable<string> files,
CancellationToken stoppingToken)
{
foreach (var file in files)
{
await LoadTemplateAsync(file, stoppingToken);
}
}
private async Task LoadTemplateAsync(
string file,
CancellationToken stoppingToken)
{
try
{
var content = await File.ReadAllTextAsync(file, stoppingToken);
var name = Path.GetFileNameWithoutExtension(file);
if (engine.TryParse(content, out var result, out var error))
{
if (storage.Load(name, result))
{
failedTemplates.Remove(name);
logger.LogInformation("Loaded '{name}' email form", name);
}
else
{
logger.LogWarning(
"Failed to load '{Name}' - storage capacity reached",
name
);
}
}
else
{
failedTemplates.Add(name);
logger.LogError("Failed load '{name}' email form, error: {error}", name, error);
}
}
catch (Exception ex) when (
ex is IOException or
UnauthorizedAccessException)
{
logger.LogError(
ex,
"Failed to read template file {File}",
file
);
}
}
private async void OnTemplateChanged(object sender, FileSystemEventArgs e)
{
try
{
await semaphore.WaitAsync();
await LoadTemplateAsync(e.FullPath, CancellationToken.None);
}
finally
{
semaphore.Release();
}
}
private async Task RetryFailedTemplatesAsync(CancellationToken stoppingToken)
{
if (failedTemplates.Count == 0) return;
logger.LogInformation(
"Retrying {Count} failed templates",
failedTemplates.Count
);
var templates = failedTemplates.ToList();
foreach (var template in templates)
{
var file = Path.Combine(
options.TemplateDirectory,
template + ".html"
);
await LoadTemplateAsync(file, stoppingToken);
}
}
public override void Dispose()
{
watcher?.Dispose();
semaphore.Dispose();
base.Dispose();
}
}

border-radius: 4px;
text-decoration: none;
margin-bottom: 20px;
}

.footer {
font-size: 14px;
color: #777777;
color: #888888;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix footer text contrast for accessibility compliance.

The current footer text color (#888888) on background (#2a2a2a) has a contrast ratio of 3.54:1, which fails WCAG AA requirements (minimum 4.5:1 for normal text).

Apply this fix:

-            color: #888888;
+            color: #a7a7a7;

The new color provides a contrast ratio of 4.52:1, meeting WCAG AA requirements.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
color: #888888;
color: #a7a7a7;

Comment on lines +52 to 62
.button {
display: inline-block;
font-size: 18px;
font-weight: bold;
padding: 10px 20px;
color: #ffffff;
background-color: #6200ea;
border-radius: 4px;
text-decoration: none;
margin-bottom: 20px;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reduce style duplication with a shared base class.

The .button and .otp-code classes share many properties and could be refactored to follow DRY principles.

Consider this refactor:

+.button-base {
+    display: inline-block;
+    font-weight: bold;
+    padding: 10px 20px;
+    color: #ffffff;
+    background-color: #6200ea;
+    border-radius: 4px;
+    text-decoration: none;
+    margin-bottom: 20px;
+    transition: background-color 0.2s ease;
+}
+
+.button-base:hover {
+    background-color: #7722ff;
+}
+
+.button-base:active {
+    background-color: #5000d0;
+}
+
-.otp-code {
-    display: inline-block;
+.otp-code extends .button-base {
     font-size: 28px;
-    font-weight: bold;
-    padding: 10px 20px;
-    color: #ffffff;
-    background-color: #6200ea;
-    border-radius: 4px;
-    text-decoration: none;
-    margin-bottom: 20px;
 }
 
-.button {
-    display: inline-block;
+.button extends .button-base {
     font-size: 18px;
-    font-weight: bold;
-    padding: 10px 20px;
-    color: #ffffff;
-    background-color: #6200ea;
-    border-radius: 4px;
-    text-decoration: none;
-    margin-bottom: 20px;
 }

Committable suggestion skipped: line range outside the PR's diff.

@0xF6 0xF6 merged commit 31bf9ca into master Nov 16, 2024
2 checks passed
@0xF6 0xF6 deleted the feature/fluid_and_email_forms branch November 16, 2024 04:28
@argon-chat argon-chat locked and limited conversation to collaborators Nov 16, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants