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

User Management #13

Merged
merged 10 commits into from
Apr 29, 2024
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
4 changes: 4 additions & 0 deletions MPM-Betting.Api/MPM-Betting.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0-preview.3.24210.17" />
<PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching" Version="9.0.0-preview.3.24210.17" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="9.0.0-preview.3.24172.13" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0-preview.3.24172.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0-preview.3.24172.4" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0-preview.3.24172.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0-preview.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

Expand Down
24 changes: 20 additions & 4 deletions MPM-Betting.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
using System.Net.Mail;
using Microsoft.EntityFrameworkCore;
using MPM_Betting.DataModel;
using MPM_Betting.Services;
using MPM_Betting.Services.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.AddServiceDefaults();
builder.AddMpmDbContext();
builder.AddRedisDistributedCache("redis");
builder.AddMpmCache();
builder.AddMpmAuth();
builder.AddMpmMail();
builder.AddFootballApi();



builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMemoryCache();

var app = builder.Build();

Expand All @@ -32,6 +39,15 @@
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/testMail", async (SmtpClient smtpClient, string email) =>
{
using var message = new MailMessage("[email protected]", email);
message.Subject = "Take this email and wear it with proud";
message.Body = "some data, <h1>HTML works?</h1><h2>Who knows</h2>";
message.IsBodyHtml = true;
await smtpClient.SendMailAsync(message);
});

app.MapGet("/weatherforecast", () =>
{
Thread.Sleep(2000);
Expand Down
111 changes: 110 additions & 1 deletion MPM-Betting.Aspire/MPM-Betting.Aspire.AppHost/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using Aspire.Hosting;
using System.Diagnostics;
using System.Reflection;
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Hosting;

namespace MPM_Betting.Aspire.AppHost;

Expand Down Expand Up @@ -51,4 +57,107 @@ public static IResourceBuilder<ExecutableResource> AddAzureFunction<TServiceMeta
executableBuilder.WithAnnotation(serviceMetadata);
return executableBuilder;
}

/// <summary>
/// Creates a password parameter that in the development environment is generated once and stored in the user secrets store.
/// </summary>
/// <remarks>
/// The password is only stable when in the development environment and the application is running. In all other cases, the password is generated each time.
/// </remarks>
public static IResourceBuilder<ParameterResource> CreateStablePassword(this IDistributedApplicationBuilder builder, string name,
bool lower = true, bool upper = true, bool numeric = true, bool special = true,
int minLower = 0, int minUpper = 0, int minNumeric = 0, int minSpecial = 0)
{
ParameterDefault generatedPassword = new GenerateParameterDefault
{
MinLength = 22, // enough to give 128 bits of entropy when using the default 67 possible characters. See remarks in PasswordGenerator.Generate
Lower = lower,
Upper = upper,
Numeric = numeric,
Special = special,
MinLower = minLower,
MinUpper = minUpper,
MinNumeric = minNumeric,
MinSpecial = minSpecial
};

if (builder.Environment.IsDevelopment() && builder.ExecutionContext.IsRunMode)
{
// In development mode, generate a new password each time the application starts
generatedPassword = new UserSecretsParameterDefault(builder.Environment.ApplicationName, name, generatedPassword);
}

var parameterResource = new ParameterResource(name, parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault), true)
{
Default = generatedPassword
};

return ResourceBuilder.Create(parameterResource, builder);
}

private static string GetParameterValue(IConfiguration configuration, string name, ParameterDefault? parameterDefault)
{
var configurationKey = $"Parameters:{name}";
return configuration[configurationKey]
?? parameterDefault?.GetDefaultValue()
?? throw new DistributedApplicationException($"Parameter resource could not be used because configuration key '{configurationKey}' is missing and the Parameter has no default value."); ;
}

private class UserSecretsParameterDefault(string applicationName, string parameterName, ParameterDefault parameterDefault) : ParameterDefault
{
public override string GetDefaultValue()
{
var value = parameterDefault.GetDefaultValue();
var configurationKey = $"Parameters:{parameterName}";
TrySetUserSecret(applicationName, configurationKey, value);
return value;
}

public override void WriteToManifest(ManifestPublishingContext context) => parameterDefault.WriteToManifest(context);

private static bool TrySetUserSecret(string applicationName, string name, string value)
{
if (string.IsNullOrEmpty(applicationName)) return false;
var appAssembly = Assembly.Load(new AssemblyName(applicationName));
if (appAssembly is null || appAssembly.GetCustomAttribute<UserSecretsIdAttribute>()?.UserSecretsId is not
{ } userSecretsId) return false;
// Save the value to the secret store
try
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
new List<string>(["user-secrets", "set", name, value, "--id", userSecretsId]).ForEach(startInfo.ArgumentList.Add);
var setUserSecrets = Process.Start(startInfo);
setUserSecrets?.WaitForExit(TimeSpan.FromSeconds(10));
return setUserSecrets?.ExitCode == 0;
}
catch (Exception) { }

return false;
}
}

