docs: Update documentation for 145 files

Generated by AurionDocs
Job ID: 934f8c39-8082-4942-8d17-72ed8f5f8d50
Source commit: 40aea9a
This commit is contained in:
Hue
2026-07-23 11:44:20 +02:00
parent 40aea9a04b
commit 607217b314
144 changed files with 5098 additions and 6498 deletions
@@ -13,15 +13,15 @@
- [EnsureSystemChannelAsync](#ensuresystemchannelasync)
- [GetChannelByNameAsync](#getchannelbynameasync)
- [GetChannelCryptoAsync](#getchannelcryptoasync)
- [GetChannelKeyEnvelopeAsync](#getchannelkeyenvelopeasync)
- [GetChannelListAsync](#getchannellistasync)
- [GetChannelMetaAsync](#getchannelmetaasync)
- [GetChannelTopicAsync](#getchanneltopicasync)
- [GetChannelsAsync](#getchannelsasync)
- [RekeyChannelAsync](#rekeychannelasync)
- [SetChannelPasswordAsync](#setchannelpasswordasync)
- [UpdateTopicAsync](#updatetopicasync)
- [ValidateChannelPassword](#validatechannelpassword)
- [GetChannelKeyEnvelopeAsync](#getchannelkeyenvelopeasync)
- [GetChannelTopicAsync](#getchanneltopicasync)
---
@@ -34,16 +34,15 @@ public class ChannelService : IChannelService
```
Manages server-side channel (room) operations: creation, deletion, listing and metadata, membership enforcement, password gating, and end-to-end encryption key envelopes. Reach for ChannelService when you need authoritative server logic that enforces channel rules and persists channel state (including password and E2E envelope handling), rather than making client-side assumptions or manipulating storage directly.
A high-level service that implements [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) and centralizes channel lifecycle and membership operations for the server: listing and paging channels (`GetChannelsAsync`), creating/updating/deleting channels (`CreateChannelAsync`, `UpdateTopicAsync`, `DeleteChannelAsync`), password and encryption envelope management (`SetChannelPasswordAsync`, `RekeyChannelAsync`, `GetChannelKeyEnvelopeAsync`), and retrieving channel metadata/crypto details (`GetChannelMetaAsync`, `GetChannelCryptoAsync`). Use `ChannelService` when you need the server-side orchestration for channel policies, membership checks and the authoritative source of channel metadata and cryptographic envelopes rather than calling lower-level storage or presence primitives directly.
## Remarks
ChannelService is the central server implementation of IChannelService and enforces policy around channels: who may see or join rooms, how passwords and encryption envelopes are handled, and how the system "log" room is treated differently from ordinary channels. It coordinates presence tracking, spam-throttling (via SpamGuard), and server logging to ensure operations such as channel creation, rekeying, and membership checks are performed consistently and safely. The service preserves the distinction between password-gated channels and end-to-end (E2E) encrypted channels by exposing separate operations for setting/clearing passwords and for rekeying the wrapped room key.
`ChannelService` acts as the application-level coordinator for channel-related concerns. It composes smaller services such as [`PresenceTracker`](PresenceTracker.cs.md), [`SpamGuard`](SpamGuard.cs.md), and [`ServerLogsService`](ServerLogs/ServerLogsService.cs.md), and enforces business rules (creator/admin permissions, role-gated system channels, creation throttling) so callers do not need to reimplement policy logic. The class is responsible for keeping cryptographic envelope state (`EncryptionSalt` / `WrappedRoomKey`) separate from message content keys and for exposing those envelopes through `GetChannelKeyEnvelopeAsync` and `GetChannelCryptoAsync` while preserving server-side metadata like sender identity counts and storage footprint.
## Notes
- SetChannelPasswordAsync is not applicable to end-to-end encrypted channels; encrypted rooms change access by rekeying via RekeyChannelAsync so the room key envelope remains consistent. Clearing a password is performed by passing null as the password parameter.
- RekeyChannelAsync is restricted to the channel creator: administrators who do not know the current passphrase cannot rekey a channel on the creator's behalf.
- The system "live log" room is role-gated and its name is reserved even when the feature is disabled; this prevents user-owned channels from accidentally becoming the stream target if the feature is enabled later.
- Channel creation is subject to spam-throttling; moderators and higher roles are exempt from the throttle enforced by SpamGuard.
- The system "live log" channel has a reserved name and is role-gated: it is visible only to configured roles regardless of membership; the name remains reserved even if the feature is disabled. Be careful when creating channels with that name.
- End-to-end encrypted channels use a different flow: `SetChannelPasswordAsync` is not available for E2E channels; to change a passphrase the service uses `RekeyChannelAsync`, which swaps the join-gate hash and the wrapped room key but does not rotate the room content key (so history remains readable to clients that can re-wrap the key).
- Channel creation is subject to throttling via [`SpamGuard`](SpamGuard.cs.md) (moderators and above are exempt) and creators are automatically added as members; callers should handle [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) responses (success/failure and error messages) rather than assuming the operation always succeeds.
---
@@ -71,15 +70,10 @@ public ChannelService(
| `logger` | `ILogger<ChannelService>` | — |
Initializes ChannelService by wiring its required collaborators into private fields for later use. The constructor accepts a scope factory, a presence tracker, a spam guard, a server logs service, and a logger, and stores them for use by the instance. In typical applications, the dependency injection container supplies these services, so ChannelService can create short-lived scopes when needed, track user presence, guard against spam, record server-side events, and emit contextual logs.
Constructs a `ChannelService` by taking its required collaborators from the dependency injection container and caching them in private fields for later use. This constructor is invoked by the DI framework when creating a `ChannelService` instance, so consumers typically rely on DI rather than invoking it directly.
## Remarks
By taking dependencies through constructor injection, ChannelService remains loosely coupled and highly testable, since test doubles can be supplied in place of real implementations. This composition root clarifies the service's responsibilities—managing channel state with awareness of presence, applying spam protection, and observability through logs.
## Notes
- If ChannelService is registered as a singleton, ensure that the injected services are thread-safe or have appropriate lifetimes; otherwise adjust registrations to avoid unsafe sharing.
- If the class creates scopes via the IServiceScopeFactory, dispose them promptly to avoid memory leaks or disposed-service access.
- Verify the DI container can resolve all dependencies at startup; a misconfiguration will surface as a runtime resolution failure.
This constructor wires together a set of collaborators required by `ChannelService`: `IServiceScopeFactory` for creating scoped services, [`PresenceTracker`](PresenceTracker.cs.md) for tracking user presence, [`SpamGuard`](SpamGuard.cs.md) for abuse protection, [`ServerLogsService`](ServerLogs/ServerLogsService.cs.md) for server-side logging, and `ILogger<ChannelService>` for structured logging. By storing these dependencies in private fields, the class remains focused on channel-related behavior while delegating infrastructure concerns to dedicated services. This separation also improves testability by allowing mocks or fakes to replace the collaborators during unit tests.
---
@@ -90,7 +84,7 @@ By taking dependencies through constructor injection, ChannelService remains loo
```csharp
public async Task<ChannelOperationResult> CreateChannelAsync(
Guid creatorUserId, string name, string? topic, bool isPublic,
string? [REDACTED:CONNECTION_STRING_PASSWORD] string? encryptionSalt = null, string? wrappedRoomKey = null)
string? password = null, string? encryptionSalt = null, string? wrappedRoomKey = null)
```
**Parameters:**
@@ -101,21 +95,22 @@ public async Task<ChannelOperationResult> CreateChannelAsync(
| `name` | `string` | — |
| `topic` | `string?` | — |
| `isPublic` | `bool` | — |
| `encryptionSalt` | `string? [REDACTED:CONNECTION_STRING_PASSWORD] string?` | `null` |
| `password` | `string?` | `null` |
| `encryptionSalt` | `string?` | `null` |
| `wrappedRoomKey` | `string?` | `null` |
**Returns:** `Task<ChannelOperationResult>`
Creates a new chat channel using the provided parameters, validating the name, enforcing reserved names, optionally handling a password (hashed) and an end-to-end encryption envelope, and persisting the channel with the creator as a member. Use this when you need to create a channel with consistent validation, security, and membership semantics.
Creates a new channel with the given `creatorUserId`, `name`, optional `topic`, visibility via `isPublic`, and optional security settings (`password`, `encryptionSalt`, `wrappedRoomKey`). It validates the input (name presence, name pattern via `ValidationConstants.ChannelNameRegex()`, and reserved names against `_serverLogs.Options.NormalizedRoomName`), ensures the channel name is unique, optionally hashes a password with BCrypt, and stores envelope data only when both `encryptionSalt` and `wrappedRoomKey` are supplied. If a password or envelope is provided, the corresponding fields are populated accordingly; otherwise they remain null. The creator automatically becomes a member, and the operation is throttled by a spam guard for non-exempt users. The method persists changes and returns a successful [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) containing a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md), or a failure with a [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) and message in cases of validation failure, duplication, or other policy violations.
## Remarks
This method centralizes all channel-creation logic, applying business rules such as name normalization (lowercasing and trimming), reserved-name protection for the log room, password requirements for encrypted channels, and spam throttling before persisting data. It leverages a scoped database context to create the channel and automatically adds the creator as a member, ensuring the creator has immediate access. The reserved log room name is enforced regardless of feature toggles, preventing accidental conflicts with system channels.
**Remarks**
This method centralizes channel creation concerns, including input validation, security policy, and persistence, so callers dont need to implement these cross-cutting concerns separately. It coordinates between domain entities ([`Channel`](../../EchoHub.Core/Models/Channel.cs.md), [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md)) and their DTOs, while enforcing organizational policies (e.g., reserved names, password requirements for encrypted channels, and anti-spam). The return shape guarantees a consistent success path with a populated [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) or a clear failure path via `ChannelOperationResult.Fail`.
## Notes
- The channel name is normalized to lowercase and trimmed, which makes channel uniqueness effectively case-insensitive.
- If an end-to-end envelope is supplied (encryptionSalt and wrappedRoomKey), a password must also be provided; otherwise creation fails with a validation error.
- A race on channel name creation is possible in highly concurrent scenarios; the code checks for existence prior to insert and relies on the database to enforce final uniqueness if necessary.
**Notes**
- Normalization and validation: the stored channel name is the lowercased, trimmed form and must pass `ValidationConstants.ChannelNameRegex()`; attempting to create a channel with a name that already exists yields `ChannelError.AlreadyExists`.
- Security coupling: if an envelope is provided, a non-empty `password` is required, and the password (if any) is hashed with BCrypt; envelope data is only stored when both `encryptionSalt` and `wrappedRoomKey` are present.
- Anti-spam policy: channel creation is guarded by `_spamGuard` (non-exempt users may be blocked for rapid creation), reinforcing rate-limiting behavior at the data access boundary.
---
@@ -138,14 +133,7 @@ public async Task<ChannelOperationResult> DeleteChannelAsync(Guid callerUserId,
**Returns:** `Task<ChannelOperationResult>`
Deletes a channel by name for a given caller, enforcing that only the channel creator or an administrator can perform the deletion and that protected/default channels cannot be removed. It normalizes the channel name, validates existence and non-system status, removes the channel from the database, saves changes, and returns a ChannelOperationResult containing a ChannelDto with the channels identity and metadata; on failure it maps to a corresponding ChannelError with a descriptive message.
## Remarks
This method encapsulates the channel-deletion policy in a single place, ensuring consistent authorization checks and error signaling across call sites. It delegates data access to EchoHubDbContext via a scoped DI container and returns a ChannelDto representing the deleted channels identity and basic attributes, which can be used by clients to refresh UI state or logs.
## Notes
- The ChannelDto is constructed after the channel row is removed and SaveChangesAsync completes, so the returned DTO serves as a confirmation of what was deleted rather than a live snapshot of a remaining entity.
Deletes a channel by name, enforcing that only the channel's creator or an administrator can perform the deletion while protecting the default and system channels. The input channel name is normalized to lower-case and trimmed, a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) is used to locate the channel, and the operation returns a [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) with a specific [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) if the channel does not exist or cannot be deleted. If authorized, the channel is removed from the `db.Channels`, changes are persisted, and a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) describing the deleted channel is returned inside a successful [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md).
---
@@ -155,7 +143,7 @@ This method encapsulates the channel-deletion policy in a single place, ensuring
```csharp
public async Task<(bool Success, string? Error, bool PasswordRequired)> EnsureChannelMembershipAsync(
Guid userId, string channelName, string? [REDACTED:CONNECTION_STRING_PASSWORD]
Guid userId, string channelName, string? password = null)
```
**Parameters:**
@@ -167,15 +155,20 @@ public async Task<(bool Success, string? Error, bool PasswordRequired)> EnsureCh
| `PasswordRequired` | `bool` | — |
Ensures that a user is granted membership to a named channel, creating or restoring the channel when appropriate, and enforcing access rules including password protection. Call this when a user attempts to join or access a channel so the system can validate eligibility, auto-provision special channels, and persist the membership relationship in the database. The method returns a tuple (Success, Error, PasswordRequired) to indicate whether entry was granted, an error message if any, and whether the caller should prompt for a password.
Ensures that a user identified by `Guid userId` becomes a member of the channel named `channelName`, creating or restoring the channel as needed, enforcing gating rules, and returning a structured result that indicates success, a possible error message, and whether a password is required for first-time joins.
## Remarks
Centralizes channel-join semantics within ChannelService, encapsulating rules around default channels, system/log channels, and password gates. It coordinates with the database context, server configuration, and validation utilities to decide whether entry should be granted, a channel recreated, or a password prompt issued. By funneling join logic through a single path, it reduces duplication and ensures consistent behavior across different join entry points (TUI, REST, IRC).
The method normalizes the channel name using `ToLowerInvariant()` and `Trim()`, then validates it with `ValidationConstants.ChannelNameRegex()`. If the name is invalid, it returns a failed result along with an error message describing the required channel name constraints. It then opens a scope and obtains an [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to inspect and modify data related to users, channels, and memberships.
If the target channel is a live-logs channel (as determined by `_serverLogs.IsLogsChannel`), the callers ability to view that channel is verified via `_serverLogs.CanView` against the users role; otherwise membership is denied.
If the channel does not exist, the method may auto-recreate it: the default channel (as defined by `HubConstants.DefaultChannel`) is recreated with safe defaults, or a logs channel is recreated with a non-public, system-owned flag and a predefined room topic. If neither special case applies, the method reports that the channel does not exist and should be created first via the channel list.
If the channel exists but is a system channel and the request is not for a logs channel, access is blocked and the join is rejected.
When the caller is not already a member, the method enforces password protection if the channel has a `PasswordHash`. If no password is supplied, it returns success = false with `PasswordRequired` set to true. If a password is supplied but is incorrect (verified via `BCrypt.Verify`), it returns the same shape with `PasswordRequired` = true. On successful password verification (or if no password is needed), a new [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md) entry is created and persisted.
The function returns a 3-tuple: `(bool Success, string? Error, bool PasswordRequired)`. A successful join yields `(true, null, false)`; otherwise, `Error` describes the failure and `PasswordRequired` signals whether a password is needed for the join.
## Notes
- Automatic channel provisioning: If the requested channel does not exist, the method may recreate the default channel or the log channel and logs a warning. Callers should not assume a static channel list.
- Password gate: For channels with a PasswordHash, a password is required on first-time joins and validated with BCrypt. The method returns PasswordRequired = true in those cases and updates membership only after successful verification.
- Database scope and side effects: The operation creates a short-lived DI scope to access EchoHubDbContext and persists changes (new ChannelMembership, and possibly a newly created Channel). Callers should be mindful of potential race conditions if multiple concurrent joins occur for the same channel.
---
@@ -196,14 +189,13 @@ private static async Task EnsureDefaultChannelAsync(EchoHubDbContext db)
**Returns:** `Task`
Ensures the application has a canonical default channel in the EchoHub database by checking the Channels collection for a channel named HubConstants.DefaultChannel and seeding one if it does not exist. This bootstrapping helper is intended to be invoked during initialization to guarantee a general discussion channel is present without duplicating the initialization logic elsewhere.
Ensures that the default channel exists in the database by checking for a channel named `HubConstants.DefaultChannel`. If none exists, it creates a new [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) with a generated `Id` (`Guid.NewGuid()`), the default name, a `Topic` of `General discussion`, and a system `CreatedByUserId` of `Guid.Empty`, then saves changes with `SaveChangesAsync`.
## Remarks
By centralizing the default-channel bootstrapping in EnsureDefaultChannelAsync, callers avoid duplicating the existence check and channel-creation code across startup paths. It ties together the Channel entity, the HubConstants default channel name, and the database context, so changes to the default channel semantics propagate from this single place. The method is private and static, reinforcing that it is an internal bootstrap concern rather than a reusable operation for callers.
Centralizes the provisioning of the default channel, letting startup and runtime logic rely on a known channel name without duplicating initialization checks. By using `HubConstants.DefaultChannel` and `Guid.Empty` as the creator, it signals that the record is system-generated and intended as a baseline rather than user-created.
## Notes
- Potential race condition under concurrent invocations: the existence check followed by insertion is not atomic, which could raise a constraint violation if two callers run at the same time.
- CreatedByUserId = Guid.Empty marks system-generated creation; auditing considerations may require handling.
- Potential race condition if this method is invoked concurrently during initialization; ensure it runs once or enforce a database constraint on `Channels.Name` to prevent duplicates.
---
@@ -225,15 +217,10 @@ public async Task<ChannelDto> EnsureSystemChannelAsync(string channelName, strin
**Returns:** `Task<ChannelDto>`
Ensures there is a system-owned channel with the specified name by normalizing the name and either creating a new system channel or converting an existing non-system channel into a system channel. It then returns a ChannelDto describing the channels identity, topic, visibility, and system status.
Ensures that a system channel with the specified name exists in the database by normalizing the name and looking it up. If none is found, it creates a new system channel (not public) with CreatedByUserId set to an empty GUID and logs its creation. If a non-system channel already exists with that name, it is claimed as a system channel by updating its IsSystem and IsPublic flags and clearing the PasswordHash, logging a warning. It returns a ChannelDto describing the channel's identity and status.
## Remarks
Guarantees a canonical system channel identity for internal communications and server content streaming. It encapsulates the create-or-claim logic behind a single API and logs whether a channel was created or claimed. If the target channel already exists and is already marked as system, the method is effectively a no-op and simply returns its ChannelDto.
## Notes
- There is a potential race condition when two concurrent invocations try to create the same system channel; relying on database constraints or proper isolation is recommended to avoid duplicates.
- If a non-system channel exists with the same name, the code will convert it to a system channel by setting IsSystem = true, IsPublic = false, and clearing PasswordHash; CreatedAt remains the original timestamp.
- The channel name is lower-cased and trimmed before the lookup, so callers should not rely on case-sensitive or whitespace-sensitive channel naming.
This method centralizes the architectural concept of system channels by guaranteeing a canonical system channel for a given name, creating or reclaiming it as needed and thereby preventing user-owned channels from shadowing system channels with reserved identifiers.
---
@@ -254,15 +241,14 @@ public async Task<ChannelDto?> GetChannelByNameAsync(string channelName)
**Returns:** `Task<ChannelDto?>`
GetChannelByNameAsync fetches a channel by its name after normalizing the input to lowercase and trimming whitespace. It creates a new DI scope to obtain EchoHubDbContext, queries the Channels set for a channel whose Name matches the normalized input, and, if found, counts the number of Messages belonging to that channel. If no matching channel exists, it returns null. The returned ChannelDto includes the channels Id, Name, Topic, visibility (IsPublic), the total MessageCount, CreatedAt timestamp, and two boolean flags indicating whether a password hash exists and whether a WrappedRoomKey is present, plus whether the channel is a system channel. This method centralizes the data-shaping of channel metadata for consumers (e.g., channel listings or details) and hides direct EF queries behind a concise API.
Fetches a channel by name in a case-insensitive manner and returns a compact [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) that includes the channels identity, metadata, and the current message count. It normalizes the input, creates a short-lived DI scope to obtain the [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), resolves the channel by its lowercased name, counts its related [`Message`](../../EchoHub.Core/Models/Message.cs.md)s, and returns a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) populated with the channels id, name, topic, visibility, created timestamp, and flags indicating whether a password or a wrapped room key exists, plus whether it is a system channel. If no channel matches, it returns `null`.
## Remarks
This abstraction centralizes channel metadata retrieval for UI and API surfaces, ensuring consistent ChannelDto shaping and hiding data-access details behind a single, strongly-typed API. It also clarifies that a null return indicates a non-existent channel.
By encapsulating the read path behind `GetChannelByNameAsync`, callers avoid dealing with EF queries or DI lifetimes directly. It centralizes how channel metadata is retrieved and projected into a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md), which helps maintain consistent data contracts across the application. The per-call scope ensures proper disposal of the [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and aligns with typical request-scoped lifetimes.
## Notes
- Returns null when no channel matches the provided name; callers should handle the nullable result.
- Performs two database queries (FirstOrDefaultAsync for the channel, then CountAsync for its messages) when a channel exists; this is straightforward but has a potential perf cost.
- Relies on input normalization to lowercase; if stored channel names are not stored in a comparable form, the lookup could miss matches.
- Potential ambiguity if multiple channels share the same normalized name; `FirstOrDefaultAsync` may return any one of them.
- Two database round-trips per invocation: one to fetch the channel and another to count its messages; consider combining into a single query if profiling shows this as a bottleneck.
---
@@ -283,7 +269,42 @@ public async Task<ChannelCryptoDto?> GetChannelCryptoAsync(string channelName)
**Returns:** `Task<ChannelCryptoDto?>`
Retrieves the ChannelCryptoDto describing the cryptographic state of a channel. The method normalizes the input channel name to lowercase and trims whitespace, then queries the EchoHubDbContext for a Channel with the matching name. If no channel is found, it returns null. If a channel exists, it returns a ChannelCryptoDto where the first value indicates whether a WrappedRoomKey is present (WrappedRoomKey != null) and includes the channel's EncryptionSalt. Data access occurs within a short-lived DI scope created from _scopeFactory, resolving EchoHubDbContext for the lookup.
GetChannelCryptoAsync retrieves the cryptographic metadata for a named channel. It normalizes the input channel name by lowercasing and trimming, opens a short-lived scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), and queries the `Channels` set for a channel whose `Name` matches the normalized value. If the channel is found, it returns a [`ChannelCryptoDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) whose first component indicates whether a `WrappedRoomKey` is present and whose second component carries the channel's `EncryptionSalt`; if no channel matches, it returns null.
## Remarks
By encapsulating this logic in a dedicated method, callers avoid duplicating the database query and the cryptographic-state interpretation across the codebase. It centralizes encryption-metadata access behind a simple, asynchronous call and uses a scoped DbContext to minimize lifetime and concurrency issues.
## Notes
- Returns null when the channel does not exist.
- The first component of [`ChannelCryptoDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) indicates the presence of a `WrappedRoomKey`; the `EncryptionSalt` may be null depending on data, so callers should handle null salts.
---
### GetChannelKeyEnvelopeAsync
> **File:** `src/EchoHub.Server/Services/ChannelService.cs`
> **Kind:** method
```csharp
public async Task<(string? EncryptionSalt, string? WrappedRoomKey)> GetChannelKeyEnvelopeAsync(string channelName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `EncryptionSalt` | `string?` | — |
| `WrappedRoomKey` | `string?` | — |
Gets the encryption envelope for a channel by name. It normalizes the input with `ToLowerInvariant()` and `Trim()`, opens a short-lived DI scope to resolve [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), and queries the `Channels` set for a channel whose `Name` matches. It returns a tuple of the channel's `EncryptionSalt` and `WrappedRoomKey` (as `string?`); if no matching channel exists, both values are `null`.
## Remarks
This method centralizes access to channel encryption metadata and hides the details of DI-scoped DbContext usage from callers. It provides a single, easy-to-consume envelope for encryption-related data, which is useful when preparing to decrypt or unwrap channel-specific material. By returning `(string? EncryptionSalt, string? WrappedRoomKey)` as nullable values instead of throwing when a channel is absent, callers must handle the absence gracefully.
## Notes
- Returns `(null, null)` when the channel cannot be found.
- Each invocation creates a new DI scope, which is appropriate for isolated data access but may have perf implications in hot paths; consider scope management or caching at a higher level if this method is called frequently.
---
@@ -298,7 +319,13 @@ public async Task<List<ChannelListItem>> GetChannelListAsync()
**Returns:** `Task<List<ChannelListItem>>`
Fetches and returns a list of channel summaries. The method creates a scoped DI container, reads the EchoHubDbContext, loads all channels ordered by name, and maps each channel to a ChannelListItem that includes the channel's name, topic, the number of online users in that channel (via the presence tracker), whether the channel is public, and whether a password is set. This is typically used to populate a channel directory or lobby UI with up-to-date channel metadata and presence information.
GetChannelListAsync asynchronously loads all channels from the database, orders them by `Name`, and projects each channel into a [`ChannelListItem`](../../EchoHub.Core/Contracts/IChannelService.cs.md) that includes the channel's `Name`, `Topic`, the current online user count from `_presenceTracker.GetOnlineUsersInChannel(c.Name).Count`, the public status (`c.IsPublic`), and whether a password is configured (`c.PasswordHash != null`). The method returns a `List<ChannelListItem>` suitable for rendering a channel catalog in a UI or API response.
## Remarks
GetChannelListAsync acts as an orchestrator between the persistent store ([`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md)) and the inmemory presence tracker (`_presenceTracker`). It centralizes channel-list assembly so callers don't need to know how presence counts are computed or how channels are stored. By resolving [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) within a shortlived scope via `_scopeFactory.CreateScope()`, it ensures proper disposal of the database context per invocation and keeps DI concerns isolated from consumer code.
## Notes
- Presence counts are computed per channel; listing many channels may impact response time. If the channel catalog grows large, consider caching or batching presence data to improve responsiveness.
---
@@ -319,7 +346,40 @@ public async Task<ChannelMetaDto?> GetChannelMetaAsync(string channelName)
**Returns:** `Task<ChannelMetaDto?>`
Fetches channel-level metadata for a given channel name without returning the messages themselves. It normalizes the input by lowercasing and trimming, resolves the channel via a scoped DI context, and if the channel exists returns a ChannelMetaDto containing the channel's Id, normalized Name, Topic, flags indicating whether a WrappedRoomKey or PasswordHash exists, the total MessageCount, the distinct count of Senders, an estimated storage footprint for the channel (attachments plus text), and the channel's CreatedAt timestamp. If no channel matches the provided name, the method returns null. The operation executes within a scoped DI context to ensure proper disposal of the database context.
GetChannelMetaAsync retrieves the metadata for a channel by its name and returns a [`ChannelMetaDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) (or null if the channel cannot be found). It normalizes the input with `ToLowerInvariant()` and `Trim()`, opens a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) via `_scopeFactory.CreateScope()`, and looks up the channel in `db.Channels` by `Name`. When found, it computes the total `messageCount` from `db.Messages.CountAsync(...)`, the number of distinct `SenderUserId`s, and the estimated on-disk footprint from attachments and message text, then returns a new [`ChannelMetaDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) containing `c.Id`, `c.Name`, `c.Topic`, booleans for `c.WrappedRoomKey != null` and `c.PasswordHash != null`, the counts, the total footprint, and `c.CreatedAt`.
## Remarks
This method uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to perform multiple read-only queries and aggregates data from `db.Channels` and `db.Messages`. The returned booleans reflect whether `c.WrappedRoomKey` or `c.PasswordHash` are non-null, indicating encryption and access protection. For encrypted channels, the footprint uses ciphertext sizes to reflect on-disk cost, and sender identities are treated as metadata preserved by the server even when messages are encrypted.
## Notes
- Callers must handle the possibility that the return value is `null` when no channel matches the given `channelName`.
---
### GetChannelTopicAsync
> **File:** `src/EchoHub.Server/Services/ChannelService.cs`
> **Kind:** method
```csharp
public async Task<(string? Topic, bool Exists)> GetChannelTopicAsync(string channelName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Topic` | `string?` | — |
| `Exists` | `bool` | — |
GetChannelTopicAsync retrieves the topic for a channel identified by `channelName`. It normalizes the input by calling `ToLowerInvariant()` and `Trim()`, opens a short-lived DI scope to resolve [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), queries the `Channels` set for a channel whose `Name` equals the normalized value using `FirstOrDefaultAsync`, and returns the `(Topic, Exists)` tuple; if no channel exists, it returns `(null, false)`.
## Remarks
Encapsulates a small piece of data access behind a scoped context, avoiding long-lived DbContext usage and centralizing the normalization logic for channel lookups. The API communicates existence via the `Exists` flag, while the `Topic` can still be `null` if a channel exists but has no topic set.
## Notes
- The lookup uses `FirstOrDefaultAsync` on `db.Channels`; if more than one channel shares the same normalized `Name`, the returned topic is non-deterministic; enforce unique `Name` values to avoid surprises.
- A per-call DI scope is created to obtain [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md); callers should not rely on an ambient scope for this operation.
---
@@ -342,15 +402,10 @@ public async Task<PaginatedResponse<ChannelDto>> GetChannelsAsync(Guid userId, i
**Returns:** `Task<PaginatedResponse<ChannelDto>>`
Fetches a paginated list of channels visible to the specified user, ensuring a default channel exists and applying system-channel visibility rules. It builds a Page of ChannelDto items by filtering channels based on whether they are system channels (only visible if the caller has the appropriate server role) or non-system channels (visible if public or if the user is a member). The method returns a PaginatedResponse containing the channels and the total count, ordered with system channels first and then by name. Per-channel metadata includes the number of messages, creation time, and security flags such as whether a password is set or a wrapped room key is present.
GetChannelsAsync returns a paginated list of channels visible to the user identified by `userId`. It first ensures a default channel exists, then determines if the caller can view system channels via `_serverLogs.CanView(caller?.Role ?? ServerRole.Member)`, and finally queries `db.Channels` to surface system channels only when permitted or non-system channels that are public or where the user is a member (via `ChannelMemberships`). The results are ordered with system channels first, then by `Name`, and projected into [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) objects containing each channels `Id`, `Name`, `Topic`, `IsPublic`, `Messages.Count`, `CreatedAt`, and flags for `PasswordHash != null` and `WrappedRoomKey != null`, plus `IsSystem`. The method returns a [`PaginatedResponse<ChannelDto>`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) with the current page of channels and the total count.
## Remarks
This method centralizes channel discovery and visibility logic used by API surfaces and the UI. By enforcing system-channel visibility through server-side role checks and by materializing concise per-channel data into ChannelDto, callers receive a consistent, paged view of channels while preserving the default channel guarantee. The use of a scoped DbContext and a two-phase query (total count, then page fetch) encapsulates the data-access concerns behind a single, well-defined operation.
## Notes
- EnsureDefaultChannelAsync(db) may create the default channel if it is missing; this side effect occurs on every call. callers should be aware of potential writes on read-like operations.
- The total and page fetch are executed as separate queries; data may change between these calls, affecting the reported total and the returned page.
- The channel's Messages.Count is computed in the projection, yielding a per-channel count without loading full message collections.
GetChannelsAsync centralizes the channel visibility policy: system channels (the live log room) are exposed only to users whose role allows viewing server logs, while non-system channels are visible if they are public or the user is a member, as determined by `ChannelMemberships`. The results are produced from a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and are ordered to surface system channels first, then alphabetically by name, and are projected into lightweight [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) records to drive UI lists without leaking unnecessary data. This encapsulation ensures consistent, permission-aware channel listing across the application.
---
@@ -378,14 +433,11 @@ public async Task<ChannelOperationResult> RekeyChannelAsync(Guid callerUserId, s
**Returns:** `Task<ChannelOperationResult>`
RekeyChannelAsync rotates the passphrase for an end-to-end encrypted channel by swapping the join-gate hash and the wrapped room key, while the actual content key remains unchanged so the history stays readable. The client then re-wraps the content key under the new passphrase-derived key. This operation is restricted to the channel creator; administrators who do not know the current passphrase cannot perform a rekey.
RekeyChannelAsync rekeys an end-to-end encrypted channel by swapping the `join-gate` hash and the `WrappedRoomKey`, re-wrapping the channel's content key under the new passphrase-derived key while leaving the content key itself unchanged so historical messages remain decryptable. The operation is restricted to the channel creator; admins cannot rekey a room unless they know the current passphrase.
## Remarks
This method encapsulates a security-sensitive transition that updates credential material without discarding encrypted content. By validating the new passphrase (via ValidateChannelPassword) and requiring non-empty new salt and wrapped key before touching the database, it preserves both confidentiality and integrity. The operation executes in a scoped data context to ensure the channel state is read and persisted atomically, reflecting the latest creator-approved configuration while keeping history intact.
Passphrase changes are validated, and the operation returns a [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) that is either a success containing a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) or a failure with a [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) and message (e.g. `NotFound`, `ValidationFailed`, or `Forbidden`). Internally, the method uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to locate the channel by name, ensure the channel is end-to-end encrypted, verify the caller is the creator, check the old password, and persist updates to `PasswordHash`, `EncryptionSalt`, and `WrappedRoomKey`. Upon success, it computes the current message count and returns a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) reflecting the updated credentials.
## Notes
- New password, salt, and wrapped key are validated before any changes are persisted; if validation fails, the operation aborts with a ValidationFailed result.
- Rekeying is restricted to the channel creator; the method enforces this by verifying the caller's user ID and the correctness of the current passphrase before applying changes.
This operation centralizes the sensitive rekey workflow and ensures the channel state remains consistent and auditable within a single database transaction.
---
@@ -409,18 +461,15 @@ public async Task<ChannelOperationResult> SetChannelPasswordAsync(Guid callerUse
**Returns:** `Task<ChannelOperationResult>`
Sets, changes, or clears (null) a channel's join password. This operation is allowed only for the channel's creator or an administrator. End-to-end encrypted channels do not accept password changes here; such channels must use RekeyChannelAsync to rotate the passphrase, preserving the room key envelope.
Sets, changes, or clears (null) a channel's join password. Creator or admin only. Not available on end-to-end encrypted channels — those change passphrase via RekeyChannelAsync so the room key envelope stays consistent. The method normalizes the channel name to lowercase and trims, validates the password via `ValidateChannelPassword`, and then uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to locate the channel and enforce authorization. If the channel doesn't exist, is a system channel, or is end-to-end encrypted, it returns an appropriate [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) failure. If the caller is the channel creator or an admin, it updates the channel's `PasswordHash` (hashing a non-null password with `BCrypt.Net.BCrypt.HashPassword` or clearing it when `password` is null), persists the changes, counts the channel's messages, and returns a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) describing the channel along with flags for password protection and encryption.
## Remarks
By centralizing password management in this method, the system enforces consistent authorization, validation, and persistence rules for channel passphrases. It guards against modifying system channels and avoids altering encryption state for end-to-end encrypted channels at this layer, delegating that concern to RekeyChannelAsync when appropriate. The method returns a ChannelDto describing the updated channel, including whether a password is set and whether the channel remains end-to-end encrypted.
Centralizes channel password management behind a single operation that enforces ownership and role-based access. It interacts with the EF Core context to fetch and persist channel state and to surface up-to-date metadata via [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) (including whether a password is active and whether the channel is end-to-end encrypted). The method explicitly avoids modifying end-to-end encrypted channels here, directing such changes to `RekeyChannelAsync` to preserve the room key envelope.
## Notes
- Clearing the password (passing `null`) removes the join password, which may affect who can join depending on the channel's other visibility settings.
- Only the channel creator or an admin can perform password changes; otherwise the call returns `ChannelError.Forbidden`.
- Passwords are stored as BCrypt hashes; if a null password is provided, the password is cleared (PasswordHash becomes null).
- The channel name is normalized to lowercase and trimmed before lookup to ensure stable, case-insensitive matching.
- If the channel does not exist, is a system channel, or the caller lacks sufficient privileges (not the creator or an admin), the operation fails with NotFound, Protected, or Forbidden respectively.
- After a successful change, the returned ChannelDto includes the current message count and flags indicating HasPassword and HasWrappedKey, reflecting the channel's encryption state.
---
@@ -444,15 +493,16 @@ public async Task<ChannelOperationResult> UpdateTopicAsync(
**Returns:** `Task<ChannelOperationResult>`
Updates the topic of a channel, performing authorization, validation, and persistence in one operation. Given the caller's user ID and the channel name, it normalizes the name, enforces topic length (when provided), ensures only the channel creator can update, persists the topic change, and returns a ChannelOperationResult containing a ChannelDto with the channel's identity, current topic, visibility, message count, creation time, and indicators for password protection and wrapped room key.
Updates the topic of a channel by name, but only if the caller is the channel's creator. It trims and validates a non-null `topic` against `ValidationConstants.MaxChannelTopicLength` (a null `topic` clears the topic), normalizes the channel name to lower-case, persists the change via EF Core, and returns a `ChannelOperationResult.Success` with a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) containing the updated channel data plus a live `MessageCount`. If the channel is missing, or the caller isn't the creator, or the topic is too long, the method returns a corresponding failure via `ChannelOperationResult.Fail` with an appropriate [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md).
## Remarks
This method centralizes the domain logic for updating a channel topic behind a service boundary. It enforces the business rule that only the channel creator may modify the topic, and it uses a scoped DbContext to apply the change, ensuring consistency with the data-access layer. The returned ChannelDto exposes a compact snapshot of the channel, including whether the channel is password-protected and whether a wrapped room key exists, which informs UI decisions without leaking internal state.
Only the channel creator can update the topic, enforced by comparing `dbChannel.CreatedByUserId` to `callerUserId`. The method uses a short-lived DI scope to fetch [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), performs a read of the channel by name, applies the update, saves changes, and then counts the channel's `Messages` to populate the `MessageCount` in the returned [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md). The [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) also exposes security-related flags derived from `PasswordHash` and `WrappedRoomKey` to help clients adjust their UI and access logic.
## Notes
- Topic can be null to clear the current topic (the code stores topic?.Trim()).
- Channel name normalization is applied so lookups are case-insensitive and consistent.
- The operation yields concrete failure codes (NotFound, Forbidden, ValidationFailed) to guide callers in handling user feedback.
- The `MessageCount` is retrieved via `db.Messages.CountAsync(m => m.ChannelId == dbChannel.Id)` after applying the update; for very active channels this can add latency.
- Passing a `null` `topic` clears the topic; callers should handle potential null values in the UI.
---
@@ -473,83 +523,15 @@ private static string? ValidateChannelPassword(ref string? password)
**Returns:** `string?`
Normalizes and validates a channel password. If the input password is null or consists only of whitespace, it is treated as no password (the value is effectively normalized to null) and no error is produced. For non-empty input, the method enforces length constraints defined by ValidationConstants and returns an error message when the password is too short or too long; otherwise, it returns null to indicate a valid password. The password is passed by reference, allowing the caller to observe and adopt the normalized value in place.
Normalizes a provided channel password by treating whitespace-only input as the absence of a password (`null`) and then enforces length constraints from [`ValidationConstants`](../../EchoHub.Core/Constants/ValidationConstants.cs.md) (minimum via `MinChannelPasswordLength`, maximum via `MaxPasswordLength`). It returns an error message when the password is too short or too long, or `null` when the value is valid.
## Remarks
Centralizes the channel password policy so all call sites apply the same minimum and maximum length rules and the same interpretation of an empty password. The implementation defers to ValidationConstants for policy values, ensuring changes to password requirements propagate consistently. The use of a ref parameter enables in-place normalization, so the normalized password (or its absence) is visible to the caller without requiring a separate assignment.
By using a `ref` parameter for `password`, the input variable may be mutated to `null` by the callee to reflect the decision that no password is set. This centralizes channel password rules in one place, ensuring consistent behavior across channel creation and update flows.
## Notes
- The caller must pass a mutable variable by ref; passing a constant or read-only expression will not compile.
- The method returns null when the password is valid (or when treated as no password), or a non-null string containing the user-facing validation message when invalid.
---
## GetChannelKeyEnvelopeAsync
> **File:** `src/EchoHub.Server/Services/ChannelService.cs`
> **Kind:** method
```csharp
public async Task<(string? EncryptionSalt, string? WrappedRoomKey)> GetChannelKeyEnvelopeAsync(string channelName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `EncryptionSalt` | `string?` | — |
| `WrappedRoomKey` | `string?` | — |
Gets the encryption envelope for a given channel by name by querying the Channels table via a scoped EchoHubDbContext; it returns the channel's EncryptionSalt and WrappedRoomKey as a tuple, or (null, null) if the channel cannot be found. This method is intended for scenarios where callers need to access per-channel cryptographic parameters to decrypt or initialize channel data, without surfacing the data-access details to higher layers.
## Remarks
Encapsulates a small, cohesive data-access operation and hides EF Core/DI plumbing from callers. By creating a scoped scope and resolving EchoHubDbContext per call, it avoids leaking a long-lived DbContext into consumer code and makes the envelope retrieval occur in a single boundary. It relies on the Channels table's Name field to identify a channel and returns two optional values, allowing callers to decide how to handle missing encryption data. This placement fits ChannelService as a dedicated place to retrieve channel-related metadata used by encryption/decryption flows.
## Notes
- Caller must handle possible nulls in both EncryptionSalt and WrappedRoomKey; if the channel isn't found, both will be null.
- Since the input channelName is lowercased before querying, ensure channel.Name storage is consistent (lowercase) to guarantee matches; otherwise, the lookup could miss existing channels.
---
## GetChannelTopicAsync
> **File:** `src/EchoHub.Server/Services/ChannelService.cs`
> **Kind:** method
```csharp
public async Task<(string? Topic, bool Exists)> GetChannelTopicAsync(string channelName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Topic` | `string?` | — |
| `Exists` | `bool` | — |
Retrieves the topic for a named channel from the EchoHub database. The method normalizes the input (lowercases and trims), opens a scoped DI context to resolve EchoHubDbContext, and queries the Channels set for a channel with the matching name. If no channel is found, it returns (null, false); otherwise it returns the channel's Topic along with true, indicating the channel exists. The operation is asynchronous, allowing callers to await the database query without blocking.
## Remarks
This method encapsulates a small, focused data-access concern: turning a channel name into its topic, while also signaling whether the channel exists. Returning a value tuple (Topic, Exists) makes it straightforward for call sites to branch logic without null checks against the channel entity. The DI-scoped DbContext use ensures clean disposal per call and aligns with standard EF Core usage in a DI-driven application.
## Example
```csharp
var (topic, exists) = await GetChannelTopicAsync("general");
if (exists)
{
Console.WriteLine(topic);
}
else
{
Console.WriteLine("Channel not found.");
}
```
## Notes
- The lookup lowercases the channel name; ensure stored channel names are normalized the same way to guarantee matches.
- Topic can still be null even when Exists is true; callers should handle null topics gracefully.
- If multiple channels share the same name (data integrity issue), FirstOrDefaultAsync returns the first match.
- Because the parameter is `ref`, the caller should re-read the original variable after the call because its value may have been changed to `null`.
- A return value of `null` indicates a valid or absent password; non-null strings are error messages describing the violation.
---