feat: implement user registration and update SASL authentication handling

This commit is contained in:
HueByte
2026-02-23 14:51:04 +01:00
parent 28c4b2993f
commit 27a25b1b43
8 changed files with 64 additions and 11 deletions
+3
View File
@@ -4,6 +4,9 @@ Release history for EchoHub.
## Releases ## Releases
- [v0.2.8](v0.2.8.md) - Docker Support, IRC Account Creation & BOM Fix
- [v0.2.7](v0.2.7.md) - User List Fix & Terminal.Gui NuGet Migration
- [v0.2.6](v0.2.6.md) - Major Refactoring & Code Organization
- [v0.2.5](v0.2.5.md) - Session Persistence, Auto-Updates, Audio & Transparent Theme - [v0.2.5](v0.2.5.md) - Session Persistence, Auto-Updates, Audio & Transparent Theme
- [v0.2.4](v0.2.4.md) - E2E Message Encryption - [v0.2.4](v0.2.4.md) - E2E Message Encryption
- [v0.2.3](v0.2.3.md) - Moderation, Embeds & UI Overhaul - [v0.2.3](v0.2.3.md) - Moderation, Embeds & UI Overhaul
+2
View File
@@ -1,5 +1,7 @@
- name: Overview - name: Overview
href: index.md href: index.md
- name: v0.2.8
href: v0.2.8.md
- name: v0.2.7 - name: v0.2.7
href: v0.2.7.md href: v0.2.7.md
- name: v0.2.6 - name: v0.2.6
-8
View File
@@ -4,14 +4,6 @@
- Fix user list empty on initial connect — `FetchAndUpdateOnlineUsers` was called before `InvokeUI` set the current channel, causing an early return - Fix user list empty on initial connect — `FetchAndUpdateOnlineUsers` was called before `InvokeUI` set the current channel, causing an early return
## New Features
- Add Docker support for EchoHub.Server — `docker compose up -d` for easy self-hosting with persistent volume for database, uploads, and logs
## CI
- Add Docker workflow — builds and pushes multi-arch (`amd64`/`arm64`) server image to GHCR on release
## Infrastructure ## Infrastructure
- Switch Terminal.Gui from local fork submodule back to NuGet package (`2.0.0-develop.5039`) — transparent color PR merged upstream - Switch Terminal.Gui from local fork submodule back to NuGet package (`2.0.0-develop.5039`) — transparent color PR merged upstream
+15
View File
@@ -0,0 +1,15 @@
# v0.2.8
## Bug Fixes
- Fix IRC gateway sending UTF-8 BOM on first message, breaking CAP negotiation and SASL auth for all clients
- Handle `AUTHENTICATE *` (SASL abort) instead of crashing on invalid base64
## New Features
- Add Docker support for EchoHub.Server — `docker compose up -d` for easy self-hosting with persistent volume for database, uploads, and logs
- IRC account creation — connecting with a new username auto-registers the account (PASS and SASL PLAIN)
## CI
- Add Docker workflow — builds and pushes multi-arch (`amd64`/`arm64`) server image to GHCR on release
+1 -1
View File
@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.2.7</Version> <Version>0.2.8</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
@@ -29,4 +29,5 @@ public interface IChatService
Task<UserProfileDto?> GetUserProfileAsync(string username); Task<UserProfileDto?> GetUserProfileAsync(string username);
Task<List<string>> GetChannelsForUserAsync(string username); Task<List<string>> GetChannelsForUserAsync(string username);
Task<(Guid UserId, string Username)?> AuthenticateUserAsync(string username, string password); Task<(Guid UserId, string Username)?> AuthenticateUserAsync(string username, string password);
Task<(Guid UserId, string Username)?> RegisterUserAsync(string username, string password);
} }
+10 -2
View File
@@ -170,9 +170,13 @@ public sealed class IrcCommandHandler
var result = await _chatService.AuthenticateUserAsync(username, password); var result = await _chatService.AuthenticateUserAsync(username, password);
// Auth failed — try registering a new account
if (result is null)
result = await _chatService.RegisterUserAsync(username, password);
if (result is null) if (result is null)
{ {
_logger.LogWarning("SASL auth failed for user '{Username}' (connection {Id})", _logger.LogWarning("SASL auth/register failed for user '{Username}' (connection {Id})",
username, _conn.ConnectionId); username, _conn.ConnectionId);
await _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_SASLFAIL, await _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_SASLFAIL,
":SASL authentication failed"); ":SASL authentication failed");
@@ -286,10 +290,14 @@ public sealed class IrcCommandHandler
var result = await _chatService.AuthenticateUserAsync(_conn.Nickname!, _conn.Password); var result = await _chatService.AuthenticateUserAsync(_conn.Nickname!, _conn.Password);
// Auth failed — try registering a new account
if (result is null)
result = await _chatService.RegisterUserAsync(_conn.Nickname!, _conn.Password);
if (result is null) if (result is null)
{ {
await _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_PASSWDMISMATCH, await _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_PASSWDMISMATCH,
":Password incorrect or account not found. Register via the EchoHub client first."); ":Password incorrect.");
await _conn.SendAsync("ERROR :Authentication failed"); await _conn.SendAsync("ERROR :Authentication failed");
return; return;
} }
@@ -339,6 +339,38 @@ public class ChatService : IChatService
return (user.Id, user.Username); return (user.Id, user.Username);
} }
public async Task<(Guid UserId, string Username)?> RegisterUserAsync(string username, string password)
{
username = username.ToLowerInvariant().Trim();
if (!ValidationConstants.UsernameRegex().IsMatch(username))
return null;
if (password.Length < 6 || password.Length > ValidationConstants.MaxPasswordLength)
return null;
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<EchoHubDbContext>();
if (await db.Users.AnyAsync(u => u.Username == username))
return null;
var isFirstUser = !await db.Users.AnyAsync();
var user = new User
{
Id = Guid.NewGuid(),
Username = username,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
Role = isFirstUser ? ServerRole.Owner : ServerRole.Member,
};
db.Users.Add(user);
await db.SaveChangesAsync();
return (user.Id, user.Username);
}
/// <summary> /// <summary>
/// Collapse consecutive newlines and cap total line count to prevent newline spam. /// Collapse consecutive newlines and cap total line count to prevent newline spam.
/// </summary> /// </summary>