private class ResourceBuilder
{
public static IResourceBuilder<T> Create<T>(T resource, IDistributedApplicationBuilder distributedApplicationBuilder) where T : IResource
{
return new ResourceBuilder<T>(resource, distributedApplicationBuilder);
}
}

private class ResourceBuilder<T>(T resource, IDistributedApplicationBuilder distributedApplicationBuilder) : IResourceBuilder<T> where T : IResource
{
public IDistributedApplicationBuilder ApplicationBuilder { get; } = distributedApplicationBuilder;

public T Resource { get; } = resource;

public IResourceBuilder<T> WithAnnotation<TAnnotation>(TAnnotation annotation, ResourceAnnotationMutationBehavior behavior = ResourceAnnotationMutationBehavior.Append) where TAnnotation : IResourceAnnotation
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0-preview.3.24210.17" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.0.0-preview.3.24210.17" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.0.0-preview.3.24210.17" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="9.0.0-preview.3.24210.17" />
<PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />
Expand All @@ -21,6 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\..\MPM-Betting.Api\MPM-Betting.Api.csproj" />
<ProjectReference Include="..\..\MPM-Betting.Blazor\MPM-Betting.Blazor.csproj" />
<ProjectReference Include="..\..\MPM-Betting.DbManager\MPM-Betting.DbManager.csproj" />
<ProjectReference Include="..\..\MPM-Betting.Functions\MPM-Betting.Functions.csproj" />
</ItemGroup>

Expand Down
34 changes: 34 additions & 0 deletions MPM-Betting.Aspire/MPM-Betting.Aspire.AppHost/MailDevResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace MPM_Betting.Aspire.AppHost;

// based on https://github.com/dotnet/docs-aspire/pull/755/files#conversations-menu

public class MailDevResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
private EndpointReference? _smtpReference;
private EndpointReference SmtpEndpoint => _smtpReference ??= new EndpointReference(this, "smtp");

public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create(
$"smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)}"
);
}

public static class MailDevContainerImageTags
{
public const string Registry = "docker.io";
public const string Image = "maildev/maildev";
public const string Tag = "2.0.2";
}

public static class MailDevResourceBuilderExtensions
{
public static IResourceBuilder<MailDevResource> AddMailDev(this IDistributedApplicationBuilder builder, string name, int? httpPort = null, int? smtpPort = null)
{
var resource = new MailDevResource(name);
return builder.AddResource(resource)
.WithImage(MailDevContainerImageTags.Image)
.WithImageRegistry(MailDevContainerImageTags.Registry)
.WithImageTag(MailDevContainerImageTags.Tag)
.WithHttpEndpoint(1080, httpPort, name: "http")
.WithEndpoint(1025, smtpPort, name: "smtp");
}
}
15 changes: 11 additions & 4 deletions MPM-Betting.Aspire/MPM-Betting.Aspire.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
.WithPersistence()
.WithDataVolume();

var sql = builder.AddSqlServer("sql")
.WithDataVolume()
var sql = builder.AddPostgres("sql", password: builder.CreateStablePassword("MPM-Betting-Password"))
//.WithDataVolume() // note gabriel: me too scared to touch for now....
.AddDatabase("MPM-Betting");

if (builder.ExecutionContext.IsPublishMode)
Expand All @@ -51,16 +51,23 @@
}
else
{
var mailDev = builder.AddMailDev("maildev");

var api = builder.AddProjectWithDotnetWatch<MPM_Betting_Api>("api")
.WithReference(sql)
.WithReference(redis);
.WithReference(redis)
.WithReference(mailDev);

var blazor = builder.AddProjectWithDotnetWatch<MPM_Betting_Blazor>("blazor")
.WithEnvironment("services__api__http__0", "http://localhost:5241")
.WithReference(redis)
.WithReference(sql);
.WithReference(sql)
.WithReference(mailDev);
}

var dbManager = builder.AddProject<MPM_Betting_DbManager>("dbmanager")
.WithReference(sql);


builder.Build().Run();

Expand Down
10 changes: 5 additions & 5 deletions MPM-Betting.Blazor.Components/ExampleJsInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,25 @@ namespace MPM_Betting.Blazor.Components;

public class ExampleJsInterop : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
private readonly Lazy<Task<IJSObjectReference>> m_ModuleTask;

public ExampleJsInterop(IJSRuntime jsRuntime)
{
moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
m_ModuleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./_content/MPM_Betting.Blazor.Components/exampleJsInterop.js").AsTask());
}

public async ValueTask<string> Prompt(string message)
{
var module = await moduleTask.Value;
var module = await m_ModuleTask.Value;
return await module.InvokeAsync<string>("showPrompt", message);
}

public async ValueTask DisposeAsync()
{
if (moduleTask.IsValueCreated)
if (m_ModuleTask.IsValueCreated)
{
var module = await moduleTask.Value;
var module = await m_ModuleTask.Value;
await module.DisposeAsync();
}
}
Expand Down
Loading