M5: moderation, permissions, channel CRUD, in-app account management

- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).

- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.

- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).

- vccli flags for all M5 operations plus --username/--password auth.

- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.

- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
This commit is contained in:
2026-06-17 15:08:05 +02:00
parent a2f159e971
commit 3990f63f0f
23 changed files with 3281 additions and 97 deletions

View File

@@ -23,6 +23,7 @@
#define ASIO_STANDALONE 1
#include <asio.hpp>
#include "db.h"
#include "proto/voicecat.pb.h"
namespace voicecat::server {
@@ -48,10 +49,10 @@ struct UdpEndpointHash {
class SessionRegistry {
public:
SessionRegistry() = default;
explicit SessionRegistry(std::shared_ptr<Database> db);
// Create the default "Lobby" channel (id=1, permanent). Call once at startup.
void init_default_channels();
// Load channels from the database, seeding defaults on first run.
void load_channels();
// Register a session (before auth). Returns the assigned session_id.
uint64_t register_session(std::weak_ptr<ConnSession> session);
@@ -71,6 +72,8 @@ class SessionRegistry {
// Snapshot for ServerStateSnapshot message.
std::vector<voicecat::v1::Channel> channel_snapshot() const;
std::vector<voicecat::v1::User> user_snapshot() const;
std::optional<voicecat::v1::User> user_snapshot_user(uint32_t user_id) const;
std::optional<std::string> user_nickname(uint32_t user_id) const;
// Resolve target sessions for a text message relay.
std::vector<std::shared_ptr<ConnSession>> resolve_text_targets(
@@ -79,7 +82,56 @@ class SessionRegistry {
// Broadcast an envelope to all sessions except the excluded one.
void broadcast(const voicecat::v1::Envelope& env, uint64_t exclude_session_id = 0) const;
// ── M2: UDP / media ──────────────────────────────────────────────────────
private:
void broadcast_unlocked(const voicecat::v1::Envelope& env,
uint64_t exclude_session_id = 0) const;
public:
// ── Permissions ────────────────────────────────────────────────────────────
void set_session_permissions(uint64_t session_id,
const voicecat::v1::Permissions& perms);
std::optional<voicecat::v1::Permissions> get_session_permissions(
uint64_t session_id) const;
// ── Moderation ─────────────────────────────────────────────────────────────
// Find a live session by its user_id. Returns nullptr if offline.
std::shared_ptr<ConnSession> find_session_by_user_id(uint32_t user_id) const;
// Forcibly disconnect a user with a reason. Broadcasts UserEvent::LEFT.
// Returns true if the user was online.
bool kick_user(uint32_t user_id, const std::string& reason);
// Kick a user and insert a persistent ban. Returns true if the user was online.
bool ban_user(uint32_t user_id, const std::string& reason, int64_t expires_at);
// Set server-mute/deafen flags on a user and broadcast the update.
bool set_server_mute(uint32_t user_id, bool muted, bool deafened);
// Move a user to a channel (permission-checked by caller).
bool move_user(uint32_t user_id, uint32_t channel_id);
// ── Channel CRUD ───────────────────────────────────────────────────────────
// Create a channel. Returns the new channel id, or 0 on error.
uint32_t create_channel(const voicecat::v1::Channel& ch, const std::string& password,
std::string& error);
// Update a channel. Returns false on error.
bool update_channel(const voicecat::v1::Channel& ch, const std::string& password,
std::string& error);
// Delete a channel. Remaining users are moved to Lobby (id=1). Returns false on error.
bool delete_channel(uint32_t channel_id, std::string& error);
// Return a channel proto by id, or nullopt.
std::optional<voicecat::v1::Channel> get_channel(uint32_t channel_id) const;
// Check a channel password.
bool check_channel_password(uint32_t channel_id, const std::string& password) const;
// ── M2: UDP / media ────────────────────────────────────────────────────────
// Register a session's UDP token (called at auth success).
void register_udp_token(const std::array<uint8_t, 16>& token, uint64_t session_id);
@@ -99,7 +151,7 @@ class SessionRegistry {
// Add/replace a stream entry on a user (called when StreamAnnounce succeeds).
// Returns the updated User proto for broadcasting, or nullopt if user not found.
std::optional<voicecat::v1::User> set_user_stream(uint32_t user_id,
const voicecat::v1::StreamInfo& info);
const voicecat::v1::StreamInfo& info);
// Remove a stream entry from a user (called on StreamStop). Returns the updated
// User proto for broadcasting, or nullopt if user not found.
@@ -119,15 +171,20 @@ class SessionRegistry {
std::optional<voicecat::v1::AudioConfig> channel_audio_config(uint32_t channel_id) const;
private:
void seed_default_channels();
mutable std::shared_mutex mu_;
std::shared_ptr<Database> db_;
uint64_t next_session_id_{1};
uint32_t next_user_id_{1};
uint32_t next_channel_id_{2}; // 1 is reserved for Lobby
uint32_t next_channel_id_{3}; // 1 and 2 are reserved for Lobby, Music Room
std::unordered_map<uint64_t, std::weak_ptr<ConnSession>> sessions_;
std::unordered_map<uint32_t, UserEntry> users_;
std::unordered_map<uint32_t, ChannelEntry> channels_;
std::unordered_map<uint64_t, voicecat::v1::Permissions> session_permissions_;
// M2: token → session_id (populated at auth, cleared on disconnect)
struct TokenHash {
@@ -150,5 +207,5 @@ class SessionRegistry {
} // namespace voicecat::server
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_SERVER_SESSION_REGISTRY_H
#endif // VOICECAT_SERVER_SESSION_REGISTRY_H