feat(server)!: require a logged-in account for every channel request

Drop unauthenticated guests from channels: the allow_guests setting, the
/guests endpoints and the guest seat table are gone, and requests without an
account session get 401 account_required. Share links are unaffected.
Contract version is now 8.0.0.
This commit is contained in:
justme committed 2026-10-16 06:46:15 +08:00
1 parent b1ac11e1d7
commit c858bd2a09
31 files changed
+194 -358

No files matched your search

+4 -5
View File
@@ -10,7 +10,7 @@ public sealed partial class ChannelService
{
var channel = tx.GetChannel(id)!;
var role = tx.GetChannelMember(id, username)?.Role == "owner" ? Role.Owner : channel.WriteHash is not null ? Role.Write : Role.Read;
return ToInfo(channel, tx.CountMembers(id), new Principal(role, [], username));
return ToInfo(channel, tx.CountMembers(id), new Principal(role, username));
}).ToList());
private static WorkspaceState NewWorkspace(string username, long now)
{
@@ -19,7 +19,7 @@ public sealed partial class ChannelService
}
public WorkspaceState GetWorkspace(Id16 id, Credentials creds) => store.Read(tx =>
{
var (space, principal) = Authenticate(tx, id, creds, SeatRule.Required);
var (space, principal) = Authenticate(tx, id, creds, Membership.Required);
var state = tx.GetWorkspace(id) ?? new WorkspaceState();
if (principal.AccountName != space.CreatorAccount)
{
@@ -42,8 +42,7 @@ public sealed partial class ChannelService
{
var state = store.Write(tx =>
{
var (channel, principal) = Authenticate(tx, id, creds, SeatRule.Required);
if (requireAccount && principal.AccountName is null) throw new WebDropException(ErrorCode.AccountRequired);
var (channel, principal) = Authenticate(tx, id, creds, Membership.Required);
if (requireAccount && principal.Role != Role.Owner) throw new WebDropException(ErrorCode.Forbidden);
if (!ChannelPolicy.CanWrite(channel, principal)) throw new WebDropException(ErrorCode.Forbidden);
var current = tx.GetWorkspace(id) ?? new WorkspaceState();
@@ -97,7 +96,7 @@ public sealed partial class ChannelService
if (!Id16.TryParse(itemId, out var parsed)) throw new WebDropException(ErrorCode.BadRequest);
var item = tx.GetItem(id, parsed) ?? throw new WebDropException(ErrorCode.NotFound);
if ((item.Flags & (FrameFlags.Control | FrameFlags.Snapshot)) != 0) throw new WebDropException(ErrorCode.BadRequest);
if (principal.Role != Role.Owner && (item.AuthorAccount is not null ? item.AuthorAccount != principal.AccountName : !item.AuthorHash.AsSpan().SequenceEqual(principal.EndpointHash))) throw new WebDropException(ErrorCode.Forbidden);
if (principal.Role != Role.Owner && item.AuthorAccount != principal.AccountName) throw new WebDropException(ErrorCode.Forbidden);
state.Locations.RemoveAll(l => l.ItemId == itemId);
state.Locations.Add(new WorkspaceLocation { ItemId = itemId, ThreadId = threadId, FolderId = folderId, AuthorAccount = item.AuthorAccount });
}