Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ jobs:
# AWS__Region=${{ env.AWS_REGION }}
# LogoDev__PublishableKey=${{ secrets.LOGODEV_PUBLISHABLE_KEY }}
# LogoDev__SecretKey=${{ secrets.LOGODEV_SECRET_KEY }}
# Email__ResendApiToken=${{ secrets.EMAIL__RESENDAPITOKEN }}
# EOF
# sudo chown ${{ secrets.LIGHTSAIL_USER }}:${{ secrets.LIGHTSAIL_USER }} /var/www/culinarycommand/.env
# sudo chmod 640 /var/www/culinarycommand/.env
Expand All @@ -184,6 +185,7 @@ jobs:
# export AWS__Region="${{ env.AWS_REGION }}"
# export LogoDev__PublishableKey="${{ secrets.LOGODEV_PUBLISHABLE_KEY }}"
# export LogoDev__SecretKey="${{ secrets.LOGODEV_SECRET_KEY }}"
# export Email__ResendApiToken="${{ secrets.EMAIL__RESENDAPITOKEN }}"
# EOF
# sudo chown ${{ secrets.LIGHTSAIL_USER }}:${{ secrets.LIGHTSAIL_USER }} /var/www/culinarycommand/.env.export
# sudo chmod 640 /var/www/culinarycommand/.env.export
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/ec2_init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ LOGODEV_SECRET_KEY=$(aws ssm get-parameter \
--query "Parameter.Value" \
--output text)

Email__ResendApiToken=$(aws ssm get-parameter \
--name "/culinarycommand/prod/Email__ResendApiToken" \
--with-decryption \
--region us-east-2 \
--query "Parameter.Value" \
--output text)

# Write environment variables to file
echo "[6/8] Writing environment file..."
cat > /etc/culinarycommand.env << EOF
Expand Down
1 change: 1 addition & 0 deletions CulinaryCommandApp/CulinaryCommand.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Content Include="AIDashboard\**\*">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<PackageReference Include="Resend" Version="0.2.2" />
</ItemGroup>

<!-- <ItemGroup>
Expand Down
11 changes: 10 additions & 1 deletion CulinaryCommandApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Microsoft.AspNetCore.DataProtection;
using CulinaryCommand.Vendor.Services;
using System.IO;
using Resend;
using CulinaryCommandApp.Inventory.Entities;


Expand Down Expand Up @@ -165,7 +166,15 @@
builder.Services.AddScoped<IUnitService, UnitService>();
builder.Services.AddScoped<IInventoryTransactionService, InventoryTransactionService>();
builder.Services.AddScoped<IInventoryManagementService, InventoryManagementService>();
builder.Services.AddScoped<IEmailSender, EmailSender>();
builder.Services.AddOptions(); // Start of Email Setup
builder.Services.AddHttpClient<ResendClient>();
builder.Services.Configure<ResendClientOptions>(o =>
{
o.ApiToken = builder.Configuration["Email:ResendApiToken"]
?? throw new InvalidOperationException("Email:ResendApiToken is not set.");
});
builder.Services.AddTransient<IResend, ResendClient>();
builder.Services.AddScoped<IEmailSender, EmailSender>(); // End of Email Setup
builder.Services.AddScoped<ITaskAssignmentService, TaskAssignmentService>();
builder.Services.AddScoped<ITaskLibraryService, TaskLibraryService>();
builder.Services.AddScoped<IPurchaseOrderService, PurchaseOrderService>();
Expand Down
84 changes: 75 additions & 9 deletions CulinaryCommandApp/Services/EmailSender.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,82 @@
namespace CulinaryCommand.Services
using System.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Resend;

namespace CulinaryCommand.Services;

public interface IEmailSender
{
public class EmailSender : IEmailSender
Task SendInviteEmailAsync(string toEmail, string firstName, string inviteToken);
}

