feat: Add Create Channel dialog and related events

- Introduced CreateChannelDialog for user to create new channels with name and topic.
- Added OnCreateChannelRequested event in MainWindow for channel creation.
- Enhanced EchoHubConnection with OnReconnected event for connection state handling.
- Updated EchoHubDbContext to use application base directory for SQLite database path.
- Integrated Serilog for logging in EchoHub.Server.
- Refactored database initialization and migration logic into DatabaseSetup class.
- Implemented FirstRunSetup to ensure appsettings.json and generate JWT secret if needed.
- Updated appsettings files to configure Serilog logging.
- Enhanced error handling and logging in ChatHub methods for better traceability.
This commit is contained in:
HueByte
2026-02-19 04:36:40 +01:00
parent ebc8fffec8
commit c9c76c43fa
15 changed files with 1191 additions and 962 deletions
+2 -1
View File
@@ -15,7 +15,8 @@ public class EchoHubDbContext(DbContextOptions<EchoHubDbContext> options) : DbCo
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlite("Data Source=echohub.db");
var dbPath = Path.Combine(AppContext.BaseDirectory, "echohub.db");
optionsBuilder.UseSqlite($"Data Source={dbPath}");
}
}
+1
View File
@@ -12,6 +12,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
+252 -185
View File
@@ -24,246 +24,313 @@ public class ChatHub(EchoHubDbContext db, ILogger<ChatHub> logger, PresenceTrack
public override async Task OnConnectedAsync()
{
presenceTracker.UserConnected(Context.ConnectionId, CurrentUserId, CurrentUsername);
var user = await db.Users.FindAsync(CurrentUserId);
if (user is not null)
try
{
user.LastSeenAt = DateTimeOffset.UtcNow;
user.Status = UserStatus.Online;
await db.SaveChangesAsync();
presenceTracker.UserConnected(Context.ConnectionId, CurrentUserId, CurrentUsername);
var user = await db.Users.FindAsync(CurrentUserId);
if (user is not null)
{
user.LastSeenAt = DateTimeOffset.UtcNow;
user.Status = UserStatus.Online;
await db.SaveChangesAsync();
}
await base.OnConnectedAsync();
logger.LogInformation("{User} connected (ConnectionId: {ConnectionId})", CurrentUsername, Context.ConnectionId);
}
catch (Exception ex)
{
logger.LogError(ex, "Error in OnConnectedAsync for {ConnectionId}", Context.ConnectionId);
throw;
}
await base.OnConnectedAsync();
logger.LogInformation("{User} connected (ConnectionId: {ConnectionId})", CurrentUsername, Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var preDisconnectUsername = Context.User?.FindFirstValue("username");
var channelsBeforeDisconnect = preDisconnectUsername is not null
? presenceTracker.GetChannelsForUser(preDisconnectUsername)
: [];
var username = presenceTracker.UserDisconnected(Context.ConnectionId);
if (username is not null && !presenceTracker.IsOnline(username))
try
{
var user = await db.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user is not null)
var preDisconnectUsername = Context.User?.FindFirstValue("username");
var channelsBeforeDisconnect = preDisconnectUsername is not null
? presenceTracker.GetChannelsForUser(preDisconnectUsername)
: [];
var username = presenceTracker.UserDisconnected(Context.ConnectionId);
if (username is not null && !presenceTracker.IsOnline(username))
{
user.LastSeenAt = DateTimeOffset.UtcNow;
user.Status = UserStatus.Invisible;
await db.SaveChangesAsync();
var presence = new UserPresenceDto(
username,
user.DisplayName,
user.NicknameColor,
UserStatus.Invisible,
user.StatusMessage);
foreach (var channel in channelsBeforeDisconnect)
var user = await db.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user is not null)
{
await Clients.Group(channel).UserStatusChanged(presence);
user.LastSeenAt = DateTimeOffset.UtcNow;
user.Status = UserStatus.Invisible;
await db.SaveChangesAsync();
var presence = new UserPresenceDto(
username,
user.DisplayName,
user.NicknameColor,
UserStatus.Invisible,
user.StatusMessage);
foreach (var channel in channelsBeforeDisconnect)
{
await Clients.Group(channel).UserStatusChanged(presence);
}
}
}
await base.OnDisconnectedAsync(exception);
logger.LogInformation("{User} disconnected (ConnectionId: {ConnectionId})", username ?? "Unknown", Context.ConnectionId);
}
catch (Exception ex)
{
logger.LogError(ex, "Error in OnDisconnectedAsync for {ConnectionId}", Context.ConnectionId);
throw;
}
await base.OnDisconnectedAsync(exception);
logger.LogInformation("{User} disconnected (ConnectionId: {ConnectionId})", username ?? "Unknown", Context.ConnectionId);
}
public async Task<List<MessageDto>> JoinChannel(string channelName)
{
channelName = channelName.ToLowerInvariant().Trim();
if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
try
{
await Clients.Caller.Error("Invalid channel name. Use 2-100 characters: letters, digits, underscores, or hyphens.");
channelName = channelName.ToLowerInvariant().Trim();
if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
{
await Clients.Caller.Error("Invalid channel name. Use 2-100 characters: letters, digits, underscores, or hyphens.");
return [];
}
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
if (channel is null)
{
await Clients.Caller.Error($"Channel '{channelName}' does not exist. Create it first via the channel list.");
return [];
}
presenceTracker.JoinChannel(CurrentUsername, channelName);
await Groups.AddToGroupAsync(Context.ConnectionId, channelName);
await Clients.OthersInGroup(channelName).UserJoined(channelName, CurrentUsername);
logger.LogInformation("{User} joined channel '{Channel}'", CurrentUsername, channelName);
var history = await GetChannelHistory(channelName, HubConstants.DefaultHistoryCount);
return history;
}
catch (Exception ex)
{
logger.LogError(ex, "Error joining channel '{Channel}' for {User}", channelName, CurrentUsername);
await Clients.Caller.Error($"Failed to join channel: {ex.Message}");
return [];
}
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
if (channel is null)
{
await Clients.Caller.Error($"Channel '{channelName}' does not exist. Create it first via the channel list.");
return [];
}
presenceTracker.JoinChannel(CurrentUsername, channelName);
await Groups.AddToGroupAsync(Context.ConnectionId, channelName);
await Clients.OthersInGroup(channelName).UserJoined(channelName, CurrentUsername);
logger.LogInformation("{User} joined channel '{Channel}'", CurrentUsername, channelName);
var history = await GetChannelHistory(channelName, HubConstants.DefaultHistoryCount);
return history;
}
public async Task LeaveChannel(string channelName)
{
channelName = channelName.ToLowerInvariant().Trim();
try
{
channelName = channelName.ToLowerInvariant().Trim();
presenceTracker.LeaveChannel(CurrentUsername, channelName);
presenceTracker.LeaveChannel(CurrentUsername, channelName);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, channelName);
await Clients.OthersInGroup(channelName).UserLeft(channelName, CurrentUsername);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, channelName);
await Clients.OthersInGroup(channelName).UserLeft(channelName, CurrentUsername);
logger.LogInformation("{User} left channel '{Channel}'", CurrentUsername, channelName);
logger.LogInformation("{User} left channel '{Channel}'", CurrentUsername, channelName);
}
catch (Exception ex)
{
logger.LogError(ex, "Error leaving channel '{Channel}' for {User}", channelName, CurrentUsername);
await Clients.Caller.Error($"Failed to leave channel: {ex.Message}");
}
}
public async Task SendMessage(string channelName, string content)
{
channelName = channelName.ToLowerInvariant().Trim();
if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
try
{
await Clients.Caller.Error("Invalid channel name.");
return;
channelName = channelName.ToLowerInvariant().Trim();
if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
{
await Clients.Caller.Error("Invalid channel name.");
return;
}
if (string.IsNullOrWhiteSpace(content))
{
await Clients.Caller.Error("Message content cannot be empty.");
return;
}
if (content.Length > HubConstants.MaxMessageLength)
{
await Clients.Caller.Error($"Message exceeds maximum length of {HubConstants.MaxMessageLength} characters.");
return;
}
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
if (channel is null)
{
await Clients.Caller.Error($"Channel '{channelName}' does not exist.");
return;
}
var sender = await db.Users.FindAsync(CurrentUserId);
var message = new Message
{
Id = Guid.NewGuid(),
Content = content,
Type = MessageType.Text,
SentAt = DateTimeOffset.UtcNow,
ChannelId = channel.Id,
SenderUserId = CurrentUserId,
SenderUsername = CurrentUsername,
};
db.Messages.Add(message);
await db.SaveChangesAsync();
var messageDto = new MessageDto(
message.Id,
message.Content,
message.SenderUsername,
sender?.NicknameColor,
channelName,
MessageType.Text,
null,
null,
message.SentAt);
await Clients.Group(channelName).ReceiveMessage(messageDto);
logger.LogDebug("{User} sent message in '{Channel}'", CurrentUsername, channelName);
}
if (string.IsNullOrWhiteSpace(content))
catch (Exception ex)
{
await Clients.Caller.Error("Message content cannot be empty.");
return;
logger.LogError(ex, "Error sending message in '{Channel}' for {User}", channelName, CurrentUsername);
await Clients.Caller.Error($"Failed to send message: {ex.Message}");
}
if (content.Length > HubConstants.MaxMessageLength)
{
await Clients.Caller.Error($"Message exceeds maximum length of {HubConstants.MaxMessageLength} characters.");
return;
}
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
if (channel is null)
{
await Clients.Caller.Error($"Channel '{channelName}' does not exist.");
return;
}
var sender = await db.Users.FindAsync(CurrentUserId);
var message = new Message
{
Id = Guid.NewGuid(),
Content = content,
Type = MessageType.Text,
SentAt = DateTimeOffset.UtcNow,
ChannelId = channel.Id,
SenderUserId = CurrentUserId,
SenderUsername = CurrentUsername,
};
db.Messages.Add(message);
await db.SaveChangesAsync();
var messageDto = new MessageDto(
message.Id,
message.Content,
message.SenderUsername,
sender?.NicknameColor,
channelName,
MessageType.Text,
null,
null,
message.SentAt);
await Clients.Group(channelName).ReceiveMessage(messageDto);
logger.LogDebug("{User} sent message in '{Channel}'", CurrentUsername, channelName);
}
public async Task<List<MessageDto>> GetChannelHistory(string channelName, int count = HubConstants.DefaultHistoryCount)
{
channelName = channelName.ToLowerInvariant().Trim();
count = Math.Clamp(count, 1, ValidationConstants.MaxHistoryCount);
try
{
channelName = channelName.ToLowerInvariant().Trim();
count = Math.Clamp(count, 1, ValidationConstants.MaxHistoryCount);
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
if (channel is null)
if (channel is null)
return [];
var messages = await db.Messages
.Where(m => m.ChannelId == channel.Id)
.OrderByDescending(m => m.SentAt)
.Take(count)
.Join(db.Users,
m => m.SenderUserId,
u => u.Id,
(m, u) => new MessageDto(
m.Id,
m.Content,
m.SenderUsername,
u.NicknameColor,
channelName,
m.Type,
m.AttachmentUrl,
m.AttachmentFileName,
m.SentAt))
.ToListAsync();
messages.Reverse();
return messages;
}
catch (Exception ex)
{
logger.LogError(ex, "Error fetching history for '{Channel}'", channelName);
await Clients.Caller.Error($"Failed to load history: {ex.Message}");
return [];
var messages = await db.Messages
.Where(m => m.ChannelId == channel.Id)
.OrderByDescending(m => m.SentAt)
.Take(count)
.Join(db.Users,
m => m.SenderUserId,
u => u.Id,
(m, u) => new MessageDto(
m.Id,
m.Content,
m.SenderUsername,
u.NicknameColor,
channelName,
m.Type,
m.AttachmentUrl,
m.AttachmentFileName,
m.SentAt))
.ToListAsync();
messages.Reverse();
return messages;
}
}
public async Task UpdateStatus(UserStatus status, string? statusMessage)
{
if (statusMessage is not null && statusMessage.Length > ValidationConstants.MaxStatusMessageLength)
try
{
await Clients.Caller.Error($"Status message must not exceed {ValidationConstants.MaxStatusMessageLength} characters.");
return;
if (statusMessage is not null && statusMessage.Length > ValidationConstants.MaxStatusMessageLength)
{
await Clients.Caller.Error($"Status message must not exceed {ValidationConstants.MaxStatusMessageLength} characters.");
return;
}
var user = await db.Users.FindAsync(CurrentUserId);
if (user is null)
{
await Clients.Caller.Error("User not found.");
return;
}
user.Status = status;
user.StatusMessage = statusMessage?.Trim();
user.LastSeenAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync();
var presence = new UserPresenceDto(
user.Username,
user.DisplayName,
user.NicknameColor,
status,
statusMessage);
var channels = presenceTracker.GetChannelsForUser(CurrentUsername);
foreach (var channel in channels)
{
await Clients.Group(channel).UserStatusChanged(presence);
}
}
var user = await db.Users.FindAsync(CurrentUserId);
if (user is null)
catch (Exception ex)
{
await Clients.Caller.Error("User not found.");
return;
}
user.Status = status;
user.StatusMessage = statusMessage?.Trim();
user.LastSeenAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync();
var presence = new UserPresenceDto(
user.Username,
user.DisplayName,
user.NicknameColor,
status,
statusMessage);
var channels = presenceTracker.GetChannelsForUser(CurrentUsername);
foreach (var channel in channels)
{
await Clients.Group(channel).UserStatusChanged(presence);
logger.LogError(ex, "Error updating status for {User}", CurrentUsername);
await Clients.Caller.Error($"Failed to update status: {ex.Message}");
}
}
public async Task<List<UserPresenceDto>> GetOnlineUsers(string channelName)
{
channelName = channelName.ToLowerInvariant().Trim();
try
{
channelName = channelName.ToLowerInvariant().Trim();
var onlineUsernames = presenceTracker.GetOnlineUsersInChannel(channelName);
var onlineUsernames = presenceTracker.GetOnlineUsersInChannel(channelName);
var users = await db.Users
.Where(u => onlineUsernames.Contains(u.Username))
.Select(u => new UserPresenceDto(
u.Username,
u.DisplayName,
u.NicknameColor,
u.Status,
u.StatusMessage))
.ToListAsync();
var users = await db.Users
.Where(u => onlineUsernames.Contains(u.Username))
.Select(u => new UserPresenceDto(
u.Username,
u.DisplayName,
u.NicknameColor,
u.Status,
u.StatusMessage))
.ToListAsync();
return users;
return users;
}
catch (Exception ex)
{
logger.LogError(ex, "Error listing users in '{Channel}'", channelName);
await Clients.Caller.Error($"Failed to list users: {ex.Message}");
return [];
}
}
}
+13 -77
View File
@@ -1,21 +1,31 @@
using System.Text;
using System.Threading.RateLimiting;
using EchoHub.Core.Constants;
using Microsoft.AspNetCore.RateLimiting;
using EchoHub.Core.Models;
using EchoHub.Server.Auth;
using EchoHub.Server.Data;
using EchoHub.Server.Hubs;
using EchoHub.Server.Services;
using EchoHub.Server.Setup;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Serilog;
// ── First-run setup ──────────────────────────────────────────────────────────
FirstRunSetup.EnsureAppSettings();
var builder = WebApplication.CreateBuilder(args);
// ── Serilog ──────────────────────────────────────────────────────────────────
builder.Host.UseSerilog((context, config) =>
config.ReadFrom.Configuration(context.Configuration));
// ── SQLite + EF Core ──────────────────────────────────────────────────────────
var defaultDbPath = Path.Combine(AppContext.BaseDirectory, "echohub.db");
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? "Data Source=echohub.db";
?? $"Data Source={defaultDbPath}";
builder.Services.AddDbContext<EchoHubDbContext>(options =>
options.UseSqlite(connectionString));
@@ -127,81 +137,7 @@ builder.Services.AddCors(options =>
var app = builder.Build();
// ── Database initialization ───────────────────────────────────────────────────
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<EchoHubDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
// If the DB was created by EnsureCreated (no __EFMigrationsHistory table),
// back it up and recreate so MigrateAsync can manage the schema properly.
if (await db.Database.CanConnectAsync())
{
var conn = db.Database.GetDbConnection();
await conn.OpenAsync();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory'";
var hasMigrationTable = Convert.ToInt64(await cmd.ExecuteScalarAsync()) > 0;
if (!hasMigrationTable)
{
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Users'";
var hasLegacyTables = Convert.ToInt64(await cmd.ExecuteScalarAsync()) > 0;
if (hasLegacyTables)
{
var dbPath = conn.DataSource;
await conn.CloseAsync();
// Back up the legacy DB file before deleting
if (!string.IsNullOrEmpty(dbPath) && File.Exists(dbPath))
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupPath = $"{dbPath}.legacy_{timestamp}";
File.Copy(dbPath, backupPath, overwrite: false);
logger.LogWarning("Legacy database backed up to '{BackupPath}'.", backupPath);
}
await db.Database.EnsureDeletedAsync();
logger.LogWarning("Legacy database removed. A new database will be created with migration support.");
}
else
{
await conn.CloseAsync();
}
}
else
{
await conn.CloseAsync();
}
}
await db.Database.MigrateAsync();
logger.LogInformation("Database migrated successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Database migration failed.");
throw;
}
// Seed the default channel if it doesn't exist
if (!await db.Channels.AnyAsync(c => c.Name == HubConstants.DefaultChannel))
{
db.Channels.Add(new Channel
{
Id = Guid.NewGuid(),
Name = HubConstants.DefaultChannel,
Topic = "General discussion",
CreatedByUserId = Guid.Empty,
});
await db.SaveChangesAsync();
logger.LogInformation("Default channel '{Channel}' created.", HubConstants.DefaultChannel);
}
}
await DatabaseSetup.InitializeAsync(app.Services);
// ── Middleware ─────────────────────────────────────────────────────────────────
app.UseCors();
@@ -6,7 +6,8 @@ public class FileStorageService
public FileStorageService(IConfiguration configuration)
{
_storagePath = configuration["Storage:Path"] ?? "./uploads";
_storagePath = configuration["Storage:Path"]
?? Path.Combine(AppContext.BaseDirectory, "uploads");
if (!Directory.Exists(_storagePath))
{
+90
View File
@@ -0,0 +1,90 @@
using EchoHub.Core.Constants;
using EchoHub.Core.Models;
using EchoHub.Server.Data;
using Microsoft.EntityFrameworkCore;
namespace EchoHub.Server.Setup;
public static class DatabaseSetup
{
public static async Task InitializeAsync(IServiceProvider services)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<EchoHubDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>()
.CreateLogger("EchoHub.Server.Setup.DatabaseSetup");
await MigrateAsync(db, logger);
await SeedDefaultChannelAsync(db, logger);
}
private static async Task MigrateAsync(EchoHubDbContext db, ILogger logger)
{
try
{
if (await db.Database.CanConnectAsync())
await HandleLegacyDatabaseAsync(db, logger);
await db.Database.MigrateAsync();
logger.LogInformation("Database migrated successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Database migration failed.");
throw;
}
}
private static async Task HandleLegacyDatabaseAsync(EchoHubDbContext db, ILogger logger)
{
var conn = db.Database.GetDbConnection();
await conn.OpenAsync();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory'";
var hasMigrationTable = Convert.ToInt64(await cmd.ExecuteScalarAsync()) > 0;
if (!hasMigrationTable)
{
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Users'";
var hasLegacyTables = Convert.ToInt64(await cmd.ExecuteScalarAsync()) > 0;
if (hasLegacyTables)
{
var dbPath = conn.DataSource;
await conn.CloseAsync();
if (!string.IsNullOrEmpty(dbPath) && File.Exists(dbPath))
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupPath = $"{dbPath}.legacy_{timestamp}";
File.Copy(dbPath, backupPath, overwrite: false);
logger.LogWarning("Legacy database backed up to '{BackupPath}'.", backupPath);
}
await db.Database.EnsureDeletedAsync();
logger.LogWarning("Legacy database removed. A new database will be created with migration support.");
return;
}
}
await conn.CloseAsync();
}
private static async Task SeedDefaultChannelAsync(EchoHubDbContext db, ILogger logger)
{
if (await db.Channels.AnyAsync(c => c.Name == HubConstants.DefaultChannel))
return;
db.Channels.Add(new Channel
{
Id = Guid.NewGuid(),
Name = HubConstants.DefaultChannel,
Topic = "General discussion",
CreatedByUserId = Guid.Empty,
});
await db.SaveChangesAsync();
logger.LogInformation("Default channel '{Channel}' created.", HubConstants.DefaultChannel);
}
}
+48
View File
@@ -0,0 +1,48 @@
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace EchoHub.Server.Setup;
public static class FirstRunSetup
{
public static void EnsureAppSettings()
{
var contentRoot = Directory.GetCurrentDirectory();
var settingsPath = Path.Combine(contentRoot, "appsettings.json");
var examplePath = Path.Combine(contentRoot, "appsettings.example.json");
if (!File.Exists(settingsPath) && File.Exists(examplePath))
{
File.Copy(examplePath, settingsPath);
Console.WriteLine("Created appsettings.json from example config.");
}
if (!File.Exists(settingsPath))
return;
EnsureJwtSecret(settingsPath);
}
private static void EnsureJwtSecret(string settingsPath)
{
var json = File.ReadAllText(settingsPath);
var root = JsonNode.Parse(json, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip });
if (root is null)
return;
var currentSecret = root["Jwt"]?["Secret"]?.GetValue<string>();
if (!string.IsNullOrEmpty(currentSecret) && !currentSecret.StartsWith("CHANGE_ME"))
return;
var secret = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48));
root["Jwt"] ??= new JsonObject();
root["Jwt"]!["Secret"] = secret;
var writeOptions = new JsonSerializerOptions { WriteIndented = true };
File.WriteAllText(settingsPath, root.ToJsonString(writeOptions));
Console.WriteLine("Generated new JWT secret in appsettings.json.");
}
}
@@ -1,8 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"System": "Warning"
}
}
}
}
+21 -5
View File
@@ -11,12 +11,28 @@
"Name": "My EchoHub Server",
"Description": "A self-hosted EchoHub chat server"
},
"Logging": {
"LogLevel": {
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "logs/echohub-server-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 14,
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
]
},
"AllowedHosts": "*"
}