Skip to content
Draft
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
61 changes: 60 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1 +1,60 @@
Always use System.text.json for working with JSON markup

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.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
15 changes: 15 additions & 0 deletions src/SharpSite.Abstractions.Base/IRegisterServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,18 @@ public interface IRegisterServices
IHostApplicationBuilder RegisterServices(IHostApplicationBuilder services, bool disableRetry = false);

}

public interface IManageDatabase
{
/// <summary>
/// Creates the database if it does not exist.
/// </summary>
void CreateDatabaseIfNotExists(string connectionString);

/// <summary>
/// Updates the database schema to the latest versions
/// </summary>
/// <returns></returns>
Task UpdateDatabaseSchemaAsync(string connectionString);

}
12 changes: 12 additions & 0 deletions src/SharpSite.Abstractions/ApplicationStateModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

}


}
33 changes: 32 additions & 1 deletion src/SharpSite.Data.Postgres/RegisterPostgresServices.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SharpSite.Abstractions;
using SharpSite.Abstractions.Base;

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<PgContext>();
optionsBuilder.UseNpgsql<PgContext>(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 failure on line 27 in src/SharpSite.Data.Postgres/RegisterPostgresServices.cs

View workflow job for this annotation

GitHub Actions / Playwright Tests

) expected

Check failure on line 27 in src/SharpSite.Data.Postgres/RegisterPostgresServices.cs

View workflow job for this annotation

GitHub Actions / Playwright Tests

) expected

Check failure on line 27 in src/SharpSite.Data.Postgres/RegisterPostgresServices.cs

View workflow job for this annotation

GitHub Actions / test

) expected

Check failure on line 27 in src/SharpSite.Data.Postgres/RegisterPostgresServices.cs

View workflow job for this annotation

GitHub Actions / test

) expected

// check if AppSettings has the connection string

}

host.Services.AddTransient<IPageRepository, PgPageRepository>();
host.Services.AddTransient<IPostRepository, PgPostRepository>();
host.Services.AddTransient<IManageDatabase, RegisterPostgresServices>();
host.AddNpgsqlDbContext<PgContext>(Constants.DBNAME, configure =>
{
configure.DisableRetry = disableRetry;
Expand All @@ -20,6 +41,16 @@
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<PgContext>();
optionsBuilder.UseNpgsql<PgContext>(connectionString);
using var context = new PgContext(optionsBuilder.Options);
await context.Database.MigrateAsync();
}

}

public static class Constants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<PgSecurityContext>();
optionsBuilder.UseNpgsql<PgSecurityContext>(connectionString);
using (var context = new PgSecurityContext(optionsBuilder.Options))
{
context.Database.EnsureCreated();


}

/// <summary>
/// Updates the database schema to the latest versions
/// </summary>
/// <returns></returns>
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<PgSecurityContext>();
optionsBuilder.UseNpgsql<PgSecurityContext>(connectionString);
using (var context = new PgSecurityContext(optionsBuilder.Options))
{
return context.Database.MigrateAsync();
}

}

public void MapEndpoints(IEndpointRouteBuilder endpointDooHickey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ public class ApplicationState : ApplicationStateModel
{
public record CurrentThemeRecord(string IdVersion);



public record LocalizationRecord(string? DefaultCulture, string[]? SupportedCultures);

[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
Expand All @@ -26,6 +24,23 @@ public record LocalizationRecord(string? DefaultCulture, string[]? SupportedCult

public Dictionary<string, ISharpSiteConfigurationSection> 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<ApplicationState, ISharpSiteConfigurationSection, Task>? ConfigurationSectionChanged;

[JsonIgnore]
Expand Down
20 changes: 10 additions & 10 deletions src/SharpSite.Web/Components/Startup/Step2.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ Placeholder="Upload a logo for your website"
</p>

<div style="display: flex; justify-content: center; gap: 10px; margin-top: 10px;">
<button class="btn btn-primary" @onclick="Finish">Finish</button>
<button class="btn btn-secondary" @onclick="SkipAndFinish">Skip and Finish</button>
<button class="btn btn-primary" @onclick="Next">Next</button>
<button class="btn btn-secondary" @onclick="Skip">Skip</button>
</div>

@code {
Expand All @@ -59,7 +59,7 @@ Placeholder="Upload a logo for your website"
await base.OnInitializedAsync();
}

private async Task Finish(MouseEventArgs args)
private async Task Next(MouseEventArgs args)
{

var FileStorage = PluginManager.GetPluginProvidedService<IHandleFileStorage>();
Expand All @@ -70,19 +70,19 @@ Placeholder="Upload a logo for your website"
}

await FileStorage.AddFile(new FileData(Logo, new FileMetaData("logo.png", "image/png", DateTimeOffset.Now)));
AppState.StartupCompleted = true;
AppState.StartupStep = 0;
AppState.StartupStep = 3;
AppState.HasCustomLogo = "logo.png";
await AppState.Save();
NavManager.NavigateTo($"/", false);
NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false);
}

private async Task SkipAndFinish(MouseEventArgs args)
private async Task Skip(MouseEventArgs args)
{
AppState.StartupCompleted = true;
AppState.StartupStep = 0;

AppState.StartupStep = 3;
await AppState.Save();
NavManager.NavigateTo($"/", false);
NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false);

}

}
106 changes: 106 additions & 0 deletions src/SharpSite.Web/Components/Startup/Step3.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
@page "/start/step3"
@using SharpSite.Abstractions.FileStorage
@using System.ComponentModel.DataAnnotations
@using SharpSite.Security.Postgres
@inject ApplicationState AppState
@inject NavigationManager NavManager
@inject PluginManager PluginManager
@inject IManageDatabase DatabaseManager
@rendermode InteractiveServer

<img src="/img/logo-500.webp" alt="SharpSite" style="min-width: 500px;" />

<h2 class="h2">Step @Step - Database Storage for @AppState.SiteName</h2>

<p>
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.
</p>

<p>
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.
</p>

<p>
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.
</p>

<EditForm Model="@DatabaseConfig" OnValidSubmit="SaveDatabaseConfig">
<DataAnnotationsValidator />
<ValidationSummary />

<div>
<label for="serverName">Database Server Name:</label>
<InputText id="serverName" @bind-Value="DatabaseConfig.ServerName" class="form-control" />
<ValidationMessage For="@(() => DatabaseConfig.ServerName)" />
</div>
<div>
<label for="userId">User ID:</label>
<InputText id="userId" @bind-Value="DatabaseConfig.UserId" class="form-control" />
<ValidationMessage For="@(() => DatabaseConfig.UserId)" />
</div>
<div>
<label for="password">Password:</label>
<InputText id="password" @bind-Value="DatabaseConfig.Password" type="password" class="form-control" />
<ValidationMessage For="@(() => DatabaseConfig.Password)" />
</div>
<button type="submit" class="btn btn-primary">Finish</button>
</EditForm>

@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;
}

}
Loading
Loading