public class EmailSender : IEmailSender
{
private readonly IResend _resend;
private readonly ILogger<EmailSender> _logger;
private readonly string _fromAddress = "noreply@culinary-command.com";
private readonly string _fromName = "Culinary Command";
private readonly string _appBaseUrl = "https://culinary-command.com";

public EmailSender(IResend resend, ILogger<EmailSender> logger, IConfiguration configuration)
{
public async Task SendEmailAsync(string toEmail, string subject, string body)
_resend = resend;
_logger = logger;
}

public async Task SendInviteEmailAsync(string toEmail, string firstName, string inviteToken)
{
if (string.IsNullOrWhiteSpace(toEmail))
throw new ArgumentException("Recipient email is required.", nameof(toEmail));

if (string.IsNullOrWhiteSpace(inviteToken))
throw new ArgumentException("Invite token is required.", nameof(inviteToken));

var safeName = WebUtility.HtmlEncode(firstName ?? string.Empty);
var inviteLink = $"{_appBaseUrl.TrimEnd('/')}/account/setup?token={Uri.EscapeDataString(inviteToken)}";

var message = new EmailMessage
{
// TODO: Replace with real SMTP or SendGrid logic
Console.WriteLine("=== EMAIL SENT ===");
Console.WriteLine($"To: {toEmail}");
Console.WriteLine($"Subject: {subject}");
Console.WriteLine(body);
From = $"{_fromName} <{_fromAddress}>",
Subject = "Set up your Culinary Command account",
HtmlBody = $@"
<div style=""font-family: Arial, sans-serif; line-height: 1.6;"">
<p>Hi {safeName},</p>
<p>You’ve been invited to join <strong>Culinary Command</strong>.</p>
<p>
<a href=""{inviteLink}""
style=""display:inline-block;padding:10px 16px;background:#111;color:#fff;text-decoration:none;border-radius:6px;"">
Set up your account
</a>
</p>
<p>Or copy and paste this link into your browser:</p>
<p>{WebUtility.HtmlEncode(inviteLink)}</p>
<p>If you weren’t expecting this email, you can ignore it.</p>
</div>"
};

message.To.Add(toEmail);

try
{
var response = await _resend.EmailSendAsync(message);

if (!response.Success)
{
var error = response.Exception?.Message ?? "Unknown Resend error";

await Task.CompletedTask;
_logger.LogError("Resend failed for {Email}. Error: {Error}", toEmail, error);

throw new InvalidOperationException(
$"Failed to send invite email to {toEmail}: {error}");
}

_logger.LogInformation("Invite email sent successfully to {Email}", toEmail);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while sending invite email to {Email}", toEmail);
throw;
}
}
}
7 changes: 0 additions & 7 deletions CulinaryCommandApp/Services/IEmailSender.cs

This file was deleted.

21 changes: 7 additions & 14 deletions CulinaryCommandApp/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,20 +463,13 @@ public async Task SendInviteEmailAsync(User user)
if (string.IsNullOrWhiteSpace(user.InviteToken))
throw new InvalidOperationException("User does not have an invite token.");

string link = $"https://culinary-command.com/account/setup?token={user.InviteToken}";

string subject = "Your CulinaryCommand Account Invitation";
string body = $@"
<h2>Welcome to CulinaryCommand!</h2>
<p>You have been invited to join <strong>{user.Company?.Name}</strong>.</p>
<p>Click the button below to set your password and activate your account:</p>
<p><a href='{link}' style='padding:10px 20px;background:#4CAF50;color:white;text-decoration:none;border-radius:4px;'>Set Your Password</a></p>
<p>If the button doesn't work, use this link:</p>
<p>{link}</p>
";

// implement actual email send (SendGrid, SMTP, Mailgun, whatever)
await _emailSender.SendEmailAsync(user.Email!, subject, body);
var firstName = user.Name?.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "there";

await _emailSender.SendInviteEmailAsync(
user.Email!,
firstName,
user.InviteToken
);
}

public async Task<User?> GetUserByInviteTokenAsync(string token)
Expand Down
Loading