Skip to content
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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
Server__Name=My EchoHub Server
Server__Description=A self-hosted EchoHub chat server
Server__PublicServer=false
# Server__PublicHost=echohub.example.com
# Hostnames advertised to the EchoHubSpace directory. Index per entry.
# Server__PublicHosts__0=echohub.example.com
# Server__PublicHosts__1=alias.example.com
# Topic tags surfaced in the EchoHubSpace browser. Index per entry.
# Server__Tags__0=community
# Server__Tags__1=gaming
# Server__Admins__0=adminUsername

# ── JWT ──────────────────────────────────────────────────────────────
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/release-checklist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Release Checklist

on:
pull_request:
branches: [master]
workflow_dispatch:

jobs:
release-checklist:
name: Release Checklist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Extract version
id: version
run: |
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
if [ -z "$VERSION" ]; then
echo "::error file=src/Directory.Build.props::Could not read version from Directory.Build.props"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"

- name: Check version was bumped from master
run: |
BRANCH_VERSION="${{ steps.version.outputs.version }}"
git fetch origin master --depth=1
MASTER_VERSION=$(git show origin/master:src/Directory.Build.props | grep -oP '(?<=<Version>)[^<]+')
echo "Branch: $BRANCH_VERSION | Master: $MASTER_VERSION"
if [ "$BRANCH_VERSION" = "$MASTER_VERSION" ]; then
echo "::error file=src/Directory.Build.props::Version $BRANCH_VERSION was not bumped from master. Update <Version> in src/Directory.Build.props."
exit 1
fi

- name: Check changelog file exists
run: |
VERSION="${{ steps.version.outputs.version }}"
FILE="docs/changelog/v${VERSION}.md"
if [ ! -f "$FILE" ]; then
echo "::error::Missing changelog file: $FILE"
exit 1
fi
echo "Found: $FILE"

- name: Check changelog TOC
run: |
VERSION="${{ steps.version.outputs.version }}"
if ! grep -q "v${VERSION}.md" docs/changelog/toc.yml; then
echo "::error file=docs/changelog/toc.yml::v${VERSION} not found in changelog TOC. Add it to docs/changelog/toc.yml."
exit 1
fi
if ! grep -q "v${VERSION}" docs/changelog/index.md; then
echo "::error file=docs/changelog/index.md::v${VERSION} not found in changelog index. Add it to docs/changelog/index.md."
exit 1
fi
echo "toc.yml and index.md: OK"
2 changes: 1 addition & 1 deletion docs/articles/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/inst
To install a specific version or to a custom directory:

```bash
curl -sSfL .../install.sh | sh -s -- --version 0.2.10
curl -sSfL .../install.sh | sh -s -- --version 0.2.11
curl -sSfL .../install.sh | sh -s -- --install-dir /opt/echohub
```

Expand Down
1 change: 1 addition & 0 deletions docs/changelog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Release history for EchoHub.

## Releases

- [v0.2.11](v0.2.11.md) - EchoHubSpace Auth, Live Directory Updates & Server Browser Metadata
- [v0.2.10](v0.2.10.md) - Command Palette, Infinite History Scroll & Auto-Updater Fixes
- [v0.2.9](v0.2.9.md) - Install Script & Chocolatey Fixes
- [v0.2.8](v0.2.8.md) - Docker Support, IRC Account Creation & BOM Fix
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog/toc.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
- name: Overview
href: index.md
- name: v0.2.11
href: v0.2.11.md
- name: v0.2.10
href: v0.2.10.md
- name: v0.2.9
Expand Down
23 changes: 23 additions & 0 deletions docs/changelog/v0.2.11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# v0.2.11

EchoHubSpace directory protocol overhaul: authenticated server registration with persistent claim tokens, near-real-time user-count updates, and richer server metadata (tags, multi-host, version). Coordinated cutover with the EchoHubSpace directory deploy.

## New Features

- EchoHubSpace claim-token authentication — the directory issues a per-server claim token on first registration, persisted atomically alongside the SQLite database (chmod 0600 on Unix). Subsequent reconnects authenticate with the token instead of relying on raw hostname-squatting protection. Token survives both client and directory restarts; lost tokens require an admin-side `DELETE /api/servers/{id}` on the directory to recover
- Server tags — public servers can advertise topic tags via the new `Server:Tags` config array, surfacing as filter facets in the EchoHubSpace browser
- Multi-host advertisement — a single server can register multiple hostnames (e.g. apex domain, IPv6, alias domains) by listing them in `Server:PublicHosts`. All hosts route to the same directory row
- Server version sent to directory — the EchoHubSpace browser shows what version each public server is running, pulled from the server's assembly informational version
- Operator-facing `GET /api/server/directory` endpoint (Admin role required) — returns `ServerId`, `IsRegistered`, `LastRegisteredAt`, `LastError`, and any `ConflictingHosts` for support tickets. Never exposes the claim token itself, only a `HasClaimToken` boolean

