diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index f7fc68e..c44e6f4 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1 +1,60 @@
-Always use System.text.json for working with JSON markup
\ No newline at end of file
+
+You are a senior Blazor and .NET developer, experienced in C#, ASP.NET Core, and Entity Framework Core. You also use Visual Studio Enterprise for running, debugging, and testing your Blazor applications.
+
+## Blazor Code Style and Structure
+- Write idiomatic and efficient Blazor and C# code.
+- Follow .NET and Blazor conventions.
+- Use Razor Components appropriately for component-based UI development.
+- Prefer inline functions for smaller components but separate complex logic into code-behind or service classes.
+- Async/await should be used where applicable to ensure non-blocking UI operations.
+
+## Naming Conventions
+- Follow PascalCase for component names, method names, and public members.
+- Use underscore prefix and then PascalCase for private fields.
+- Use camelCase for local variables.
+- Prefix interface names with "I" (e.g., IUserService).
+
+## Blazor and .NET Specific Guidelines
+- Utilize Blazor's built-in features for component lifecycle (e.g., OnInitializedAsync, OnParametersSetAsync).
+- Use data binding effectively with @bind.
+- Leverage Dependency Injection for services in Blazor.
+- Structure Blazor components and services following Separation of Concerns.
+- Use C# 10+ features like record types, pattern matching, and global usings.
+
+## Error Handling and Validation
+- Implement proper error handling for Blazor pages and API calls.
+- Use logging for error tracking in the backend and consider capturing UI-level errors in Blazor with tools like ErrorBoundary.
+- Implement validation using FluentValidation or DataAnnotations in forms.
+
+## Blazor API and Performance Optimization
+- Utilize Blazor SSR for most pages in the site, with Blazor Interactive Server rendering used for all Admin pages
+- Use asynchronous methods (async/await) for API calls or UI actions that could block the main thread.
+- Optimize Razor components by reducing unnecessary renders and using StateHasChanged() efficiently.
+- Minimize the component render tree by avoiding re-renders unless necessary, using ShouldRender() where appropriate.
+- Use EventCallbacks for handling user interactions efficiently, passing only minimal data when triggering events.
+
+## Caching Strategies
+- Implement in-memory caching for frequently used data, especially for Blazor Server apps. Use IMemoryCache for lightweight caching solutions.
+- For Blazor WebAssembly, utilize localStorage or sessionStorage to cache application state between user sessions.
+- Consider Distributed Cache strategies (like Redis or SQL Server Cache) for larger applications that need shared state across multiple users or clients.
+- Cache API calls by storing responses to avoid redundant calls when data is unlikely to change, thus improving the user experience.
+
+## State Management Libraries
+- Use Blazor’s built-in Cascading Parameters and EventCallbacks for basic state sharing across components.
+- For server-side Blazor, use Scoped Services and the StateContainer pattern to manage state within user sessions while minimizing re-renders.
+
+## API Design and Integration
+- Use HttpClient or other appropriate services to communicate with external APIs or your own backend.
+- Implement error handling for API calls using try-catch and provide proper user feedback in the UI.
+
+## Testing and Debugging in Visual Studio
+- Test Blazor components and services using xUnit.
+- Use Moq for mocking dependencies during tests.
+
+## Security and Authentication
+- Implement Authentication and Authorization in the Blazor app where necessary using ASP.NET Identity or JWT tokens for API authentication.
+- Use HTTPS for all web communication and ensure proper CORS policies are implemented.
+
+## API Documentation and Swagger
+- Use Swagger/OpenAPI for API documentation for your backend API services.
+- Ensure XML documentation for models and API methods for enhancing Swagger documentation.
diff --git a/.gitignore b/.gitignore
index b4e06c5..d083d9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
# Exclude installed plugins from Sharpsite.web
src/SharpSite.Web/plugins/
+src/SharpSite.Web/_plugins/
artifacts/FirstPlugin/
artifacts/FileSystemPlugin/
src/SharpSite.Web/Locales/SharpTranslator/
diff --git a/src/SharpSite.Abstractions.Base/IRegisterServices.cs b/src/SharpSite.Abstractions.Base/IRegisterServices.cs
index 8a76caa..fd823ee 100644
--- a/src/SharpSite.Abstractions.Base/IRegisterServices.cs
+++ b/src/SharpSite.Abstractions.Base/IRegisterServices.cs
@@ -11,3 +11,18 @@ public interface IRegisterServices
IHostApplicationBuilder RegisterServices(IHostApplicationBuilder services, bool disableRetry = false);
}
+
+public interface IManageDatabase
+{
+ ///
+ /// Creates the database if it does not exist.
+ ///
+ void CreateDatabaseIfNotExists(string connectionString);
+
+ ///
+ /// Updates the database schema to the latest versions
+ ///
+ ///
+ Task UpdateDatabaseSchemaAsync(string connectionString);
+
+}
diff --git a/src/SharpSite.Abstractions/ApplicationStateModel.cs b/src/SharpSite.Abstractions/ApplicationStateModel.cs
index 7d33bb6..c52f264 100644
--- a/src/SharpSite.Abstractions/ApplicationStateModel.cs
+++ b/src/SharpSite.Abstractions/ApplicationStateModel.cs
@@ -26,6 +26,18 @@ public class ApplicationStateModel
public string PageNotFoundContent { get; set; } = string.Empty;
+ public virtual string GetConfigurationByName(string name, string defaultValue = "")
+ {
+
+ return name switch
+ {
+ "SiteName" => SiteName,
+ "PageNotFoundContent" => PageNotFoundContent,
+ "MaximumUploadSizeMB" => MaximumUploadSizeMB.ToString(),
+ _ => defaultValue
+ };
+
+ }
}
diff --git a/src/SharpSite.Data.Postgres/RegisterPostgresServices.cs b/src/SharpSite.Data.Postgres/RegisterPostgresServices.cs
index ccb41fe..d870ea0 100644
--- a/src/SharpSite.Data.Postgres/RegisterPostgresServices.cs
+++ b/src/SharpSite.Data.Postgres/RegisterPostgresServices.cs
@@ -1,3 +1,4 @@
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SharpSite.Abstractions;
@@ -5,13 +6,33 @@
namespace SharpSite.Data.Postgres;
-public class RegisterPostgresServices : IRegisterServices
+public class RegisterPostgresServices : IRegisterServices, IManageDatabase
{
+ public void CreateDatabaseIfNotExists(string connectionString)
+ {
+
+ // create an instance of the database if it does not exist using the entity framework context with the connection string passed in
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseNpgsql(connectionString);
+ using var context = new PgContext(optionsBuilder.Options);
+ context.Database.EnsureCreated();
+
+ }
+
+
public IHostApplicationBuilder RegisterServices(IHostApplicationBuilder host, bool disableRetry = false)
{
+ // check if the database connection string is available
+ if (string.IsNullOrEmpty(host.Configuration[$"Connectionstrings:{Constants.DBNAME}"]) {
+
+ // check if AppSettings has the connection string
+
+ }
+
host.Services.AddTransient();
host.Services.AddTransient();
+ host.Services.AddTransient();
host.AddNpgsqlDbContext(Constants.DBNAME, configure =>
{
configure.DisableRetry = disableRetry;
@@ -20,6 +41,16 @@ public IHostApplicationBuilder RegisterServices(IHostApplicationBuilder host, bo
return host;
}
+
+ public async Task UpdateDatabaseSchemaAsync(string connectionString)
+ {
+ // create an instance of the database if it does not exist using the entity framework context with the connection string passed in
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseNpgsql(connectionString);
+ using var context = new PgContext(optionsBuilder.Options);
+ await context.Database.MigrateAsync();
+ }
+
}
public static class Constants
diff --git a/src/SharpSite.Security.Postgres/RegisterPostgresSecurityServices.cs b/src/SharpSite.Security.Postgres/RegisterPostgresSecurityServices.cs
index 450dc98..e266ff8 100644
--- a/src/SharpSite.Security.Postgres/RegisterPostgresSecurityServices.cs
+++ b/src/SharpSite.Security.Postgres/RegisterPostgresSecurityServices.cs
@@ -14,7 +14,7 @@
namespace SharpSite.Security.Postgres;
-public class RegisterPostgresSecurityServices : IRegisterServices, IRunAtStartup
+public class RegisterPostgresSecurityServices : IRegisterServices, IRunAtStartup, IManageDatabase
{
private const string InitializeUsersActivitySourceName = "Initial Users and Roles";
@@ -120,6 +120,34 @@ public async Task RunAtStartup(IServiceProvider services)
activity?.AddEvent(new ActivityEvent("Assigned admin user to Admin role"));
}
+ public void CreateDatabaseIfNotExists(string connectionString)
+ {
+
+ // create the PgSecurityContext if it does not exist using the entity framework context with the connection string passed in
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseNpgsql(connectionString);
+ using (var context = new PgSecurityContext(optionsBuilder.Options))
+ {
+ context.Database.EnsureCreated();
+
+
+ }
+
+ ///
+ /// Updates the database schema to the latest versions
+ ///
+ ///
+ public Task UpdateDatabaseSchemaAsync(string connectionString)
+ {
+
+ // create the PgSecurityContext if it does not exist using the entity framework context with the connection string passed in
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseNpgsql(connectionString);
+ using (var context = new PgSecurityContext(optionsBuilder.Options))
+ {
+ return context.Database.MigrateAsync();
+ }
+
}
public void MapEndpoints(IEndpointRouteBuilder endpointDooHickey)
diff --git a/src/SharpSite.Web/ApplicatonState.cs b/src/SharpSite.Web/ApplicationState.cs
similarity index 89%
rename from src/SharpSite.Web/ApplicatonState.cs
rename to src/SharpSite.Web/ApplicationState.cs
index c4f0a0f..393b19e 100644
--- a/src/SharpSite.Web/ApplicatonState.cs
+++ b/src/SharpSite.Web/ApplicationState.cs
@@ -12,8 +12,6 @@ public class ApplicationState : ApplicationStateModel
{
public record CurrentThemeRecord(string IdVersion);
-
-
public record LocalizationRecord(string? DefaultCulture, string[]? SupportedCultures);
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@@ -26,6 +24,23 @@ public record LocalizationRecord(string? DefaultCulture, string[]? SupportedCult
public Dictionary ConfigurationSections { get; private set; } = new();
+ public string ContentConnectionString { get; set; } = string.Empty;
+ public string SecurityConnectionString { get; set; } = string.Empty;
+
+ public override string GetConfigurationByName(string name, string defaultValue = "")
+ {
+ return name switch
+ {
+ "ContentConnectionString" => ContentConnectionString,
+ "SecurityConnectionString" => SecurityConnectionString,
+ "SiteName" => SiteName,
+ "PageNotFoundContent" => PageNotFoundContent,
+ "MaximumUploadSizeMB" => MaximumUploadSizeMB.ToString(),
+ "RobotsTxtCustomContent" => RobotsTxtCustomContent ?? string.Empty,
+ _ => base.GetConfigurationByName(name, defaultValue)
+ };
+ }
+
public event Func? ConfigurationSectionChanged;
[JsonIgnore]
diff --git a/src/SharpSite.Web/Components/Startup/Step2.razor b/src/SharpSite.Web/Components/Startup/Step2.razor
index 40ef164..9bf1d57 100644
--- a/src/SharpSite.Web/Components/Startup/Step2.razor
+++ b/src/SharpSite.Web/Components/Startup/Step2.razor
@@ -42,8 +42,8 @@ Placeholder="Upload a logo for your website"
Step @Step - Database Storage for @AppState.SiteName
+
+
+ Let's configure where the data for your website is going to be stored. This is important because this is where all of our data, including your blog posts and page information, will be securely stored and retrieved.
+
+
+
+ Currently, we only support Postgres as a database. This is a very popular and powerful database that is used by many websites. It is also very easy to use and has a lot of great features.
+ Sharp site is going to create two database instances on your server: one that contains the data for the content served by SharpSite and a second that contains this security information that SharpSite uses
+ to authenticate users and manage permissions. This is a very important step, so please make sure that you have the correct information before continuing.
+
+
+
+ Let's configure the database connection string. This is a string that tells SharpSite how to connect to your database. You can get this from your hosting provider or from your local database installation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+
+ const int Step = 3;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (AppState.StartupCompleted) NavManager.NavigateTo("/", true);
+ if (AppState.StartupStep != Step) NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false);
+
+ await base.OnInitializedAsync();
+ }
+
+ private DatabaseConfigModel DatabaseConfig { get; set; } = new();
+
+ private async Task SaveDatabaseConfig()
+ {
+
+ // Format a connection string for the database using Postgres syntax and using a database name of "sharpsite" and a port of 5432.
+ string connectionString = $"Host={DatabaseConfig.ServerName};Port=5432;Username={DatabaseConfig.UserId};Password={DatabaseConfig.Password};Database=sharpsite;Pooling=true;SSL Mode=Prefer;Trust Server Certificate=true;";
+ AppState.ContentConnectionString = connectionString;
+
+ // Format a connection string for the database using Postgres syntax and using a database name of "sharpsite_security" and a port of 5432.
+ string securityConnectionString = $"Host={DatabaseConfig.ServerName};Port=5432;Username={DatabaseConfig.UserId};Password={DatabaseConfig.Password};Database=sharpsite_security;Pooling=true;SSL Mode=Prefer;Trust Server Certificate=true;";
+ AppState.SecurityConnectionString = securityConnectionString;
+
+ AppState.StartupStep = 0;
+ AppState.StartupCompleted = true;
+ await AppState.Save();
+
+ DatabaseManager.CreateDatabaseIfNotExists(connectionString);
+ await DatabaseManager.UpdateDatabaseSchemaAsync(connectionString);
+
+ // Create the security database if it does not exist.
+ var securityServices = new RegisterPostgresSecurityServices();
+ securityServices.CreateDatabaseIfNotExists(securityConnectionString);
+ await securityServices.UpdateDatabaseSchemaAsync(securityConnectionString);
+
+ // Restart the application to apply the changes.
+
+ // NavManager.NavigateTo("/", true);
+
+ }
+
+ private class DatabaseConfigModel
+ {
+ [Required()]
+ public string ServerName { get; set; } = string.Empty;
+
+ [Required()]
+ public string UserId { get; set; } = string.Empty;
+
+ [Required()]
+ public string Password { get; set; } = string.Empty;
+ }
+
+}
\ No newline at end of file
diff --git a/src/SharpSite.Web/Program.cs b/src/SharpSite.Web/Program.cs
index 2ec21d0..d731db7 100644
--- a/src/SharpSite.Web/Program.cs
+++ b/src/SharpSite.Web/Program.cs
@@ -1,3 +1,4 @@
+
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using SharpSite.Abstractions;
@@ -9,6 +10,8 @@
var builder = WebApplication.CreateBuilder(args);
+var appState = builder.AddPluginManagerAndAppState();
+
// Load plugins for postgres
#region Postgres Plugins
var pg = new RegisterPostgresServices();
@@ -18,8 +21,6 @@
pgSecurity.RegisterServices(builder);
#endregion
-var appState = builder.AddPluginManagerAndAppState();
-
// add the custom localization features for the application framework
builder.ConfigureRequestLocalization();
@@ -92,4 +93,4 @@
app.UseMiddleware();
-app.Run();
+app.RunAsync()