diff --git a/MyApp.Client/nuget.config b/MyApp.Client/nuget.config
deleted file mode 100644
index 6548586..0000000
--- a/MyApp.Client/nuget.config
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/MyApp.Client/package.json b/MyApp.Client/package.json
index 20d2c17..8cdc1c1 100644
--- a/MyApp.Client/package.json
+++ b/MyApp.Client/package.json
@@ -11,45 +11,45 @@
"preview": "vite preview"
},
"dependencies": {
- "@radix-ui/react-dialog": "^1.0.5",
- "@radix-ui/react-dropdown-menu": "^2.0.6",
- "@radix-ui/react-slot": "^1.0.2",
- "@servicestack/client": "^2.1.1",
- "@tanstack/react-table": "^8.12.0",
+ "@radix-ui/react-dialog": "^1.1.1",
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@servicestack/client": "^2.1.5",
+ "@tanstack/react-table": "^8.20.5",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
- "clsx": "^2.1.0",
+ "clsx": "^2.1.1",
"gray-matter": "^4.0.3",
- "lucide-react": "^0.331.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
+ "lucide-react": "^0.438.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
- "react-helmet-async": "^2.0.4",
- "react-router": "^6.22.0",
- "react-router-dom": "^6.22.0",
- "swr": "^2.2.4",
- "tailwind-merge": "^2.2.1",
+ "react-helmet-async": "^2.0.5",
+ "react-router": "^6.26.1",
+ "react-router-dom": "^6.26.1",
+ "swr": "^2.2.5",
+ "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
- "@iconify/react": "^4.1.1",
+ "@iconify/react": "^5.0.2",
"@mdx-js/rollup": "^3.0.1",
- "@tailwindcss/forms": "^0.5.7",
- "@tailwindcss/typography": "^0.5.10",
- "@types/mdx": "^2.0.11",
- "@types/node": "^20.11.19",
- "@types/react": "^18.2.55",
- "@types/react-dom": "^18.2.19",
+ "@tailwindcss/forms": "^0.5.8",
+ "@tailwindcss/typography": "^0.5.15",
+ "@types/mdx": "^2.0.13",
+ "@types/node": "^22.5.3",
+ "@types/react": "^18.3.5",
+ "@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11",
"@types/remark-prism": "^1.3.7",
- "@typescript-eslint/eslint-plugin": "^6.21.0",
- "@typescript-eslint/parser": "^6.21.0",
- "@vitejs/plugin-react": "^4.2.1",
- "autoprefixer": "^10.4.17",
- "eslint": "^8.56.0",
- "eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-react-refresh": "^0.4.5",
- "postcss": "^8.4.35",
+ "@typescript-eslint/eslint-plugin": "^8.4.0",
+ "@typescript-eslint/parser": "^8.4.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.9.1",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "eslint-plugin-react-refresh": "^0.4.11",
+ "postcss": "^8.4.45",
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.0",
"remark-directive": "^3.0.0",
@@ -57,10 +57,10 @@
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-prism": "^1.3.6",
- "tailwindcss": "^3.4.1",
- "typescript": "^5.2.2",
- "vite": "^5.1.0",
- "vite-plugin-pages": "^0.32.0",
+ "tailwindcss": "^3.4.10",
+ "typescript": "^5.5.4",
+ "vite": "^5.4.3",
+ "vite-plugin-pages": "^0.32.3",
"vite-plugin-press": "^1.0.10",
"vite-plugin-svgr": "^4.2.0"
}
diff --git a/MyApp.Client/src/pages/error.tsx b/MyApp.Client/src/pages/error.tsx
new file mode 100644
index 0000000..19adbaa
--- /dev/null
+++ b/MyApp.Client/src/pages/error.tsx
@@ -0,0 +1,17 @@
+import { useSearchParams } from "react-router-dom"
+
+export default () => {
+ const [query, _] = useSearchParams()
+ const message = query.get('message') ?? 'Unknown error'
+
+ return (<>
+
+ >
+ )
+}
diff --git a/MyApp.ServiceInterface/EmailServices.cs b/MyApp.ServiceInterface/EmailServices.cs
index 5e3974a..dc01b35 100644
--- a/MyApp.ServiceInterface/EmailServices.cs
+++ b/MyApp.ServiceInterface/EmailServices.cs
@@ -1,7 +1,7 @@
using System.Net.Mail;
using Microsoft.Extensions.Logging;
using ServiceStack;
-using MyApp.ServiceModel;
+using ServiceStack.Jobs;
namespace MyApp.ServiceInterface;
@@ -45,16 +45,26 @@ public class SmtpConfig
public string? Bcc { get; set; }
}
-///
-/// Uses a configured SMTP client to send emails
-///
-public class EmailServices(SmtpConfig config, ILogger log)
- // TODO: Uncomment to enable sending emails with SMTP
- // : Service
+public class SendEmail
+{
+ public string To { get; set; }
+ public string? ToName { get; set; }
+ public string Subject { get; set; }
+ public string? BodyText { get; set; }
+ public string? BodyHtml { get; set; }
+}
+
+[Worker("smtp")]
+public class SendEmailCommand(ILogger logger, IBackgroundJobs jobs, SmtpConfig config)
+ : SyncCommand
{
- public object Any(SendEmail request)
+ private static long count = 0;
+ protected override void Run(SendEmail request)
{
- log.LogInformation("Sending email to {Email} with subject {Subject}", request.To, request.Subject);
+ Interlocked.Increment(ref count);
+ var log = Request.CreateJobLogger(jobs, logger);
+ log.LogInformation("Sending {Count} email to {Email} with subject {Subject}",
+ count, request.To, request.Subject);
using var client = new SmtpClient(config.Host, config.Port);
client.Credentials = new System.Net.NetworkCredential(config.Username, config.Password);
@@ -80,7 +90,5 @@ public object Any(SendEmail request)
}
client.Send(msg);
-
- return new EmptyResponse();
}
}
diff --git a/MyApp.ServiceModel/Emails.cs b/MyApp.ServiceModel/Emails.cs
deleted file mode 100644
index 140fcd5..0000000
--- a/MyApp.ServiceModel/Emails.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using ServiceStack;
-using ServiceStack.DataAnnotations;
-
-namespace MyApp.ServiceModel;
-
-[ExcludeMetadata]
-[Restrict(InternalOnly = true)]
-public class SendEmail : IReturn
-{
- public string To { get; set; }
- public string? ToName { get; set; }
- public string Subject { get; set; }
- public string? BodyText { get; set; }
- public string? BodyHtml { get; set; }
-}
diff --git a/MyApp/Configure.BackgroundJobs.cs b/MyApp/Configure.BackgroundJobs.cs
new file mode 100644
index 0000000..44d1c7c
--- /dev/null
+++ b/MyApp/Configure.BackgroundJobs.cs
@@ -0,0 +1,62 @@
+using Microsoft.AspNetCore.Identity;
+using MyApp.Data;
+using MyApp.ServiceInterface;
+using ServiceStack.Jobs;
+
+[assembly: HostingStartup(typeof(MyApp.ConfigureBackgroundJobs))]
+
+namespace MyApp;
+
+public class ConfigureBackgroundJobs : IHostingStartup
+{
+ public void Configure(IWebHostBuilder builder) => builder
+ .ConfigureServices(services => {
+ services.AddPlugin(new CommandsFeature());
+ services.AddPlugin(new BackgroundsJobFeature());
+ services.AddHostedService();
+ }).ConfigureAppHost(afterAppHostInit: appHost => {
+ var services = appHost.GetApplicationServices();
+ var jobs = services.GetRequiredService();
+ // Example of registering a Recurring Job to run Every Hour
+ //jobs.RecurringCommand(Schedule.Hourly);
+ });
+}
+
+public class JobsHostedService(ILogger log, IBackgroundJobs jobs) : BackgroundService
+{
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await jobs.StartAsync(stoppingToken);
+
+ using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
+ while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
+ {
+ await jobs.TickAsync();
+ }
+ }
+}
+
+///
+/// Sends emails by executing SendEmailCommand in a background job where it's serially processed by 'smtp' worker
+///
+public class EmailSender(IBackgroundJobs jobs) : IEmailSender
+{
+ public Task SendEmailAsync(string email, string subject, string htmlMessage)
+ {
+ jobs.EnqueueCommand(new SendEmail {
+ To = email,
+ Subject = subject,
+ BodyHtml = htmlMessage,
+ });
+ return Task.CompletedTask;
+ }
+
+ public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
+ SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here.");
+
+ public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
+ SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here.");
+
+ public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
+ SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
+}
diff --git a/MyApp/Configure.Mq.cs b/MyApp/Configure.Mq.cs
deleted file mode 100644
index 7108270..0000000
--- a/MyApp/Configure.Mq.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using ServiceStack.Messaging;
-using MyApp.ServiceInterface;
-using MyApp.ServiceModel;
-using Microsoft.AspNetCore.Identity;
-using MyApp.Data;
-
-[assembly: HostingStartup(typeof(MyApp.ConfigureMq))]
-
-namespace MyApp;
-
-/**
- * Register ServiceStack Services you want to be able to invoke in a managed Background Thread
- * https://docs.servicestack.net/background-mq
-*/
-public class ConfigureMq : IHostingStartup
-{
- public void Configure(IWebHostBuilder builder) => builder
- .ConfigureServices((context, services) => {
- var smtpConfig = context.Configuration.GetSection(nameof(SmtpConfig))?.Get();
- if (smtpConfig is not null)
- {
- services.AddSingleton(smtpConfig);
- }
- services.AddSingleton(c => new BackgroundMqService());
- services.AddPlugin(new CommandsFeature());
- })
- .ConfigureAppHost(afterAppHostInit: appHost => {
- var mqService = appHost.Resolve();
-
- //Register ServiceStack APIs you want to be able to invoke via MQ
- mqService.RegisterHandler(appHost.ExecuteMessage);
- mqService.Start();
- });
-}
-
-// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
-
-///
-/// Sends emails by publishing a message to the Background MQ Server where it's processed in the background
-///
-public class EmailSender(IMessageService messageService) : IEmailSender
-{
- public Task SendEmailAsync(string email, string subject, string htmlMessage)
- {
- using var mqClient = messageService.CreateMessageProducer();
- mqClient.Publish(new SendEmail
- {
- To = email,
- Subject = subject,
- BodyHtml = htmlMessage,
- });
-
- return Task.CompletedTask;
- }
-
- public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
- SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here.");
-
- public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
- SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here.");
-
- public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
- SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
-}
\ No newline at end of file
diff --git a/MyApp/MyApp.csproj b/MyApp/MyApp.csproj
index ac17a4b..f23edb2 100644
--- a/MyApp/MyApp.csproj
+++ b/MyApp/MyApp.csproj
@@ -26,6 +26,7 @@
+
diff --git a/MyApp/Program.cs b/MyApp/Program.cs
index 443174c..90d2732 100644
--- a/MyApp/Program.cs
+++ b/MyApp/Program.cs
@@ -26,7 +26,9 @@
.PersistKeysToFileSystem(new DirectoryInfo("App_Data"));
// Add application services.
-services.AddSingleton, IdentityNoOpEmailSender>();
+// services.AddSingleton, IdentityNoOpEmailSender>();
+// Uncomment to send emails with SMTP, configure SMTP with "SmtpConfig" in appsettings.json
+services.AddSingleton, EmailSender>();
services.AddScoped, AdditionalUserClaimsPrincipalFactory>();
// Register all services
diff --git a/MyApp/Properties/launchSettings.json b/MyApp/Properties/launchSettings.json
index 3bd9867..fc2e56e 100644
--- a/MyApp/Properties/launchSettings.json
+++ b/MyApp/Properties/launchSettings.json
@@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "metadata",
- "applicationUrl": "http://localhost:5122",
+ "applicationUrl": "https://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..b06e0d0
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file