## Refactoring

- Replace 30s polling with event-driven directory updates — `PresenceTracker` now raises `UserCountChanged` only when the distinct user count actually changes (multi-tab/multi-connection users no longer trigger). `ServerDirectoryService` consumes via a single-slot `Channel<int>` (latest-wins coalesces bursts) with a 1-second min-interval throttle. Directory reflects user-count changes within ~1s instead of up to 30s stale
- Wrap directory hub responses in a `Response<T>` envelope with `IsSuccess`/`Data`/`Errors`/`Version` shape — protocol version is pinned client-side (currently `1.0`); mismatches trigger a permanent-failure stop with operator-facing log
- Stop attempting re-registration after permanent failures (`HostAlreadyClaimed`, `InvalidToken`, `HostConflict`, `InvalidInput`) — the directory no longer terminates the connection on these errors, so the client suppresses re-register on `Reconnected` to avoid tight retry loops. Operator must restart the server after fixing config

## Configuration

- **Breaking**: `Server:PublicHost` (string) renamed to `Server:PublicHosts` (string array). Public servers must update `appsettings.json` — single-host deployments use a one-element array
- New `Server:Tags` (string array) — defaults to empty
- New optional `Server:DirectoryClaimPath` — overrides the path of the persisted claim file. Defaults to a `directory-claim.json` next to the SQLite database. Treat the file as a secret; back it up alongside the database
2 changes: 1 addition & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ while [ $# -gt 0 ]; do
sed -n '2,8p' "$0" 2>/dev/null || true
echo ""
echo " curl -sSfL https://raw.githubusercontent.com/$REPO/master/scripts/install.sh | sh"
echo " curl ... | sh -s -- --version 0.2.10"
echo " curl ... | sh -s -- --version 0.2.11"
echo " curl ... | sh -s -- --install-dir /opt/echohub"
exit 0
;;
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.2.10</Version>
<Version>0.2.11</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
Expand Down
49 changes: 48 additions & 1 deletion src/EchoHub.Server/Controllers/ServerController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Security.Claims;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using EchoHub.Server.Data;
using EchoHub.Server.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
Expand All @@ -13,11 +16,13 @@ public class ServerController : ControllerBase
{
private readonly EchoHubDbContext _db;
private readonly IConfiguration _config;
private readonly DirectoryClaimStore _claimStore;

public ServerController(EchoHubDbContext db, IConfiguration config)
public ServerController(EchoHubDbContext db, IConfiguration config, DirectoryClaimStore claimStore)
{
_db = db;
_config = config;
_claimStore = claimStore;
}

[HttpGet("info")]
Expand Down Expand Up @@ -47,4 +52,46 @@ public IActionResult GetEncryptionKey()

return Ok(new EncryptionKeyResponse(key));
}

/// <summary>
/// Operator-facing view of the EchoHubSpace directory registration: ServerId for admin
/// support tickets, current registration state, and the last error/conflict if any.
/// Never exposes the claim token itself.
/// </summary>
[HttpGet("directory")]
[Authorize]
public async Task<IActionResult> GetDirectoryStatus()
{
var (_, error) = await GetCallerAsync(ServerRole.Admin);
if (error is not null) return error;

var status = _claimStore.Status;
var response = new
{
ServerId = _claimStore.ServerId,
HasClaimToken = _claimStore.ClaimToken is not null,
status.IsRegistered,
status.LastRegisteredAt,
status.LastError,
status.ConflictingHosts,
};

return Ok(response);
}

private async Task<(User? Caller, IActionResult? Error)> GetCallerAsync(ServerRole minimumRole)
{
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userIdClaim is null)
return (null, Unauthorized(new ErrorResponse("Authentication required.")));

var caller = await _db.Users.FindAsync(Guid.Parse(userIdClaim));
if (caller is null)
return (null, Unauthorized(new ErrorResponse("User not found.")));

if (caller.Role < minimumRole)
return (null, StatusCode(403, new ErrorResponse($"Requires {minimumRole} role or higher.")));

return (caller, null);
}
}
1 change: 1 addition & 0 deletions src/EchoHub.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
builder.Services.AddSingleton<ImageToAsciiService>();
builder.Services.AddSingleton<FileStorageService>();
builder.Services.AddSingleton<LinkEmbedService>();
builder.Services.AddSingleton<DirectoryClaimStore>();
builder.Services.AddHostedService<ServerDirectoryService>();
builder.Services.AddHostedService<FileCleanupService>();
builder.Services.AddHostedService<MuteExpirationService>();
Expand Down
Loading
Loading